freezmo/app/globals.css
Martin Dvorak 6564e88138 Mobile declutter pass: step flows, compact cards, simplified settings
Phones only (<=639px); desktop markup is untouched.

- New deck is a 3-step flow: progress (1/3 + bar) and a fixed Back/Next
  bar above the tab bar. Step 1 = Add photos or files / Camera / Paste
  text instead (no drop zone or paste copy on touch), step 2 = folder,
  difficulty and count with defaults plus one collapsed focus/notes row,
  step 3 = one summary line (photos/docs/text · difficulty · count ·
  folder) with Freeze. Going Back keeps every input; step transitions
  respect prefers-reduced-motion.
- Make my homework is a 2-step flow sharing the same pattern; tapping a
  mode card starts it (no separate CTA on phones), with a short preview
  of the assignment on step 2. The hero is now 'Make my homework' /
  'Udělej mi úkol' everywhere ('Solve your homework' is gone) and the
  intro is one line.
- Library: stats line removed, folder row and 'Unorganized' heading
  hidden until folders exist (New folder lives in the drawer), Import is
  a small icon button beside the tabs. Deck and homework cards are
  compact on phones: title, one meta line (activities · difficulty ·
  best), a one-line description that expands on tap, the whole card
  tappable and a small Study/Open pill instead of the full-width button.
- Settings sheet on phones: no .env.local subtitle; Language stays first;
  one Connection card (status dot + 'Connected · 2 models ready', key
  field with the eye inside, Save only when edited, auto re-check on
  save, refresh icon instead of Test connection); privacy one-liner with
  a More expander (daemon tip inside); raw health rows behind one
  collapsed Details list; Remove key as a small danger button at the very
  bottom (same confirmation).
- Decorative orbs on phones moved further off the screen edges so they
  never glow under cards or primary buttons.
- Shared markup for folder/difficulty/count and mode cards keeps one
  source of state; new lib/use-phone.ts gates the layouts.

Verified: new declutter suite 116/116 (360 + 390, EN + CS, photo + PDF +
pasted text, Back keeps input, summaries, settings flow), regression
74/74, mobile 52/52, i18n 25/25. Lint + build clean. SW freezmo-v20.
2026-10-10 10:43:32 +00:00

759 lines
26 KiB
CSS

@import "tailwindcss";
/* Class-based dark mode (toggled on <html> by the theme script). */
@custom-variant dark (&:where(.dark, .dark *));
@theme {
/* Navy-tinted neutral scale — sampled from the app icon. ---------------- */
--color-frost-50: #f5f8fd;
--color-frost-100: #eaf0fa;
--color-frost-200: #d9e3f3;
--color-frost-300: #c2cfe4;
--color-frost-400: #92a3c0;
--color-frost-500: #6a7c9b;
--color-frost-600: #4e5f7d;
--color-frost-700: #3c4b66;
--color-frost-800: #2b3752;
--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;
/* Aurora hues used only for the frosted backdrop wash. ----------------- */
--color-aurora-mint: #9fe3d0;
--color-aurora-sky: #9ccdf5;
--color-aurora-lilac: #c3b6f0;
--color-aurora-peach: #f6cdb4;
--font-sans: -apple-system, BlinkMacSystemFont, ui-sans-serif, system-ui,
"Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji";
--shadow-frost: 0 14px 34px -22px rgb(13 13 13 / 0.28);
--shadow-lift: 0 4px 14px -8px rgb(13 13 13 / 0.18);
--shadow-hairline: 0 1px 0 0 rgb(13 13 13 / 0.04);
--animate-float: float 11s ease-in-out infinite;
--animate-pop: pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
--animate-wiggle: wiggle 0.4s ease-in-out;
--animate-rise: rise 0.4s cubic-bezier(0.22, 1, 0.36, 1) both;
--animate-sheet: sheet 0.32s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes float {
0%, 100% { transform: translate3d(0, 0, 0) scale(1) }
50% { transform: translate3d(0, -18px, 0) scale(1.04) }
}
@keyframes pop {
0% { transform: scale(0.92) }
60% { transform: scale(1.02) }
100% { transform: scale(1) }
}
@keyframes wiggle {
0%, 100% { transform: translateX(0) }
25% { transform: translateX(-5px) }
75% { transform: translateX(5px) }
}
@keyframes rise {
from { opacity: 0; transform: translateY(10px) }
to { opacity: 1; transform: translateY(0) }
}
@keyframes sheet {
from { opacity: 0; transform: translateY(24px) }
to { opacity: 1; transform: translateY(0) }
}
/* ---------------------------------------------------------------------------
Theme tokens.
Light: ChatGPT-ish neutral (page #ffffff, sidebar #f9f9f9, border #e5e5e5).
Dark: page #212121, sidebar #171717, surfaces #2f2f2f, borders rgba(255,255,255,.1).
Text tiers are picked to clear WCAG AA (4.5:1) on their own background.
--------------------------------------------------------------------------- */
:root {
color-scheme: light;
--page: #eef5fd;
--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.6: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);
/* A cool rim instead of near-white: white-on-white made light-mode card
edges invisible against the pale page. */
--glass-border: rgb(143 174 212 / 0.55);
--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);
/* 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);
--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);
--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.06), 0 9px 22px -8px rgb(14 21 40 / 0.16);
--shadow-card-hover: 0 24px 60px -24px rgb(30 64 175 / 0.45), 0 2px 6px rgb(30 64 175 / 0.12);
}
.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);
--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);
--glass-tint: rgb(255 255 255 / 0.06);
--text: #e8f0ff; /* 16.3:1 on #0a1120 */
--text-muted: #b3c4e0; /* 10.1:1 */
--text-subtle: #9db1d0; /* 8.6:1 on page, 7.2:1 on dark glass */
--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);
/* API-key gate: navy stage with the icon's blue glows, raised input. */
--gate-card: rgb(255 255 255 / 0.06);
--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);
--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);
--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;
}
html,
body {
height: 100%;
overscroll-behavior: none;
}
body {
font-family: var(--font-sans);
-webkit-tap-highlight-color: transparent;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
/* Matches the backdrop so iOS rubber-banding never shows a seam. */
background-color: var(--page);
color: var(--text);
overscroll-behavior-y: none;
}
.dark body {
color: var(--color-frost-100);
}
/* The page backdrop now lives in ONE viewport-fixed layer (see .app-backdrop)
so it can never stretch, scroll or be clipped by a content column.
`.frost-bg` stays as a transparent wrapper for pages that used it. */
.frost-bg {
background: transparent;
}
/* The key gate rides on the shared ice scene (see .ice-scene). */
/* The fixed ice scene: navy/pale base, drifting glacier blobs, glossy ice
orbs and a whisper of grain. Purely decorative. */
.ice-scene {
position: fixed;
inset: 0;
z-index: -1;
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; }
}
/* Phones: the decoration is already *behind* the content (the scene is a
fixed, pointer-events:none layer at z-index -1), but it shines through the
translucent page and glass, so it reads as overlap. Thin it right down and
tuck the orbs to the edges, away from the text column and the tab bar. */
@media (max-width: 639px) {
:root {
--ice-blob-opacity: 0.1;
--ice-orb-opacity: 0.32;
}
.dark {
--ice-blob-opacity: 0.2;
--ice-orb-opacity: 0.42;
}
/* Phones: orbs hug the screen edges so they can never sit under the
content column's cards or primary buttons (the lower-right one used to
glow right behind the main CTAs). */
.ice-orb-1 { width: 96px; height: 96px; top: -44px; left: -48px; }
.ice-orb-2 { width: 72px; height: 72px; top: 60%; left: -44px; }
.ice-orb-3 { width: 124px; height: 124px; bottom: 12vh; right: -84px; }
}
@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-panel {
border: 1px solid var(--glass-border);
border-radius: 1.5rem;
}
/* Nested glass: slightly more opaque than its parent, no extra blur. */
.glass-chip {
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);
}
.glass-chip:hover {
background: var(--hover);
}
.glass {
border-bottom: 1px solid var(--glass-border);
}
/* 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);
}
.card {
border-radius: 1.5rem; /* 24px */
border: 1px solid var(--glass-border);
}
.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);
}
}
/* Opt-in hover lift for interactive cards (deck cards). Pointer devices only,
so touch screens never get sticky hover states. */
.card-hover {
transition: background-color 180ms ease, box-shadow 180ms ease, border-color 180ms ease, transform 180ms ease;
}
@media (hover: hover) and (pointer: fine) {
.card-hover:hover {
background: var(--card-hover-bg);
border-color: var(--nav-active-border);
box-shadow: var(--shadow-card-hover);
transform: translateY(-2px);
}
}
.card-hover:active {
transform: translateY(0);
}
/* Inset grouped list ---------------------------------------------------- */
.list-group {
border-radius: 1rem;
@apply backdrop-blur-lg backdrop-saturate-150;
background: color-mix(in oklab, var(--surface) 82%, transparent);
border: 1px solid var(--hairline);
overflow: hidden;
}
.list-group > * + * {
border-top: 1px solid var(--hairline-soft);
}
/* iOS segmented control ------------------------------------------------- */
.segmented {
display: flex;
gap: 2px;
padding: 3px;
border-radius: 0.95rem;
@apply backdrop-blur-sm;
background: var(--surface-sunken);
border: 1px solid var(--hairline);
}
.segmented > button {
flex: 1;
border-radius: 0.75rem;
padding: 0.42rem 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: var(--color-frost-700);
transition: background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}
.dark .segmented > button {
color: var(--color-frost-300);
}
.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);
}
.dark .segmented > button[aria-pressed="true"],
.dark .segmented > button[aria-checked="true"] {
background-image: linear-gradient(135deg, #38bdf8 0%, #60a5fa 100%);
color: #06111f;
}
/* Link segments (library tabs): same control, but links navigate. */
.segmented > a {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
min-height: 2.75rem;
border-radius: 0.75rem;
padding: 0.42rem 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: var(--color-frost-700);
transition: background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}
.dark .segmented > a {
color: var(--color-frost-300);
}
.segmented > a[aria-current="page"] {
background-image: linear-gradient(135deg, #38bdf8 0%, #60a5fa 100%);
color: #06111f;
box-shadow: 0 6px 16px -8px rgb(56 189 248 / 0.7);
}
/* Ice gradient for progress bars and selected states. */
.grad-ice {
background-image: linear-gradient(90deg, #38bdf8 0%, #60a5fa 100%);
}
/* Sheet grabber --------------------------------------------------------- */
.sheet-grabber {
width: 36px;
height: 5px;
border-radius: 999px;
background: var(--hairline-soft);
margin: 0 auto 0.75rem;
}
.tabbar {
border-top: 1px solid var(--glass-border);
}
/* Phones: the tab bar sits over the bottom of the ice scene, so give the
labels a calmer surface than the barely-there shared glass tint. */
@media (max-width: 639px) {
.tabbar {
background: color-mix(in oklab, var(--page) 84%, transparent);
}
}
/* Soft tinted chip for activity types (a hint of colour, not a rainbow) -- */
.tint-sky { background: linear-gradient(140deg, rgb(156 205 245 / 0.34), rgb(156 205 245 / 0.14)); color: #1c4e72 }
.tint-mint { background: linear-gradient(140deg, rgb(159 227 208 / 0.4), rgb(159 227 208 / 0.16)); color: #1d5c4c }
.tint-lilac { background: linear-gradient(140deg, rgb(195 182 240 / 0.4), rgb(195 182 240 / 0.16)); color: #47397f }
.tint-peach { background: linear-gradient(140deg, rgb(246 205 180 / 0.45), rgb(246 205 180 / 0.18)); color: #7d4522 }
.tint-rose { background: linear-gradient(140deg, rgb(246 178 190 / 0.4), rgb(246 178 190 / 0.16)); color: #7c2b3d }
.dark .tint-sky { background: linear-gradient(140deg, rgb(74 164 220 / 0.26), rgb(74 164 220 / 0.1)); color: #bfe2fb }
.dark .tint-mint { background: linear-gradient(140deg, rgb(88 201 173 / 0.26), rgb(88 201 173 / 0.1)); color: #bdf0e2 }
.dark .tint-lilac { background: linear-gradient(140deg, rgb(160 140 235 / 0.26), rgb(160 140 235 / 0.1)); color: #ddd5fb }
.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 {
border: 1px solid var(--glass-border);
border-radius: 1.5rem;
}
/* Sidebar nav rows */
.nav-row {
display: flex;
align-items: center;
gap: 0.625rem;
width: 100%;
border-radius: 0.625rem;
padding: 0.5rem 0.625rem;
font-size: 13.5px;
font-weight: 500;
color: var(--text-muted);
transition: background-color 140ms ease, color 140ms ease;
}
.nav-row:hover {
background: var(--hover);
color: var(--text);
}
.nav-row[aria-current="page"],
.nav-row[aria-pressed="true"] {
background: var(--nav-active-bg);
color: var(--text);
box-shadow: inset 0 0 0 1px var(--nav-active-border), inset 0 1px 0 var(--glass-highlight);
}
/* Shared CTA language: one ice gradient in *both* themes (it mirrors the
segmented control, the progress bars and the tab-bar plus). #06111f on
#38bdf8 is ~8.8:1, so the dark label clears AA everywhere on the ramp. */
.btn-primary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
border-radius: 14px;
background-image: linear-gradient(135deg, #38bdf8 0%, #60a5fa 100%);
color: #06111f;
font-weight: 600;
box-shadow: 0 10px 24px -14px rgb(14 165 233 / 0.75);
transition: box-shadow 180ms ease, filter 180ms ease, transform 160ms ease;
}
.btn-primary:hover {
filter: brightness(1.06);
box-shadow: 0 14px 32px -14px rgb(56 189 248 / 0.9);
}
.btn-primary:active { transform: scale(0.985); }
/* Disabled: a real, visible chip instead of a 40%-opacity ghost. Important
declarations deliberately live in this (earlier) layer so they beat the
important rose/emerald overrides of the danger/success variants. */
.btn-disabled {
background-image: none !important;
background-color: var(--surface-sunken) !important;
color: var(--text-subtle) !important;
box-shadow: none !important;
filter: none !important;
transform: none !important;
cursor: not-allowed;
}
.btn-secondary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
border-radius: 14px;
background: var(--surface-strong);
color: var(--text);
border: 1px solid var(--glass-border);
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:active { transform: scale(0.985); }
.btn-ghost {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
border-radius: 14px;
background: transparent;
color: var(--text-muted);
font-weight: 500;
transition: background-color 160ms ease, color 160ms ease;
}
.btn-ghost:hover { background: var(--hover); color: var(--text); }
/* Difficulty: neutral pill, colour lives in the text only. */
.diff-easy { color: #16714f; }
.diff-medium { color: #7a5300; }
.diff-hard { color: #b4342f; }
.dark .diff-easy { color: #6fd3a8; }
.dark .diff-medium { color: #e3b341; }
.dark .diff-hard { color: #f08a84; }
/* Loading skeleton */
.skeleton {
position: relative;
overflow: hidden;
background: var(--surface-sunken);
border-radius: 0.75rem;
}
.skeleton::after {
content: "";
position: absolute;
inset: 0;
transform: translateX(-100%);
background: linear-gradient(90deg, transparent, rgb(255 255 255 / 0.45), transparent);
animation: shimmer 1.6s infinite;
}
.dark .skeleton::after {
background: linear-gradient(90deg, transparent, rgb(255 255 255 / 0.06), transparent);
}
@keyframes shimmer {
100% { transform: translateX(100%); }
}
.text-balance {
text-wrap: balance;
}
/* Focus rings ------------------------------------------------------------ */
:where(button, a, input, textarea, [tabindex]):focus-visible {
outline: 2.5px solid var(--color-accent-500);
outline-offset: 2px;
border-radius: 0.75rem;
}
/* Scrollbars ------------------------------------------------------------- */
.thin-scroll {
scrollbar-width: thin;
scrollbar-color: color-mix(in oklab, var(--color-frost-400) 55%, transparent) transparent;
}
.thin-scroll::-webkit-scrollbar { width: 8px; height: 8px }
.thin-scroll::-webkit-scrollbar-thumb {
background: color-mix(in oklab, var(--color-frost-400) 45%, transparent);
border-radius: 999px;
}
.no-scrollbar::-webkit-scrollbar { display: none }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none }
/* Mobile safe areas ------------------------------------------------------ */
.pt-safe { padding-top: max(env(safe-area-inset-top), 0.75rem) }
.pb-safe { padding-bottom: max(env(safe-area-inset-bottom), 0.75rem) }
.pb-tabbar { padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 5.25rem) }
@media (display-mode: standalone) {
.standalone-hide { display: none }
}
/* Phones: Apple's 44px minimum for touch targets. The class is a no-op on
desktop (and on tablets), so components can carry it unconditionally. */
@media (max-width: 639px) {
.touch-target { min-height: 44px; min-width: 44px; }
.nav-row { min-height: 44px; }
.segmented > button { min-height: 44px; }
.folder-pill { min-height: 44px; }
}
/* Settings sheet: contained momentum scrolling; the safe-area padding comes
from the pb-safe utility plus a trailing spacer. The mask fades the last
36px of *content* (not a coloured overlay, so it blends with the glass in
both themes) — that is the scroll cue; the trailing spacer keeps real
content out of the faded strip once you reach the bottom. */
.sheet-scroll {
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
-webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 36px), transparent);
mask-image: linear-gradient(to bottom, #000 calc(100% - 36px), transparent);
}
}
/* Mobile form fields ----------------------------------------------------- */
/* iOS Safari zooms the whole page when a focused field's text is under 16px
(it ignores user-scalable=no for that). Deliberately unlayered so it wins
over the per-component Tailwind sizes, and only on phone-sized screens. */
@media (max-width: 639px) {
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
textarea,
select {
font-size: 16px;
}
}
/* Reduced motion --------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
}