diff --git a/README.md b/README.md index 2370e77..259cf1c 100644 --- a/README.md +++ b/README.md @@ -112,11 +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 -- **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). +- **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. 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 +728,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 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 +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. 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 @@ -742,24 +741,22 @@ 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 on glass.** Text colours come from +21. **Dark mode is token-driven, and contrast is audited.** Text colours come from `--text` / `--text-muted` / `--text-subtle` (never hardcoded), and a `contrast.mjs` audit checks - 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. + 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. 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 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 +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, deliberately almost flat.** 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 (`.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. + 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. 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 @@ -795,17 +792,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 - glacier cyan, the ice scene uses the artwork's blues, the primary CTA is an icy gradient, and + 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 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 4e6d953..112f1c3 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.

diff --git a/app/globals.css b/app/globals.css index 50cda71..5672ee2 100644 --- a/app/globals.css +++ b/app/globals.css @@ -17,17 +17,17 @@ --color-frost-900: #1d2740; --color-frost-950: #0e1526; - /* Accent: glacier cyan to ice blue — focus rings, links, highlights. ---- */ - --color-accent-50: #ecf9ff; - --color-accent-100: #d6f1ff; - --color-accent-200: #b0e5ff; - --color-accent-300: #7dd4fc; - --color-accent-400: #38bdf8; - --color-accent-500: #0ea5e9; - --color-accent-600: #0284c7; - --color-accent-700: #0369a1; - --color-accent-800: #075985; - --color-accent-900: #0c4a6e; + /* Accent: the icon's snowflake blue. ------------------------------------ */ + --color-accent-50: #eff7ff; + --color-accent-100: #dbecff; + --color-accent-200: #bcdcff; + --color-accent-300: #8fc6ff; + --color-accent-400: #59a8f7; + --color-accent-500: #3f95ea; + --color-accent-600: #2478d2; + --color-accent-700: #1d61ab; + --color-accent-800: #1c518b; + --color-accent-900: #1b4473; /* Aurora hues used only for the frosted backdrop wash. ----------------- */ --color-aurora-mint: #9fe3d0; @@ -81,110 +81,93 @@ --------------------------------------------------------------------------- */ :root { color-scheme: light; - --page: #eef5fd; + --page: #eef3fa; /* pale ice */ --sidebar: #ffffff; --surface: #ffffff; --surface-raised: #ffffff; - --surface-strong: rgb(255 255 255 / 0.72); /* nested glass: inputs, chips */ - --card-hover-bg: rgb(255 255 255 / 0.72); - --surface-sunken: rgb(12 32 62 / 0.06); - --hairline: #d5e4f4; - --card-border: var(--glass-border); - --sidebar-divider: rgb(255 255 255 / 0.75); - --hairline-soft: rgb(12 32 62 / 0.08); - --glass-tint: rgb(255 255 255 / 0.55); - --text: #0b1830; /* 16.6:1 on #eef5fd */ - --text-muted: #46586f; /* 6.6:1 on light glass */ - --text-subtle: #566a84; /* 5.0:1 on light glass */ - --hover: rgb(12 32 62 / 0.05); - --focus: #0ea5e9; - - /* Frosted ice glass — one shared recipe for every large surface. */ - --glass-bg: rgb(255 255 255 / 0.55); - --glass-border: rgb(255 255 255 / 0.8); - --glass-highlight: rgb(255 255 255 / 0.85); - --glass-shadow: 0 20px 50px -20px rgb(30 64 175 / 0.35); - --glass-solid-bg: rgb(255 255 255 / 0.8); /* question card: more opaque */ - --glass-fallback: rgb(247 251 255 / 0.97); /* no backdrop-filter */ - --nav-active-bg: rgb(255 255 255 / 0.75); - --nav-active-border: rgb(56 189 248 / 0.5); + --surface-strong: #f2f6fc; + --card-hover-bg: #ffffff; + --surface-sunken: rgb(14 21 40 / 0.05); + --hairline: #dce4f1; + --card-border: rgb(255 255 255 / 0.85); + --sidebar-divider: #dfe7f3; + --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 */ + --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); /* API-key gate: cool near-white stage with soft blue glows, tinted input. */ - --gate-card: rgb(255 255 255 / 0.62); - --gate-callout-bg: rgb(255 255 255 / 0.55); - --gate-callout-border: rgb(56 189 248 / 0.32); - --field: rgb(255 255 255 / 0.66); - --field-border: rgb(255 255 255 / 0.9); + --gate-base: #eef3fa; + --gate-glow-a: rgb(90 150 240 / 0.16); + --gate-glow-b: rgb(140 150 240 / 0.12); + --gate-card: #ffffff; + --gate-card-border: #dde5f2; + --gate-callout-bg: #eef3fa; + --gate-callout-border: #c9d5e8; + --field: #eef2f9; + --field-border: #dbe3f0; --shadow-gate: 0 28px 70px -36px rgb(14 21 40 / 0.3), 0 1px 2px rgb(14 21 40 / 0.05); /* Install sheet: light variant (dark is the default, see .dark). */ - --install-sheet: rgb(255 255 255 / 0.6); - --install-card: rgb(255 255 255 / 0.7); - --install-chip: rgb(255 255 255 / 0.55); - --install-border: rgb(255 255 255 / 0.8); + --install-sheet: #ffffff; + --install-card: #f7f7f8; + --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 1px 2px rgb(14 21 40 / 0.05), 0 5px 14px -6px rgb(14 21 40 / 0.12); - --shadow-card-hover: 0 24px 60px -24px rgb(30 64 175 / 0.45), 0 2px 6px rgb(30 64 175 / 0.12); + --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); } .dark { color-scheme: dark; - --page: #0a1120; - --sidebar: #0b1524; - --surface: #16233a; - --surface-raised: #1d2c47; - --surface-strong: rgb(255 255 255 / 0.08); - --card-hover-bg: rgb(255 255 255 / 0.1); + --page: #121a2c; + --sidebar: #0c1322; + --surface: #1a2337; + --surface-raised: #222c44; + --surface-strong: #222c44; + --card-hover-bg: #222c44; --surface-sunken: rgb(255 255 255 / 0.07); - --hairline: rgb(255 255 255 / 0.12); - --card-border: var(--glass-border); - --sidebar-divider: rgb(255 255 255 / 0.12); - --hairline-soft: rgb(255 255 255 / 0.08); + --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); --glass-tint: rgb(255 255 255 / 0.06); - --text: #e8f0ff; /* 16.3:1 on #0a1120 */ - --text-muted: #b3c4e0; /* 10.1:1 */ - --text-subtle: #90a4c6; /* 6.9:1 */ + --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 */ --hover: rgb(255 255 255 / 0.07); - --focus: #38bdf8; - - /* Frosted ice glass — dark: barely-there tint, brighter rim. */ - --glass-bg: rgb(255 255 255 / 0.06); - --glass-border: rgb(255 255 255 / 0.12); - --glass-highlight: rgb(255 255 255 / 0.25); - --glass-shadow: 0 20px 50px -20px rgb(30 64 175 / 0.35); - --glass-solid-bg: rgb(255 255 255 / 0.1); - --glass-fallback: rgb(16 26 45 / 0.97); - --nav-active-bg: rgb(56 189 248 / 0.14); - --nav-active-border: rgb(56 189 248 / 0.4); + --sidebar-edge: 0 1px 0 var(--hairline), 6px 0 16px -14px rgb(0 0 0 / 0.7); /* API-key gate: navy stage with the icon's blue glows, raised input. */ - --gate-card: rgb(255 255 255 / 0.06); + --gate-base: #0c1322; + --gate-glow-a: rgb(70 140 235 / 0.14); + --gate-glow-b: rgb(120 120 235 / 0.1); + --gate-card: #131a2c; + --gate-card-border: rgb(255 255 255 / 0.1); --gate-callout-bg: rgb(255 255 255 / 0.06); - --gate-callout-border: rgb(56 189 248 / 0.3); - --field: rgb(255 255 255 / 0.07); - --field-border: rgb(255 255 255 / 0.14); + --gate-callout-border: rgb(255 255 255 / 0.2); + --field: #1b2438; + --field-border: rgb(255 255 255 / 0.1); --shadow-gate: 0 30px 80px -40px rgb(0 0 0 / 0.85), 0 1px 2px rgb(0 0 0 / 0.4); /* Install sheet: layered dark surfaces (default theme for the sheet). */ - --install-sheet: rgb(255 255 255 / 0.06); - --install-card: rgb(255 255 255 / 0.07); - --install-chip: rgb(255 255 255 / 0.1); - --install-border: rgb(255 255 255 / 0.14); + --install-sheet: #1c1c1e; + --install-card: #2c2c2e; + --install-chip: #3a3a3c; + --install-border: rgb(255 255 255 / 0.08); --shadow-card: 0 1px 2px rgb(0 0 0 / 0.25); --card-shadow: var(--shadow-card); - --shadow-card-hover: 0 24px 60px -24px rgb(30 64 175 / 0.55), 0 2px 8px rgb(0 0 0 / 0.3); -} - -/* Ice scene tokens: the fixed layered background behind everything. */ -:root { - --ice-base-top: #f4f9ff; - --ice-base-bottom: #e6f1fb; - --ice-blob-opacity: 0.18; - --ice-orb-opacity: 0.55; - --ice-grain-opacity: 0.05; -} -.dark { - --ice-base-top: #070b16; - --ice-base-bottom: #0c1426; - --ice-blob-opacity: 0.4; - --ice-orb-opacity: 0.8; - --ice-grain-opacity: 0.045; + --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); } html, @@ -215,196 +198,102 @@ body { background: transparent; } -/* The key gate rides on the shared ice scene (see .ice-scene). */ +/* API-key gate stage: two big blurred blobs (top-right + bottom-left) over a + near-black (dark) or near-white (light) base. Real blurred elements read as + atmosphere; a flat radial gradient was too faint. Both blobs are decorative, + non-interactive and sit behind the card (z-index 0 vs the card's z-10). */ +.gate-bg { + background: var(--gate-base); +} +.gate-bg::before, +.gate-bg::after { + content: ""; + position: absolute; + z-index: 0; + width: 48rem; + height: 48rem; + border-radius: 9999px; + filter: blur(120px); + pointer-events: none; +} +.gate-bg::before { + top: -20rem; + right: -16rem; + background: var(--gate-glow-a); +} +.gate-bg::after { + bottom: -20rem; + left: -16rem; + background: var(--gate-glow-b); +} -/* The fixed ice scene: navy/pale base, drifting glacier blobs, glossy ice - orbs and a whisper of grain. Purely decorative. */ -.ice-scene { +.app-backdrop { position: fixed; inset: 0; - z-index: -1; + z-index: -10; overflow: hidden; pointer-events: none; - background: linear-gradient(180deg, var(--ice-base-top) 0%, var(--ice-base-bottom) 100%); -} - -.ice-blob { - position: absolute; - border-radius: 50%; - opacity: var(--ice-blob-opacity); - will-change: transform; - /* No filter: blur() here on purpose. A radial gradient fading to - transparent is already a soft blob, and blurring a huge layer that is - also being animated forces a re-rasterisation every frame (measured - 150ms vs 33ms per frame in a software renderer). The falloff stops below - keep the same softness. */ -} -.ice-blob-a { - width: 52vw; height: 52vw; min-width: 24rem; min-height: 24rem; - top: -14vh; left: -10vw; - background: radial-gradient(circle, rgb(56 189 248 / 0.9) 0%, rgb(56 189 248 / 0.45) 38%, transparent 72%); - animation: ice-drift-a 44s ease-in-out infinite; -} -.ice-blob-b { - width: 46vw; height: 46vw; min-width: 22rem; min-height: 22rem; - top: 4vh; right: -12vw; - background: radial-gradient(circle, rgb(96 165 250 / 0.9) 0%, rgb(96 165 250 / 0.45) 38%, transparent 72%); - animation: ice-drift-b 52s ease-in-out infinite; -} -.ice-blob-c { - width: 48vw; height: 48vw; min-width: 22rem; min-height: 22rem; - bottom: -16vh; left: 14vw; - background: radial-gradient(circle, rgb(129 140 248 / 0.9) 0%, rgb(129 140 248 / 0.42) 38%, transparent 74%); - animation: ice-drift-c 60s ease-in-out infinite; -} -.ice-blob-d { - width: 34vw; height: 34vw; min-width: 16rem; min-height: 16rem; - bottom: 2vh; right: 6vw; - background: radial-gradient(circle, rgb(56 189 248 / 0.85) 0%, rgb(56 189 248 / 0.4) 38%, transparent 74%); - animation: ice-drift-a 48s ease-in-out infinite reverse; -} - -@keyframes ice-drift-a { - 0%, 100% { transform: translate3d(0, 0, 0) scale(1); } - 50% { transform: translate3d(4vw, 3vh, 0) scale(1.08); } -} -@keyframes ice-drift-b { - 0%, 100% { transform: translate3d(0, 0, 0) scale(1.05); } - 50% { transform: translate3d(-5vw, 4vh, 0) scale(1); } -} -@keyframes ice-drift-c { - 0%, 100% { transform: translate3d(0, 0, 0) scale(1); } - 50% { transform: translate3d(3vw, -4vh, 0) scale(1.1); } -} - -/* Glossy ice orbs, placed so the glass panels blur them. */ -.ice-orb { - position: absolute; - border-radius: 50%; - opacity: var(--ice-orb-opacity); - background: radial-gradient( - circle at 30% 25%, - rgb(255 255 255 / 0.95) 0%, - rgb(191 227 255 / 0.55) 18%, - rgb(96 165 250 / 0.35) 48%, - rgb(37 99 235 / 0.45) 100% - ); - box-shadow: 0 18px 40px -18px rgb(30 64 175 / 0.55), inset 0 -8px 18px -10px rgb(30 64 175 / 0.6); -} -.ice-orb-1 { width: 190px; height: 190px; top: 14%; left: 12%; } -.ice-orb-2 { width: 84px; height: 84px; top: 58%; left: 4%; } -.ice-orb-3 { width: 220px; height: 220px; bottom: -70px; right: 10%; } -.ice-orb-4 { width: 52px; height: 52px; top: 24%; right: 28%; } -.ice-orb-5 { width: 120px; height: 120px; bottom: 16%; left: 38%; } - -/* Frosted grain over the whole scene. */ -.ice-grain { - position: absolute; - inset: 0; - opacity: var(--ice-grain-opacity); - mix-blend-mode: overlay; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E"); -} - -@media (prefers-reduced-motion: reduce) { - .ice-blob { animation: none; } -} - -/* Phones: fewer, softer blobs so the drift animation stays cheap. Unlayered - on purpose — layered rules would lose to the .ice-blob declarations above. */ -@media (max-width: 1023px) { - .ice-blob-c, - .ice-blob-d, - .ice-orb-4, - .ice-orb-5 { - display: none; - } - .ice-orb-1 { width: 120px; height: 120px; } - .ice-orb-3 { width: 150px; height: 150px; } + /* 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); } @layer components { -/* Frosted ice glass ------------------------------------------------------ - One shared recipe for the large surfaces only: translucent tint, 24px - blur, a bright rim, an inner top highlight and a blue-tinted shadow. - Nested glass (chips, inputs) never adds a second blur layer. Tailwind's - backdrop utilities emit the -webkit- prefix too (hand-written - backdrop-filter gets minified to -webkit- only, which paints nothing). */ -.glass-panel, -.card, -.sidebar-surface, -.study-chrome, -.tabbar, -.glass, -.gate-card { - @apply backdrop-blur-xl backdrop-saturate-[1.4]; - background: var(--glass-bg); - box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-highlight); -} - +/* Glass primitives ------------------------------------------------------ */ .glass-panel { - border: 1px solid var(--glass-border); - border-radius: 1.5rem; + @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); } -/* Nested glass: slightly more opaque than its parent, no extra blur. */ .glass-chip { + @apply backdrop-blur-md; background: var(--surface-sunken); - border: 1px solid var(--glass-border); -} - -/* More opaque glass for the study question card, where text must stay crisp. */ -.glass-solid { - background: var(--glass-solid-bg); + border: 1px solid transparent; } .glass-chip:hover { background: var(--hover); } .glass { - border-bottom: 1px solid var(--glass-border); + /* 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); + border-bottom: 1px solid var(--hairline); } /* Study-feed chrome (progress header + bottom pager): the same neutral surface as the cards, with a neutral border and shadow. */ .study-chrome { - border: 1px solid var(--glass-border); + @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); + border: 1px solid var(--hairline); + box-shadow: var(--shadow-card); +} +.dark .study-chrome { + border-color: var(--hairline-soft); } .card { - border-radius: 1.5rem; /* 24px */ - border: 1px solid var(--glass-border); + 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); + border: 1px solid var(--card-border); + box-shadow: var(--card-shadow), inset 0 1px 0 var(--glass-top); } - -.gate-card { - border: 1px solid var(--glass-border); - border-radius: 1.75rem; /* 28px */ -} - -/* Blur is expensive on phones: halve it, and thin out the ice scene. */ -@media (max-width: 1023px) { - .glass-panel, - .card, - .sidebar-surface, - .study-chrome, - .tabbar, - .glass, - .gate-card { - @apply backdrop-blur-lg; - } -} - -/* No backdrop-filter support: fall back to a more opaque surface. */ -@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { - .glass-panel, - .card, - .sidebar-surface, - .study-chrome, - .tabbar, - .glass, - .gate-card { - background: var(--glass-fallback); - } +.dark .card, +.dark .glass-panel { + border-color: var(--glass-edge); } /* Opt-in hover lift for interactive cards (deck cards). Pointer devices only, @@ -415,8 +304,10 @@ body { @media (hover: hover) and (pointer: fine) { .card-hover:hover { background: var(--card-hover-bg); - border-color: var(--nav-active-border); + border-color: color-mix(in oklab, var(--hairline) 45%, var(--text) 20%); box-shadow: var(--shadow-card-hover); + } + :root:not(.dark) .card-hover:hover { transform: translateY(-2px); } } @@ -428,7 +319,7 @@ body { .list-group { border-radius: 1rem; @apply backdrop-blur-lg backdrop-saturate-150; - background: color-mix(in oklab, var(--surface) 82%, transparent); + background: color-mix(in oklab, var(--surface) 66%, transparent); border: 1px solid var(--hairline); overflow: hidden; } @@ -460,19 +351,14 @@ body { } .segmented > button[aria-pressed="true"], .segmented > button[aria-checked="true"] { - background-image: linear-gradient(135deg, #38bdf8 0%, #60a5fa 100%); - color: #06111f; - box-shadow: 0 6px 16px -8px rgb(56 189 248 / 0.7); + background: var(--surface-strong); + color: var(--text); + box-shadow: 0 1px 2px rgb(13 13 13 / 0.12); } .dark .segmented > button[aria-pressed="true"], .dark .segmented > button[aria-checked="true"] { - background-image: linear-gradient(135deg, #38bdf8 0%, #60a5fa 100%); - color: #06111f; -} - -/* Ice gradient for progress bars and selected states. */ -.grad-ice { - background-image: linear-gradient(90deg, #38bdf8 0%, #60a5fa 100%); + background: var(--surface-raised); + color: #fff; } /* Sheet grabber --------------------------------------------------------- */ @@ -485,7 +371,9 @@ body { } .tabbar { - border-top: 1px solid var(--glass-border); + @apply backdrop-blur-xl; + background: color-mix(in oklab, var(--page) 70%, transparent); + border-top: 1px solid var(--hairline); } /* Soft tinted chip for activity types (a hint of colour, not a rainbow) -- */ @@ -500,11 +388,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: frosted navy glass with a real edge. */ -/* Sidebar surface: one tall inset glass panel. */ +/* Sidebar surface: flat frosted glass with a real edge. */ .sidebar-surface { - border: 1px solid var(--glass-border); - border-radius: 1.5rem; + @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); + border-right: 1px solid var(--sidebar-divider); + box-shadow: var(--sidebar-edge); } /* Sidebar nav rows */ @@ -526,9 +417,8 @@ body { } .nav-row[aria-current="page"], .nav-row[aria-pressed="true"] { - background: var(--nav-active-bg); + background: var(--surface-sunken); color: var(--text); - box-shadow: inset 0 0 0 1px var(--nav-active-border), inset 0 1px 0 var(--glass-highlight); } /* Shared CTA language: the icon's blue, with a deeper fill in light mode. */ @@ -537,44 +427,35 @@ body { align-items: center; justify-content: center; gap: 0.5rem; - border-radius: 14px; - background-image: linear-gradient(135deg, #1d4ed8 0%, #1a5fd0 55%, #0ea5e9 100%); + border-radius: 999px; + background: #1d61ab; color: #ffffff; font-weight: 500; - box-shadow: 0 10px 24px -12px rgb(14 165 233 / 0.7); - transition: box-shadow 180ms ease, filter 180ms ease, transform 160ms ease; -} -.btn-primary:hover { - filter: brightness(1.07); - box-shadow: 0 14px 32px -12px rgb(56 189 248 / 0.85); + box-shadow: 0 1px 2px rgb(14 21 40 / 0.22); + transition: background-color 160ms ease, transform 160ms ease, box-shadow 160ms ease; } +.btn-primary:hover { background: #2478d2; } .btn-primary:active { transform: scale(0.985); } .dark .btn-primary { - background-image: linear-gradient(135deg, #ffffff 0%, #bfe3ff 100%); - color: #0b1220; - box-shadow: 0 10px 26px -14px rgb(56 189 248 / 0.6); -} -.dark .btn-primary:hover { - filter: brightness(1.03); - box-shadow: 0 14px 34px -14px rgb(56 189 248 / 0.95); + background: #59a8f7; + color: #081020; + box-shadow: none; } +.dark .btn-primary:hover { background: #7bbaff; } .btn-secondary { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; - border-radius: 14px; - background: var(--surface-strong); + border-radius: 999px; + background: transparent; color: var(--text); - border: 1px solid var(--glass-border); + border: 1px solid var(--hairline); font-weight: 500; transition: background-color 160ms ease, border-color 160ms ease, transform 160ms ease; } -.btn-secondary:hover { - background: var(--hover); - border-color: var(--nav-active-border); -} +.btn-secondary:hover { background: var(--hover); border-color: color-mix(in oklab, var(--hairline) 70%, var(--text) 12%); } .btn-secondary:active { transform: scale(0.985); } .btn-ghost { @@ -582,7 +463,7 @@ body { align-items: center; justify-content: center; gap: 0.5rem; - border-radius: 14px; + border-radius: 0.75rem; background: transparent; color: var(--text-muted); font-weight: 500; diff --git a/app/layout.tsx b/app/layout.tsx index 978af14..058afb7 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,5 @@ import type { Metadata, Viewport } from "next"; -import { IceBackdrop } from "@/components/IceBackdrop"; import { InstallSheet } from "@/components/InstallSheet"; import { ServiceWorkerRegistrar } from "@/components/ServiceWorkerRegistrar"; import "./globals.css"; @@ -75,7 +74,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })