@import "tailwindcss"; /* Class-based dark mode (toggled on 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: 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; --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: #f4f7fd; --sidebar: #e9eff9; --surface: #ffffff; --surface-raised: #ffffff; --surface-strong: #edf2fa; --card-hover-bg: #f7faff; --surface-sunken: rgb(14 21 40 / 0.05); --hairline: #dce4f1; --card-border: #d7e0ef; --sidebar-divider: #d7e0ef; --hairline-soft: rgb(14 21 40 / 0.07); --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-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: #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; --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: #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 12px 30px -12px rgb(14 21 40 / 0.2), 0 2px 6px rgb(14 21 40 / 0.07); } .dark { color-scheme: dark; --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.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); --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-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: #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); --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: #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 10px 28px -16px rgb(0 0 0 / 0.55), 0 1px 2px rgb(0 0 0 / 0.3); } /* 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, 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; } /* 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); } .app-backdrop { position: fixed; inset: 0; z-index: -10; overflow: hidden; pointer-events: none; background: 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: color-mix(in oklab, var(--surface) 82%, transparent); border: 1px solid var(--hairline); box-shadow: var(--shadow-card); } .glass-chip { @apply backdrop-blur-md; background: var(--surface-sunken); border: 1px solid transparent; } .glass-chip:hover { background: var(--hover); } .glass { /* 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) 70%, 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 { @apply backdrop-blur-xl backdrop-saturate-150; background: color-mix(in oklab, var(--surface) 88%, transparent); border: 1px solid var(--hairline); box-shadow: var(--shadow-card); } .dark .study-chrome { border-color: var(--hairline-soft); } .card { border-radius: 1.125rem; /* 18px */ @apply backdrop-blur-xl backdrop-saturate-150; background: color-mix(in oklab, var(--surface) 78%, transparent); border: 1px solid var(--card-border); box-shadow: var(--card-shadow); } /* 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: 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); } } .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: 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: var(--surface-raised); color: #fff; } /* Sheet grabber --------------------------------------------------------- */ .sheet-grabber { width: 36px; height: 5px; border-radius: 999px; background: var(--hairline-soft); margin: 0 auto 0.75rem; } .tabbar { @apply backdrop-blur-xl; background: color-mix(in oklab, var(--page) 88%, transparent); border-top: 1px solid var(--hairline); } /* 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 { @apply backdrop-blur-xl backdrop-saturate-150; 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); } /* 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(--surface-sunken); color: var(--text); } /* Shared CTA language: the icon's blue, with a deeper fill in light mode. */ .btn-primary { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; border-radius: 999px; background: #1d61ab; color: #ffffff; font-weight: 500; 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: #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: 999px; background: transparent; color: var(--text); 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: color-mix(in oklab, var(--hairline) 70%, var(--text) 12%); } .btn-secondary:active { transform: scale(0.985); } .btn-ghost { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; border-radius: 0.75rem; 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) } .pb-cta { padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 9rem) } @media (display-mode: standalone) { .standalone-hide { display: none } } } /* 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; } }