freezmo/app/globals.css
Martin Dvorak 586e4b5a40 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.
2026-10-08 09:27:42 +00:00

598 lines
19 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: 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;
}
}