freezmo/app/globals.css
Martin Dvorak 786d6cf1ae Chat chrome: one gutter, one radius scale, one even glass recipe
The header and composer were full-bleed bands (flush to the top/right/bottom
viewport edges, composer corners off-screen) next to a rail that floats with a
12px margin. The new --layout-gutter (12px) spaces the rail, header, empty-state
card and composer identically on every side; --radius-panel/-control/-chip
(-pill) replace the ad-hoc radii across the shared classes, so every panel is
rounded on all four corners and shared controls match in every theme. The
desktop-only ::after hairlines and the left-corner-only radius are gone.

Aero: the rail and the chat chrome (answers included) share one even recipe -
the wallpaper extra-blurred into a hue-free wash (background-glass.webp) under a
uniform ~50% white veil and a whisper of gloss. Measured within ~10 RGB units
across the rail top->bottom and the card/composer left->right; the blue->green
smear and the teal composer are gone. The composer's field is a clean even tile
with no wallpaper gradient inside it.

Chat fixes: send is a filled primary tile shaped exactly like attach (dimmed
while the input is empty), suggestion/context chips use the shared .chip token
(40/44px target, visible rim, full-contrast text, raw model id in the tooltip),
the AI note reads at 12px --text-muted (.ai-disclaimer), the selected sidebar
row carries the accent border + accent icon, the theme toggle is back in Aero
(the moon flips to Dark; Settings brings Aero back), and the empty state is
centered exactly between header and composer.

Outside CSS: the wallpaper's top-left HUD-circle decoration (it peeked through
the 12px gutters on wide windows) was healed out of background.webp and
background-blur.webp; the orbs/bubbles ride one masked .ice-floats layer that
fades them under the chrome bands and the rail strip, so none is sliced by a
panel edge (still parked under reduced motion).

Version 0.6.0, SW -> freezmo-v82. Tests: chat suite pins the new geometry (12px
gutter, 24px corners, even wash on all four panels, send-vs-attach shape,
chips/input radii); mobile pins the phone chrome and the Aero toggle; bubbles
pins the floats layer + its reduced-motion opt-out.
2026-10-14 21:32:50 +00:00

1772 lines
64 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@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);
--track: rgb(12 32 62 / 0.14);
--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: #40536b; /* 7.2:1 on light glass, 5.9:1 on sheet glass */
--text-subtle: #566a84; /* 5.6:1 on light glass */
--hover: rgb(12 32 62 / 0.05);
--focus: #0ea5e9;
/* Shared layout scale: one gutter for every floating panel and one radius
per role, so the rail, the header, the composer and the cards line up in
every theme — only the surface treatment differs. */
--layout-gutter: 0.75rem; /* 12px — the same inset the rail floats with */
--radius-panel: 1.5rem; /* 24px — floating panels and cards */
--radius-control: 0.875rem; /* 14px — buttons, inputs, tiles */
--radius-chip: 0.75rem; /* 12px — chips, nav rows, small controls */
--radius-pill: 999px; /* fully rounded */
/* 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.68);
--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 */
/* Sheet panels float over a dark scrim, so they need a more opaque surface
than the cards: at 0.55 the muted text under it measured ~2.8:1. */
--glass-sheet-bg: rgb(255 255 255 / 0.93);
--glass-fallback: rgb(247 251 255 / 0.97); /* no backdrop-filter */
--nav-active-bg: rgb(219 238 254 / 0.95);
--nav-active-border: rgb(56 189 248 / 0.65);
/* 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.08), 0 12px 26px -10px rgb(14 21 40 / 0.22);
--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);
--track: rgb(255 255 255 / 0.18);
--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: #c2d1e8; /* ~12:1 */
--text-subtle: #adbfd9; /* ~10:1 on page, 8.4: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-sheet-bg: rgb(17 27 46 / 0.94);
--glass-fallback: rgb(16 26 45 / 0.97);
--nav-active-bg: rgb(56 189 248 / 0.18);
--nav-active-border: rgb(56 189 248 / 0.55);
/* 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.
The four glacier blobs are deliberately whisper-faint now: at 0.18/0.40
they competed with the content (the "huge circles" read as part of the
UI). 25% of the old values. */
:root {
--ice-base-top: #f4f9ff;
--ice-base-bottom: #e6f1fb;
--ice-blob-opacity: 0.045;
--ice-orb-opacity: 0.22;
--ice-grain-opacity: 0.05;
}
.dark {
--ice-base-top: #070b16;
--ice-base-bottom: #0c1426;
--ice-blob-opacity: 0.1;
--ice-orb-opacity: 0.38;
--ice-grain-opacity: 0.045;
}
/* Frutiger Aero — the third theme (Settings → Theme). An underwater scene:
sunlight shafts, caustic patches, reeds on the left, a city silhouette on
the right and bubbles rising through it; glass is a pale blue with the
classic horizon gloss. Token-driven like light/dark; the light/dark toggles
hide while it is on (components/ThemeToggle.tsx). */
.aero {
color-scheme: light;
--page: #1f86c9;
--sidebar: #d6efff;
--surface: #ffffff;
--surface-raised: #ffffff;
--surface-strong: rgb(255 255 255 / 0.72);
--card-hover-bg: rgb(255 255 255 / 0.5);
--surface-sunken: rgb(255 255 255 / 0.5);
--hairline: rgb(255 255 255 / 0.75);
--card-border: var(--glass-border);
--sidebar-divider: rgb(255 255 255 / 0.65);
--hairline-soft: rgb(255 255 255 / 0.55);
--glass-tint: rgb(255 255 255 / 0.6);
--text: #05203a; /* 10.9:1 on the aero glass */
--text-muted: #0a2137; /* 11.5:1 on glass, 5.0:1 on the water */
--text-subtle: #061726; /* 13.6:1 on glass, 5.5:1 on the water */
/* A few components use the frost scale directly (hero descriptions, the
wizard's step help): darker here, because they sit on the water. */
--color-frost-600: #16324a;
--color-frost-700: #102a41;
--hover: rgb(255 255 255 / 0.32);
--focus: #0369a1;
/* Pale blue glass, like the mock: bright enough for dark navy text. */
--glass-bg: rgb(228 246 255 / 0.8);
--glass-border: rgb(255 255 255 / 0.9);
--glass-highlight: rgb(255 255 255 / 0.98);
--glass-shadow: 0 18px 44px -20px rgb(2 62 110 / 0.55);
--glass-solid-bg: rgb(232 248 255 / 0.9);
--glass-sheet-bg: rgb(232 246 255 / 0.94);
--glass-fallback: rgb(226 242 253 / 0.97);
--nav-active-bg: rgb(255 255 255 / 0.5);
--nav-active-border: rgb(14 165 233 / 0.75);
--gate-card: rgb(226 244 255 / 0.7);
--gate-callout-bg: rgb(255 255 255 / 0.6);
--gate-callout-border: rgb(56 189 248 / 0.45);
--field: rgb(255 255 255 / 0.78);
--field-border: rgb(255 255 255 / 0.95);
--shadow-gate: 0 30px 70px -34px rgb(2 62 110 / 0.5), 0 1px 2px rgb(2 62 110 / 0.08);
--install-sheet: rgb(226 244 255 / 0.7);
--install-card: rgb(236 248 255 / 0.78);
--install-chip: rgb(255 255 255 / 0.62);
--install-border: rgb(255 255 255 / 0.88);
--shadow-card: 0 2px 4px rgb(2 62 110 / 0.12), 0 14px 30px -16px rgb(2 62 110 / 0.4);
--card-shadow: var(--shadow-card);
--shadow-card-hover: 0 26px 60px -26px rgb(2 90 150 / 0.6), 0 3px 8px rgb(2 90 150 / 0.25);
/* Scene tokens: the water itself is painted below. */
--ice-base-top: #2fa7e6;
--ice-base-bottom: #0a5490;
--ice-blob-opacity: 0.5;
--ice-orb-opacity: 0.6;
--ice-grain-opacity: 0.02;
}
/* The scene is the wallpaper itself (a Frutiger Aero scene made with Gemini,
in public/aero/). Only the moving bubbles are drawn on top; the image
already has grass, sun, water and clouds. */
.aero .ice-scene {
background-image: url("/aero/background.webp?v=4");
background-size: cover;
background-position: center 42%;
background-repeat: no-repeat;
}
/* The drawn extras belong to the old CSS scene: off over the photo. */
.aero .ice-scene::before,
.aero .ice-scene::after,
.aero .ice-blob {
display: none;
}
/* The orbs become extra bubbles over the photo. */
.aero .ice-orb {
background:
radial-gradient(circle at 30% 24%, rgb(255 255 255 / 0.95) 0%, rgb(255 255 255 / 0) 16%),
radial-gradient(circle at 72% 80%, rgb(255 255 255 / 0.4) 0%, rgb(255 255 255 / 0) 20%),
radial-gradient(
circle at 32% 28%,
rgb(255 255 255 / 0.9) 0%,
rgb(226 248 255 / 0.4) 22%,
rgb(125 211 252 / 0.14) 56%,
rgb(56 189 248 / 0.38) 100%
);
box-shadow:
inset -8px -12px 20px -10px rgb(3 74 124 / 0.5),
inset 8px 10px 16px -10px rgb(255 255 255 / 1),
inset 0 0 0 1px rgb(255 255 255 / 0.35),
0 12px 26px -16px rgb(3 74 124 / 0.55);
}
/* The Aero gloss for the floating surfaces (sheets, dropdowns and the sticky
chrome keep a real backdrop-filter — they sit over scrolling content).
Thin like the card glass: a whisper of sheen, not a white lid. */
.aero .glass-panel,
.aero .study-chrome,
.aero .tabbar,
.aero .glass {
background-image: linear-gradient(
180deg,
rgb(255 255 255 / 0.28) 0%,
rgb(255 255 255 / 0.08) 46%,
rgb(255 255 255 / 0.03) 47%,
rgb(255 255 255 / 0.08) 100%
);
}
/* Rising bubbles. Hidden entirely in the other themes, so they cost nothing
there; under reduced motion they sit still instead of rising. */
.ice-bubble {
display: none;
}
.aero .ice-bubble {
display: block;
position: absolute;
bottom: -14vh;
border-radius: 50%;
opacity: 0;
background:
radial-gradient(circle at 30% 24%, rgb(255 255 255 / 0.95) 0%, rgb(255 255 255 / 0) 16%),
radial-gradient(circle at 72% 80%, rgb(255 255 255 / 0.4) 0%, rgb(255 255 255 / 0) 20%),
radial-gradient(
circle at 32% 28%,
rgb(255 255 255 / 0.9) 0%,
rgb(226 248 255 / 0.38) 24%,
rgb(125 211 252 / 0.12) 58%,
rgb(56 189 248 / 0.34) 100%
);
box-shadow:
inset -6px -10px 16px -8px rgb(3 74 124 / 0.5),
inset 6px 8px 14px -8px rgb(255 255 255 / 1),
inset 0 0 0 1px rgb(255 255 255 / 0.35),
0 10px 22px -14px rgb(3 74 124 / 0.5);
border: 1px solid rgb(255 255 255 / 0.25);
animation: aero-bubble-rise var(--bubble-duration, 20s) linear infinite;
animation-delay: var(--bubble-delay, 0s);
will-change: transform, opacity;
}
.ice-paused .ice-bubble {
animation-play-state: paused;
}
@keyframes aero-bubble-rise {
0% { transform: translate3d(0, 0, 0) scale(0.85); opacity: 0; }
6% { opacity: var(--bubble-opacity, 0.9); }
50% { transform: translate3d(var(--bubble-sway, 3vw), -52vh, 0) scale(1); }
94% { opacity: var(--bubble-opacity, 0.9); }
100% { transform: translate3d(calc(var(--bubble-sway, 3vw) * -0.5), -104vh, 0) scale(1.06); opacity: 0; }
}
.aero .ice-bubble-1 { width: 104px; height: 104px; left: 7%; --bubble-duration: 19s; --bubble-delay: -2s; --bubble-sway: 4vw; }
.aero .ice-bubble-2 { width: 34px; height: 34px; left: 16%; --bubble-duration: 13s; --bubble-delay: -7s; --bubble-sway: -3vw; }
.aero .ice-bubble-3 { width: 64px; height: 64px; left: 28%; --bubble-duration: 22s; --bubble-delay: -12s; --bubble-sway: 3vw; }
.aero .ice-bubble-4 { width: 20px; height: 20px; left: 38%; --bubble-duration: 11s; --bubble-delay: -4s; --bubble-sway: 2vw; }
.aero .ice-bubble-5 { width: 88px; height: 88px; left: 48%; --bubble-duration: 25s; --bubble-delay: -16s; --bubble-sway: -4vw; }
.aero .ice-bubble-6 { width: 28px; height: 28px; left: 58%; --bubble-duration: 12s; --bubble-delay: -9s; --bubble-sway: 3vw; }
.aero .ice-bubble-7 { width: 72px; height: 72px; left: 68%; --bubble-duration: 20s; --bubble-delay: -5s; --bubble-sway: -2vw; }
.aero .ice-bubble-8 { width: 46px; height: 46px; left: 78%; --bubble-duration: 16s; --bubble-delay: -13s; --bubble-sway: 4vw; }
.aero .ice-bubble-9 { width: 24px; height: 24px; left: 87%; --bubble-duration: 14s; --bubble-delay: -6s; --bubble-sway: -3vw; }
.aero .ice-bubble-10 { width: 120px; height: 120px; left: 92%; --bubble-duration: 27s; --bubble-delay: -20s; --bubble-sway: -5vw; --bubble-opacity: 0.75; }
.aero .ice-bubble-11 { width: 18px; height: 18px; left: 4%; --bubble-duration: 12s; --bubble-delay: -3s; --bubble-sway: 3vw; }
.aero .ice-bubble-12 { width: 58px; height: 58px; left: 12%; --bubble-duration: 18s; --bubble-delay: -11s; --bubble-sway: -3vw; }
.aero .ice-bubble-13 { width: 30px; height: 30px; left: 21%; --bubble-duration: 15s; --bubble-delay: -5s; --bubble-sway: -2vw; }
.aero .ice-bubble-14 { width: 96px; height: 96px; left: 34%; --bubble-duration: 23s; --bubble-delay: -19s; --bubble-sway: 4vw; --bubble-opacity: 0.8; }
.aero .ice-bubble-15 { width: 22px; height: 22px; left: 42%; --bubble-duration: 10s; --bubble-delay: -8s; --bubble-sway: -3vw; }
.aero .ice-bubble-16 { width: 44px; height: 44px; left: 54%; --bubble-duration: 16s; --bubble-delay: -2s; --bubble-sway: 2vw; }
.aero .ice-bubble-17 { width: 62px; height: 62px; left: 62%; --bubble-duration: 21s; --bubble-delay: -14s; --bubble-sway: -4vw; }
.aero .ice-bubble-18 { width: 26px; height: 26px; left: 74%; --bubble-duration: 13s; --bubble-delay: -6s; --bubble-sway: 3vw; }
/* Helper lines that sit straight on the wallpaper (the keyboard tip, the AI
note) need their own surface in Aero: dark text on the grass measured
2.5:1. A small glass chip fixes it without hiding the wallpaper. */
.aero .hint-line {
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 0.75rem;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
/* Same trap as the buttons above: use the Tailwind utilities. */
@apply backdrop-blur-md backdrop-saturate-150;
}
/* Bright white rims on the study/homework segments (the frosted fill itself
comes from the shared glass recipe below). */
.aero .segmented > button,
.aero .segmented > a {
border-color: rgb(255 255 255 / 0.5);
}
/* Liquid glass buttons: translucent, glossed top half, bright rim — the
Vista/Aero button, not a solid fill. */
.aero .grad-ice,
.aero .btn-primary,
.aero .segmented > button[aria-pressed="true"],
.aero .segmented > a[aria-current="page"] {
border: 1px solid rgb(255 255 255 / 0.6);
color: #04223c;
box-shadow: 0 12px 26px -16px rgb(2 62 110 / 0.55), inset 0 1px 0 rgb(255 255 255 / 0.9);
/* The glass itself comes from the frosted recipes below. */
}
/* Liquid glass: the surfaces themselves are the frosted wallpaper recipes
below. These tokens are the fills for the layers that keep a real backdrop
blur (sheets, dropdowns, the sticky chrome) and for nested chips, so they
stay thin too. Contrast is pixel-scanned in all three themes, so the tints
cannot drop below what the text needs. */
.aero {
--glass-bg: rgb(228 246 255 / 0.22);
--glass-solid-bg: rgb(232 248 255 / 0.3);
--glass-sheet-bg: rgb(232 248 255 / 0.64);
--surface-strong: rgb(255 255 255 / 0.23);
--surface-sunken: rgb(255 255 255 / 0.17);
--track: rgb(255 255 255 / 0.38);
--nav-active-bg: rgb(255 255 255 / 0.5);
}
/* Liquid glass — one recipe for every surface, so the rail, the cards, the
chips, the segments and the buttons all read exactly like the Ask pill: a
pre-blurred, slightly lifted copy of the wallpaper (glued to the viewport,
same cover/42% geometry as the scene) under a whisper-thin veil and a
bright rim. Real backdrop-filter is skipped on purpose: the bubbles move
behind every surface, so it re-runs ~60 times a second (measured 66ms per
frame at 2x CPU throttle vs 17ms with nothing blurring). The veils are the
minimum the pixel-contrast scan needs to hold 4.5:1 for the small muted
text — anything more and the glass reads as a white panel. */
.aero .card,
.aero .glass-solid,
.aero .sidebar-surface,
.aero .gate-card,
.aero .list-group,
.aero .nav-action,
.aero .nav-row[aria-current="page"],
.aero .nav-row[aria-pressed="true"],
.aero .nav-row[data-active="true"],
.aero .glass-chip,
.aero .glass-pill,
.aero .btn-secondary,
.aero .chat-header,
.aero .chat-empty,
.aero .segmented,
/* Hand-rolled frost fills in the study, exam, chat and homework flows:
white/xx utilities and the surface tokens — without this they render as
flat white chips next to the glass. */
.aero [class*="bg-white/35"],
.aero [class*="bg-white/40"],
.aero [class*="bg-white/45"],
.aero [class*="bg-white/50"],
.aero [class*="bg-white/55"],
.aero [class*="bg-white/60"],
.aero [class*="bg-white/70"],
.aero [class*="bg-[var(--surface)]"],
.aero [class*="bg-[var(--surface-strong)]"],
.aero [class*="bg-[var(--surface-sunken)]"],
.aero [class*="bg-frost-50"] {
background-image:
linear-gradient(
180deg,
rgb(255 255 255 / 0.15) 0%,
rgb(255 255 255 / 0.06) 46%,
rgb(255 255 255 / 0.05) 53%,
rgb(255 255 255 / 0.09) 100%
),
linear-gradient(rgb(255 255 255 / 0.16), rgb(255 255 255 / 0.16)),
url("/aero/background-blur.webp?v=4");
background-size: 100% 100%, 100% 100%, cover;
background-position: 0 0, 0 0, center 42%;
background-attachment: scroll, scroll, fixed;
background-repeat: no-repeat, no-repeat, no-repeat;
}
/* The wizard's phone action bar: Aero's page token is the saturated water
blue, which read as a heavy slab over the photo — give it the same thin
glass as everything else (it already has a real backdrop blur). */
.aero [data-wizard-bar] {
background: rgb(255 255 255 / 0.2);
border-top-color: rgb(255 255 255 / 0.6);
box-shadow: 0 -14px 34px -22px rgb(2 62 110 / 0.5);
}
/* Interactive glass feedback: a soft inner glow and a brighter rim on hover,
an inner shade while pressed. Deliberately no transform and no filter — on
a pane with a viewport-fixed backdrop either one re-anchors the glass and
it flashes a wrong crop of the wallpaper (the old hover lift did exactly
that to the deck cards). */
.aero .nav-action,
.aero .nav-row,
.aero .glass-chip,
.aero .glass-pill,
.aero .btn-secondary,
.aero .btn-primary,
.aero .segmented > button,
.aero .segmented > a,
.aero .card-hover {
transition: box-shadow 170ms ease, border-color 170ms ease, background-color 170ms ease, color 170ms ease;
}
@media (hover: hover) and (pointer: fine) {
.aero .nav-action:hover,
.aero .nav-row:hover,
.aero .glass-chip:hover,
.aero .glass-pill:hover,
.aero .btn-secondary:hover,
.aero .btn-primary:hover,
.aero .segmented > button:hover,
.aero .segmented > a:hover,
.aero .card-hover:hover {
border-color: rgb(255 255 255 / 1);
background-color: rgb(255 255 255 / 0.34);
filter: none;
box-shadow:
0 18px 38px -20px rgb(2 62 110 / 0.6),
inset 0 1px 0 rgb(255 255 255 / 1),
inset 0 0 30px rgb(255 255 255 / 0.55);
}
.aero .card-hover:hover {
transform: none;
}
}
.aero .card-hover:active,
.aero .card-hover:hover,
.aero .press:active,
.aero .btn-primary:active,
.aero .segmented > button:active,
.aero .segmented > a:active,
.aero .nav-action:active,
.aero .glass-pill:active,
.aero .btn-secondary:active {
transform: none;
filter: none;
}
.aero .card-hover:active,
.aero .press:active,
.aero .btn-primary:active,
.aero .nav-action:active,
.aero .glass-pill:active,
.aero .btn-secondary:active {
box-shadow: inset 0 2px 8px rgb(2 62 110 / 0.2), inset 0 1px 0 rgb(255 255 255 / 0.8);
}
/* The conversation scrolls right up to the glass header; a hard-clipped line
of text at that edge reads as a rendering bug over the busy wallpaper, so
the top of the scroll area fades out instead (the mask is fixed to the
element, not the content — and stays inside the column's 16px top padding,
so at the very top nothing is dimmed). */
.aero .chat-scroll {
mask-image: linear-gradient(to bottom, transparent 0, black 14px);
-webkit-mask-image: linear-gradient(to bottom, transparent 0, black 14px);
}
/* Chat: the longest text in the app sits straight on the wallpaper. The
assistant answers are deliberately bubble-less in the flat themes, and the
user bubble's thin tint washes out over the bright parts of the scene — in
Aero both get a real pane: the same even wash as the chrome, a touch more
opaque (this is where the reading happens), so an answer reads as its own
bubble and its colour does not drift with the photo behind it. */
.aero .chat-bubble,
.aero .chat-answer {
border-color: rgb(255 255 255 / 0.85);
background-image:
linear-gradient(rgb(255 255 255 / 0.6), rgb(255 255 255 / 0.6)),
url("/aero/background-glass.webp?v=4");
background-size: 100% 100%, cover;
background-position: 0 0, center 42%;
background-attachment: scroll, fixed;
background-repeat: no-repeat, no-repeat;
box-shadow: 0 16px 34px -22px rgb(2 62 110 / 0.45), inset 0 1px 0 rgb(255 255 255 / 0.9);
}
.aero .chat-answer {
border: 1px solid rgb(255 255 255 / 0.85);
border-radius: var(--radius-panel);
border-bottom-left-radius: var(--radius-chip);
padding: 0.7rem 0.95rem;
}
/* The phone tab bar carries its labels right at the bottom of the water, so
it keeps a little more veil than the cards. */
.aero .tabbar {
background-image:
linear-gradient(
180deg,
rgb(255 255 255 / 0.22) 0%,
rgb(255 255 255 / 0.1) 46%,
rgb(255 255 255 / 0.08) 53%,
rgb(255 255 255 / 0.13) 100%
),
url("/aero/background-blur.webp?v=4");
background-size: 100% 100%, cover;
background-position: 0 0, center 42%;
background-attachment: scroll, fixed;
background-repeat: no-repeat, no-repeat;
}
/* Touch devices: flat glass, no bake and no live blur. The baked layer is
`background-attachment: fixed`, which mobile engines repaint late (iOS
treats it as scroll) — panes showed a stale crop until a tap forced a
repaint. The honest alternative (a real backdrop-filter) is worse: every
pane re-blurs on every frame while the bubbles animate, and phones choke —
measured as input lag, "buttons take ages to respond". So on touch the
panes keep the thin Ask-pill tint over the sharp scene: transparent glass
at zero per-frame cost. Desktop keeps the baked blur. */
@media (max-width: 639px), (pointer: coarse) {
.aero .card,
.aero .glass-solid,
.aero .sidebar-surface,
.aero .gate-card,
.aero .list-group,
.aero .nav-action,
.aero .nav-row[aria-current="page"],
.aero .nav-row[aria-pressed="true"],
.aero .nav-row[data-active="true"],
.aero .glass-chip,
.aero .glass-pill,
.aero .btn-secondary,
.aero .btn-ghost,
.aero .chat-header,
.aero .chat-empty,
.aero .segmented {
background-image: none;
background-color: rgb(255 255 255 / 0.32);
}
.aero .chat-bubble,
.aero .chat-answer {
background-image: none;
background-color: rgb(255 255 255 / 0.55);
}
/* The chat chrome and the rail keep one even, a little stronger tint on
touch: without the baked blur the flat fill has to carry the surface on
its own, or the photo underneath tints it unevenly again. */
.aero .chat-header,
.aero .chat-empty,
.aero .chat-composer-band {
background-image: none;
background-color: rgb(255 255 255 / 0.55);
}
.aero .tabbar {
background-image: none;
}
/* The hand-rolled frost fills drop the bake too and keep their own tints. */
.aero [class*="bg-white/35"],
.aero [class*="bg-white/40"],
.aero [class*="bg-white/45"],
.aero [class*="bg-white/50"],
.aero [class*="bg-white/55"],
.aero [class*="bg-white/60"],
.aero [class*="bg-white/70"],
.aero [class*="bg-[var(--surface)]"],
.aero [class*="bg-[var(--surface-strong)]"],
.aero [class*="bg-[var(--surface-sunken)]"],
.aero [class*="bg-frost-50"] {
background-image: none;
}
}
/* Buttons need more presence than the cards: a stronger veil so a button reads
as pale glass over grass, sky or water — the "Ask pill" look — instead of
blending into the blurred scene. */
.aero .btn-secondary,
.aero .glass-pill {
background-image:
linear-gradient(
180deg,
rgb(255 255 255 / 0.52) 0%,
rgb(255 255 255 / 0.36) 46%,
rgb(255 255 255 / 0.32) 53%,
rgb(255 255 255 / 0.44) 100%
),
url("/aero/background-blur.webp?v=4");
background-size: 100% 100%, cover;
background-position: 0 0, center 42%;
background-attachment: scroll, fixed;
background-repeat: no-repeat, no-repeat;
}
.aero .btn-ghost {
background-image:
linear-gradient(
180deg,
rgb(255 255 255 / 0.52) 0%,
rgb(255 255 255 / 0.36) 46%,
rgb(255 255 255 / 0.32) 53%,
rgb(255 255 255 / 0.44) 100%
),
url("/aero/background-blur.webp?v=4");
background-size: 100% 100%, cover;
background-position: 0 0, center 42%;
background-attachment: scroll, fixed;
background-repeat: no-repeat, no-repeat;
border: 1px solid rgb(255 255 255 / 0.6);
color: var(--text);
}
/* Chat chrome and the rail: one even frosted recipe. The pre-blurred,
extra-soft wash of the wallpaper (background-glass) sits under a uniform
white veil, so the surface colour cannot drift with the photo behind it —
the rail used to run from clear blue into a muddy green smear, the composer
picked up the river's teal and the header a saturated stripe of sky. */
.aero .sidebar-surface,
.aero .chat-header,
.aero .chat-empty,
.aero .chat-composer-band {
background-image:
linear-gradient(
180deg,
rgb(255 255 255 / 0.3) 0%,
rgb(255 255 255 / 0.1) 46%,
rgb(255 255 255 / 0.05) 53%,
rgb(255 255 255 / 0.12) 100%
),
linear-gradient(rgb(255 255 255 / 0.46), rgb(255 255 255 / 0.46)),
url("/aero/background-glass.webp?v=4");
background-size: 100% 100%, 100% 100%, cover;
background-position: 0 0, 0 0, center 42%;
background-attachment: scroll, scroll, fixed;
background-repeat: no-repeat, no-repeat, no-repeat;
}
/* The chat welcome (title, subtitle, suggestion chips): a card like the
library's, stretched across the conversation column so it lines up with the
floating header above and composer below it. */
.aero .chat-empty {
margin-inline: -1rem;
padding: 1.35rem 1rem 1.1rem;
border-radius: var(--radius-panel);
border: 1px solid var(--glass-border);
background-color: var(--glass-bg);
box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-highlight);
}
@media (min-width: 1024px) {
.aero .chat-empty {
margin-inline: -2.5rem;
padding: 1.5rem 2.5rem 1.25rem;
}
}
/* The column floats inside the main pane with the shared gutter; on desktop
the rail already carries its own 12px right-hand gutter, so the column
drops its left padding — every gap between the panels is then the same
--layout-gutter. (Unlayered on purpose: it must beat the Tailwind
`p-[var(--layout-gutter)]` utility.) */
@media (min-width: 1024px) {
.chat-column { padding-left: 0; }
}
/* The composer's field: a clean, even glass tile — no wallpaper gradient
inside it, just a lighter veil than the panel around it. */
.aero .chat-composer-band textarea,
.aero .chat-composer-band input[type="search"] {
background-image: none;
background-color: rgb(255 255 255 / 0.6);
border-color: rgb(255 255 255 / 0.9);
}
/* Page headings that sit straight on the wallpaper. */
.aero .page-heading {
display: inline-block;
padding: 0.45rem 0.85rem;
border-radius: 1.25rem;
border: 1px solid rgb(255 255 255 / 0.45);
background-color: rgb(255 255 255 / 0.32);
}
/* The rail used to weight its lower half (the grass read darker through the
glass) with a gradient that left a visible blue → green smear. It now uses
the same even veil as the chat chrome — see the shared recipe above — so
every row keeps the same contrast from top to bottom. */
/* Inside a sheet or dialog the backdrop is not the raw scene — a scrim and a
more opaque sheet glass sit in between — so the viewport-aligned frosted
layer would not match what is actually behind the element (it showed the
dark water under the settings theme labels: 1.5:1). Nested chrome falls
back to the flat fills there; the sheet itself is genuinely blurred. */
.aero .glass-sheet .card,
.aero .glass-sheet .glass-solid,
.aero .glass-sheet .list-group,
.aero .glass-sheet .glass-chip,
.aero .glass-sheet .segmented,
.aero .glass-sheet .segmented > button[aria-pressed="true"],
.aero .glass-sheet .segmented > a[aria-current="page"],
.aero .glass-sheet .btn-secondary,
.aero .glass-sheet .btn-primary,
.aero .glass-sheet .grad-ice,
.aero .glass-sheet [class*="bg-white/35"],
.aero .glass-sheet [class*="bg-white/40"],
.aero .glass-sheet [class*="bg-white/45"],
.aero .glass-sheet [class*="bg-white/50"],
.aero .glass-sheet [class*="bg-white/55"],
.aero .glass-sheet [class*="bg-white/60"],
.aero .glass-sheet [class*="bg-white/70"],
.aero .glass-sheet [class*="bg-[var(--surface)]"],
.aero .glass-sheet [class*="bg-[var(--surface-strong)]"],
.aero .glass-sheet [class*="bg-[var(--surface-sunken)]"],
.aero .glass-sheet [class*="bg-frost-50"] {
background-image: none;
}
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;
}
/* A full-screen overlay (study feed, review/exam session, homework guide) is
transparent on purpose so the fixed ice scene stays visible — which also
means the app chrome of the page behind it would ghost through the glass.
While one is open, that chrome is hidden instead. */
body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
visibility: hidden;
}
/* 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: slow bubbles that rise from just below the bottom edge to
above the top edge, swaying gently and fading in/out near the edges so the
loop never pops. Only transform + opacity animate, each orb on its own
composited layer (will-change: transform): no filter/backdrop-filter, and no
animated size, offsets or shadow. Per-orb duration, delay (negative, so the
scene is already populated on load), horizontal position and sway keep them
out of sync. */
.ice-orb {
position: absolute;
top: 100%;
border-radius: 50%;
opacity: var(--ice-orb-opacity);
will-change: transform;
animation: ice-rise var(--ice-orb-duration, 48s) ease-in-out infinite;
animation-delay: var(--ice-orb-delay, 0s);
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; left: 12%; --ice-orb-duration: 46s; --ice-orb-delay: -18s; --ice-sway: 4vw; }
.ice-orb-2 { width: 84px; height: 84px; left: 4%; --ice-orb-duration: 58s; --ice-orb-delay: -41s; --ice-sway: -3vw; }
.ice-orb-3 { width: 220px; height: 220px; right: 10%; --ice-orb-duration: 36s; --ice-orb-delay: -7s; --ice-sway: 5vw; }
.ice-orb-4 { width: 52px; height: 52px; right: 28%; --ice-orb-duration: 64s; --ice-orb-delay: -52s; --ice-sway: -4vw; }
.ice-orb-5 { width: 120px; height: 120px; left: 38%; --ice-orb-duration: 52s; --ice-orb-delay: -27s; --ice-sway: 3vw; }
/* The rise: -100vh - 100% clears the top edge (100% = the orb's own height);
the sway crosses the path twice and the scale drifts slightly. Opacity
reaches the token value by 12% and holds it until 88%, so the fades happen
while the orb is still partly outside the scene. */
@keyframes ice-rise {
0% { transform: translate3d(0, 0, 0) scale(0.94); opacity: 0; }
12% { opacity: var(--ice-orb-opacity); }
32% { transform: translate3d(var(--ice-sway, 0px), calc(-32vh - 32%), 0) scale(0.97); }
58% { transform: translate3d(calc(var(--ice-sway, 0px) * -0.55), calc(-58vh - 58%), 0) scale(1.02); }
80% { transform: translate3d(calc(var(--ice-sway, 0px) * 0.4), calc(-80vh - 80%), 0) scale(1.04); }
88% { opacity: var(--ice-orb-opacity); }
100% { transform: translate3d(0, calc(-100vh - 100%), 0) scale(1.06); opacity: 0; }
}
/* The rising orbs and bubbles are clipped to the scene, but the floating
glass panels (the rail, the chat header/composer, the tab bar) sit on top of
it — a bubble reaching one of those edges looked sliced by a straight line.
This layer keeps them inside the open middle band: they fade in below the
top chrome, out above the bottom chrome (the values come from where the
header and the composer actually end) and, on desktop, stay out of the rail
strip. Static under reduced motion: nothing moves there, and the parked
orbs keep their spots. */
.ice-floats {
position: absolute;
inset: 0;
overflow: hidden;
-webkit-mask-image: linear-gradient(
to bottom,
transparent 0,
transparent 68px,
black 150px,
black calc(100% - 330px),
transparent calc(100% - 172px)
);
mask-image: linear-gradient(
to bottom,
transparent 0,
transparent 68px,
black 150px,
black calc(100% - 330px),
transparent calc(100% - 172px)
);
}
@media (min-width: 640px) {
.ice-floats {
-webkit-mask-image: linear-gradient(
to bottom,
transparent 0,
transparent 68px,
black 150px,
black calc(100% - 300px),
transparent calc(100% - 151px)
);
mask-image: linear-gradient(
to bottom,
transparent 0,
transparent 68px,
black 150px,
black calc(100% - 300px),
transparent calc(100% - 151px)
);
}
}
@media (min-width: 1024px) {
/* …and the rail's strip (plus its gutter) stays clear too. */
.ice-floats {
-webkit-mask-image:
linear-gradient(
to bottom,
transparent 0,
transparent 68px,
black 150px,
black calc(100% - 300px),
transparent calc(100% - 151px)
),
linear-gradient(to right, transparent 0, transparent 260px, black 360px);
mask-image:
linear-gradient(
to bottom,
transparent 0,
transparent 68px,
black 150px,
black calc(100% - 300px),
transparent calc(100% - 151px)
),
linear-gradient(to right, transparent 0, transparent 260px, black 360px);
-webkit-mask-composite: destination-in;
mask-composite: intersect;
}
}
@media (prefers-reduced-motion: reduce) {
.ice-floats { -webkit-mask-image: none !important; mask-image: none !important; }
}
html.reduce-motion .ice-floats {
-webkit-mask-image: none !important;
mask-image: none !important;
}
/* 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; }
/* Bubbles sit still (and visible) instead of rising. */
.aero .ice-bubble { animation: none; opacity: 0.55; transform: translate3d(0, -60vh, 0); }
/* Orbs go back to the pre-rise static layout (a paused animation is not
applied at all in Chrome, which would leave them parked below the fold)
and give their composited layers back. */
.ice-orb {
animation: none;
will-change: auto;
}
.ice-orb-1 { top: 14%; left: 12%; }
.ice-orb-2 { top: 58%; left: 4%; }
.ice-orb-3 { top: auto; bottom: -70px; right: 10%; }
.ice-orb-4 { top: 24%; right: 28%; }
.ice-orb-5 { top: auto; bottom: 16%; left: 38%; }
}
/* Background tab: park every scene animation, so an invisible page costs no
compositor work. */
.ice-paused .ice-orb,
.ice-paused .ice-blob {
animation-play-state: paused;
}
/* 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-duration: 54s; --ice-orb-delay: -21s; }
.ice-orb-2 { --ice-orb-duration: 64s; --ice-orb-delay: -46s; }
.ice-orb-3 { width: 150px; height: 150px; --ice-orb-duration: 44s; --ice-orb-delay: -8s; }
}
/* 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,
tuck the orbs to the edges, away from the text column and the tab bar, and
slow their rise down further so the drifting slivers stay calm. */
@media (max-width: 639px) {
:root {
--ice-blob-opacity: 0.025;
--ice-orb-opacity: 0.14;
}
.dark {
--ice-blob-opacity: 0.05;
--ice-orb-opacity: 0.2;
}
/* Phones: orbs hug the screen edges so they can never sit under the
content column's cards or primary buttons. The lower-right orb still
read as a glow behind the wizard action bar, so phones drop it. */
.ice-orb-1 { width: 96px; height: 96px; left: -48px; --ice-orb-duration: 62s; --ice-orb-delay: -30s; --ice-sway: 5vw; }
.ice-orb-2 { width: 72px; height: 72px; left: -44px; --ice-orb-duration: 68s; --ice-orb-delay: -49s; --ice-sway: -4vw; }
.ice-orb-3 { display: none; }
}
/* The 404 mascot: the scene's glossy ice orb, at full strength so it reads on
the card glass in every theme (the scene's own orbs run at 0.22–0.6). */
.not-found-orb {
border-radius: 50%;
background: radial-gradient(
circle at 32% 26%,
rgb(255 255 255 / 0.98) 0%,
rgb(219 242 255 / 0.92) 26%,
rgb(125 200 245 / 0.95) 58%,
rgb(38 121 208 / 1) 100%
);
box-shadow:
inset -10px -14px 24px -12px rgb(3 74 124 / 0.6),
inset 8px 12px 20px -10px rgb(255 255 255 / 0.95),
inset 0 0 0 1px rgb(255 255 255 / 0.5),
0 18px 36px -18px rgb(2 62 110 / 0.6);
}
@layer components {
/* Frosted ice glass ------------------------------------------------------
Two recipes now:
- **Floating glass** (sheets, dropdowns, dialogs and the sticky chrome):
the tint below plus a backdrop blur — the blur is what reads as glass
when content scrolls underneath (the app header, the study chrome, the
tab bar, a sheet over the page).
- **Static glass** (cards, the rail, the gate, the study prompt card): the
exact same tint, rim, highlight and shadow, but **no** backdrop-filter.
On a smooth gradient the blur is invisible — and because the ice scene
animates, every blurred surface re-runs the filter on every frame.
Measured on desktop (2x throttle, software renderer): p50 frame time
66ms while the static surfaces blurred the scene, 33ms without them,
17ms with the scene's animation also stopped.
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 {
background-color: var(--glass-bg);
box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-highlight);
}
/* Only the layers that can have content behind them blur. */
.glass-panel,
.study-chrome,
.tabbar,
.glass {
@apply backdrop-blur-lg backdrop-saturate-[1.4];
}
.glass-panel {
border: 1px solid var(--glass-border);
border-radius: var(--radius-panel);
}
/* 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.
background-color (not the shorthand) so the Aero gloss can layer on top. */
.glass-solid {
background-color: var(--glass-solid-bg);
}
/* Sheets and dialogs sit over a scrim (and, on phones, over the page), so
their text needs a more opaque surface than the cards have. */
.glass-sheet {
background-color: var(--glass-sheet-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: var(--radius-panel); /* 24px */
border: 1px solid var(--glass-border);
}
.gate-card {
border: 1px solid var(--glass-border);
border-radius: var(--radius-panel);
}
/* Blur is expensive: halve it on phones (floating glass only). */
@media (max-width: 1023px) {
.glass-panel,
.study-chrome,
.tabbar,
.glass {
@apply backdrop-blur-md;
}
}
/* No backdrop-filter support: fall back to a more opaque surface (floating
glass only — the static surfaces already look right without a filter). */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
.glass-panel,
.study-chrome,
.tabbar,
.glass {
background-color: 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: var(--radius-control);
@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);
}
/* Shared chip ------------------------------------------------------------
One control language for the chat's suggestion chips, the composer's
context/model chips and the small chips inside a message: a light glass
fill, a visible rim, full-contrast text and a real tap target (44px on
phones, like every other control). */
.chip {
display: inline-flex;
align-items: center;
gap: 0.4rem;
min-height: 2.5rem;
border-radius: var(--radius-chip);
border: 1px solid var(--glass-border);
background: var(--surface-strong);
color: var(--text);
font-weight: 600;
box-shadow: inset 0 1px 0 var(--glass-highlight);
transition: background-color 160ms ease, border-color 160ms ease, color 160ms ease;
}
.chip:hover {
background: var(--hover);
border-color: var(--nav-active-border);
}
@media (max-width: 639px) {
.chip { min-height: 44px; }
}
/* The AI note under a surface that shows model output -------------------- */
.ai-disclaimer {
font-size: 12px;
line-height: 1.4;
color: var(--text-muted);
text-align: center;
}
/* iOS segmented control ------------------------------------------------- */
.segmented {
display: flex;
gap: 2px;
padding: 3px;
border-radius: var(--radius-control);
@apply backdrop-blur-sm;
background: var(--surface-sunken);
border: 1px solid var(--hairline);
}
.segmented > button {
flex: 1;
border-radius: var(--radius-chip);
padding: 0.42rem 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: var(--text);
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: var(--radius-chip);
padding: 0.42rem 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: var(--text);
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);
}
/* Aero's page is the water itself, so mixing it would leave a saturated
blue bar under dark labels (measured ~3.9:1). Mix the pale sheet glass. */
.aero .tabbar {
background: color-mix(in oklab, var(--glass-sheet-bg) 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: var(--radius-panel);
}
/* Sidebar nav rows */
.nav-row {
display: flex;
align-items: center;
gap: 0.625rem;
width: 100%;
border-radius: var(--radius-chip);
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);
}
/* The rail's two creation actions ("New deck", "New homework") are buttons,
not nav rows: once the library is full the empty-state CTAs are long gone,
and on desktop the rail is the only place left to start something. */
.nav-action {
display: flex;
align-items: center;
gap: 0.625rem;
width: 100%;
border-radius: var(--radius-chip);
padding: 0.5rem 0.625rem;
font-size: 13.5px;
font-weight: 600;
color: var(--text);
background: var(--surface-strong);
border: 1px solid var(--glass-border);
transition: background-color 160ms ease, border-color 160ms ease;
}
.nav-action:hover {
background: var(--hover);
border-color: var(--nav-active-border);
}
.nav-row[aria-current="page"],
.nav-row[aria-pressed="true"] {
background: var(--nav-active-bg);
color: var(--text);
font-weight: 600;
box-shadow: inset 0 0 0 1.5px var(--nav-active-border), inset 0 1px 0 var(--glass-highlight);
}
/* …and the icon carries the accent, so the selected row can never be mistaken
for one more tinted action button ("New deck", "New homework"). */
.nav-row[aria-current="page"] svg,
.nav-row[aria-pressed="true"] svg {
color: var(--focus);
opacity: 1;
}
/* 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: var(--radius-control);
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: var(--radius-control);
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: var(--radius-control);
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: #0f5c3f; }
.diff-medium { color: #694800; }
.diff-hard { color: #a3261f; }
.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) }
/* ---------------------------------------------------------------------------
Mobile animation foundation (≤639px / coarse pointers).
Press feedback is CSS so every button, chip, card, tab and row gets the
same instant response without wiring each component. The springy release is
a spring-like cubic-bezier on transform only; reduced motion drops it.
--------------------------------------------------------------------------- */
@media (max-width: 639px), (pointer: coarse) {
.press,
.btn-primary,
.btn-secondary,
.btn-ghost,
.card-hover,
.nav-row,
.folder-pill,
.segmented > button,
.segmented > a,
.tabbar a,
.tabbar button,
summary {
transition:
transform 0.22s cubic-bezier(0.34, 1.56, 0.64, 1),
background-color 0.16s ease,
color 0.16s ease,
border-color 0.16s ease,
box-shadow 0.16s ease;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.press:active,
.btn-primary:active,
.btn-secondary:active,
.btn-ghost:active,
.card-hover:active,
.nav-row:active,
.folder-pill:active,
.segmented > button:active,
.segmented > a:active,
.tabbar a:active,
.tabbar button:active {
transform: scale(0.96);
}
/* UI chrome is not selectable; content text stays selectable. */
.btn-primary,
.btn-secondary,
.btn-ghost,
.nav-row,
.folder-pill,
.segmented,
.tabbar,
.study-chrome,
summary,
.press {
-webkit-user-select: none;
user-select: none;
-webkit-touch-callout: none;
}
}
@media (prefers-reduced-motion: reduce) {
.press:active,
.btn-primary:active,
.btn-secondary:active,
.btn-ghost:active,
.card-hover:active,
.nav-row:active,
.folder-pill:active,
.segmented > button:active,
.segmented > a:active,
.tabbar a:active,
.tabbar button:active {
transform: none;
}
}
/* Sliding segmented highlight: when a control carries the shared layout
pill, the item itself goes transparent so the pill can glide between
items. (Unlayered, like the rules above, so it wins over them.) */
.segmented > .seg-slide[aria-current="page"],
.segmented > .seg-slide[aria-pressed="true"],
.segmented > .seg-slide[aria-checked="true"] {
background-image: none;
box-shadow: none;
}
.segmented > .seg-slide[aria-current="page"] > .seg-label,
.segmented > .seg-slide[aria-pressed="true"] > .seg-label,
.segmented > .seg-slide[aria-checked="true"] > .seg-label {
color: #06111f;
}
.dark .segmented > .seg-slide > .seg-label {
color: var(--color-frost-300);
}
.dark .segmented > .seg-slide[aria-current="page"] > .seg-label,
.dark .segmented > .seg-slide[aria-pressed="true"] > .seg-label,
.dark .segmented > .seg-slide[aria-checked="true"] > .seg-label {
color: #06111f;
}
/* Shared sliding pill for non-segmented pickers (difficulty, size). */
.slide-pill {
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
}
/* App launch: one quick fade of the shell, no layout shift. */
@keyframes app-in {
from { opacity: 0 }
to { opacity: 1 }
}
.app-shell-in { animation: app-in 0.28s ease-out both; }
@media (prefers-reduced-motion: reduce) {
.app-shell-in { animation: none; }
}
/* Phones: a quick fade when the study feed takes over (the generation
hand-off), so the result doesn't hard-cut in. */
@media (max-width: 639px) {
.fade-in-mobile { animation: app-in 0.28s ease-out both; }
}
@media (max-width: 639px) and (prefers-reduced-motion: reduce) {
.fade-in-mobile { animation: 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;
}
}
/* ---------------------------------------------------------------------------
Reduced motion (Settings toggle, default off).
The class mirrors `prefers-reduced-motion: reduce`: the scene parks and
every CSS animation/transition collapses to nothing. framer-motion gets
told separately (components/MotionPreference).
--------------------------------------------------------------------------- */
html.reduce-motion .ice-blob,
html.reduce-motion .ice-orb,
html.reduce-motion .ice-bubble {
animation: none !important;
will-change: auto;
}
html.reduce-motion.aero .ice-bubble {
opacity: 0.55;
transform: translate3d(0, -60vh, 0);
}
html.reduce-motion .ice-orb-1 { top: 14%; left: 12%; }
html.reduce-motion .ice-orb-2 { top: 58%; left: 4%; }
html.reduce-motion .ice-orb-3 { top: auto; bottom: -70px; right: 10%; }
html.reduce-motion .ice-orb-4 { top: 24%; right: 28%; }
html.reduce-motion .ice-orb-5 { top: auto; bottom: 16%; left: 38%; }
html.reduce-motion *,
html.reduce-motion *::before,
html.reduce-motion *::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}