freezmo/app/globals.css
Martin Dvorak 52cce42cde Frosted ice glass restyle
Rebuilds the look around the spec: an ice scene, one shared glass
recipe, icy accents and glass on every large surface. Visual only —
layout, behaviour, copy and functionality are untouched.

Background
- .ice-scene: fixed, z-index -1, pointer-events none. Navy base
  (#070b16 to #0c1426) in dark, pale (#f4f9ff to #e6f1fb) in light,
  four drifting glacier blobs (#38bdf8/#60a5fa/#818cf8, 30-60s
  transform-only loops), five glossy ice orbs (40-220px radial
  gradients with a highlight at 30% 25% and a blue shadow) placed so the
  glass blurs them, and an SVG feTurbulence grain at 4-5%.
- prefers-reduced-motion stops the drift; phones get two blobs and two
  orbs fewer.

Glass
- One shared recipe (.glass-panel, .card, .sidebar-surface,
  .study-chrome, .tabbar, .glass, .gate-card): rgba(255,255,255,.06)
  dark / .55 light, blur(24px) saturate(140%) (16px on phones), a bright
  1px rim, an inset top highlight and a blue-tinted outer shadow.
- Nested glass (chips, inputs, secondary buttons) is more opaque and
  never adds a second blur layer. @supports fallback for no
  backdrop-filter. Radii: 24-28px cards/sheets, 14px inputs/buttons,
  pills for chips. Cards hover brighter + cyan rim + 2px lift, 180ms.

Colour and type
- Accent is glacier cyan (#38bdf8) to ice blue (#0ea5e9); progress bars
  and selected states use the gradient; the active sidebar item is a
  frosted highlight with a cyan rim.
- Primary CTA: icy gradient — white to #bfe3ff with ink text in dark,
  deep blue in light (biased so the label sits on the darker half and
  keeps 5.6:1).
- Leftover slate/black greys replaced with navy-tinted frost tokens.
- Headings are semibold with tighter tracking.

Screens
- Dashboard: the sidebar is one tall inset rounded glass panel; deck
  cards are glass.
- Gate: one centred glass card over the scene, orbs visible behind it.
- Sheets/modals: glass over the dimmed, lightly blurred backdrop.
- Study view: question card more opaque (.glass-solid); top bar and
  pager are slim glass pills.

Perf: measured on a phone viewport with 4x CPU throttling. The animated
blobs' filter: blur() was the whole cost (150ms vs 33ms per frame), so
they are soft radial gradients with no filter now; idle 60fps, ~17ms
median while scrolling.

Tests updated for the new tokens: contrast 18/18 AA, deck chrome 15/15,
key gate 49/49, install sheet 64/64, deck fixes 15/15, retry 14/14,
zoom 6/6, final ux 12/12, security 8/8, pwa 16/16.
2026-10-08 10:11:55 +00:00

680 lines
23 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.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);
/* 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.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);
}
.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: #90a4c6; /* 6.9: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);
/* 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; }
}
@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;
}
/* 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);
}
/* 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: 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: 14px;
background-image: linear-gradient(135deg, #1d4ed8 0%, #1a5fd0 55%, #0ea5e9 100%);
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);
}
.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);
}
.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) }
.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;
}
}