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.
This commit is contained in:
Martin Dvořák 2026-10-14 21:32:50 +00:00
commit 786d6cf1ae
21 changed files with 562 additions and 290 deletions

View file

@ -100,6 +100,15 @@
--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
@ -112,8 +121,8 @@
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(255 255 255 / 0.75);
--nav-active-border: rgb(56 189 248 / 0.5);
--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);
@ -160,8 +169,8 @@
--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.14);
--nav-active-border: rgb(56 189 248 / 0.4);
--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);
@ -235,8 +244,8 @@
--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.85);
--nav-active-border: rgb(14 165 233 / 0.7);
--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);
@ -263,7 +272,7 @@
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=3");
background-image: url("/aero/background.webp?v=4");
background-size: cover;
background-position: center 42%;
background-repeat: no-repeat;
@ -417,7 +426,7 @@
--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.27);
--nav-active-bg: rgb(255 255 255 / 0.5);
}
/* Liquid glass — one recipe for every surface, so the rail, the cards, the
@ -467,7 +476,7 @@
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=3");
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;
@ -555,20 +564,15 @@
/* 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 (a stronger veil than the shared glass, so an
answer reads as its own bubble). */
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.7);
border-color: rgb(255 255 255 / 0.85);
background-image:
linear-gradient(
180deg,
rgb(255 255 255 / 0.6) 0%,
rgb(255 255 255 / 0.44) 46%,
rgb(255 255 255 / 0.4) 53%,
rgb(255 255 255 / 0.52) 100%
),
url("/aero/background-blur.webp?v=3");
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;
@ -576,9 +580,9 @@
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.7);
border-radius: 1.25rem;
border-bottom-left-radius: 0.6rem;
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;
}
@ -593,7 +597,7 @@
rgb(255 255 255 / 0.08) 53%,
rgb(255 255 255 / 0.13) 100%
),
url("/aero/background-blur.webp?v=3");
url("/aero/background-blur.webp?v=4");
background-size: 100% 100%, cover;
background-position: 0 0, center 42%;
background-attachment: scroll, fixed;
@ -633,6 +637,15 @@
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;
}
@ -665,7 +678,7 @@
rgb(255 255 255 / 0.32) 53%,
rgb(255 255 255 / 0.44) 100%
),
url("/aero/background-blur.webp?v=3");
url("/aero/background-blur.webp?v=4");
background-size: 100% 100%, cover;
background-position: 0 0, center 42%;
background-attachment: scroll, fixed;
@ -680,7 +693,7 @@
rgb(255 255 255 / 0.32) 53%,
rgb(255 255 255 / 0.44) 100%
),
url("/aero/background-blur.webp?v=3");
url("/aero/background-blur.webp?v=4");
background-size: 100% 100%, cover;
background-position: 0 0, center 42%;
background-attachment: scroll, fixed;
@ -689,85 +702,41 @@
color: var(--text);
}
/* Chat header: the same glass as the cards, a touch more fill — its title and
scope line sit directly over the sky. At lg its divider is the inset hairline
the desktop block below draws (::after), so it never hits the seam. */
.aero .chat-header {
background-color: rgb(232 248 255 / 0.4);
border-bottom: 1px solid rgb(255 255 255 / 0.6);
}
.aero .chat-header::after {
background: rgb(255 255 255 / 0.6);
/* 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 composer: one flat frosted panel for the input, the chips and the
note — instead of each of them floating on the wallpaper. */
.aero .chat-composer {
background-color: rgb(232 248 255 / 0.55);
border-top: 1px solid rgb(255 255 255 / 0.65);
box-shadow: 0 -16px 36px -26px rgb(2 62 110 / 0.5);
}
.aero .chat-composer::after {
background: rgb(255 255 255 / 0.65);
}
/* Desktop: the chat chrome ends at the rail with a rounded corner. The header
and the composer are full-bleed bands inside the main pane, which starts
right at the sidebar; a straight vertical edge there read as a hard cut (the
12px gutter showed the raw scene and the bands' fill/blur made it a color
step at the seam). Both bands now stop at the seam with the rail card's own
1.5rem radius on their left end, so the ending reads as deliberate, and their
hairlines span the content box instead of touching the seam. Phones keep the
plain full-width borders. */
@media (min-width: 1024px) {
.chat-header {
border-bottom: 0;
border-start-start-radius: 1.5rem;
border-end-start-radius: 1.5rem;
}
.aero .chat-header {
border-bottom: 0;
}
.chat-header::after {
content: "";
position: absolute;
left: 1.5rem;
right: 1rem;
bottom: 0;
height: 1px;
background: var(--glass-border);
}
.chat-composer,
.chat-composer-band {
border-start-start-radius: 1.5rem;
border-end-start-radius: 1.5rem;
}
.chat-composer-band {
border-top: 0;
}
.aero .chat-composer {
border-top: 0;
}
.chat-composer::after {
content: "";
position: absolute;
left: 2.5rem;
right: 2.5rem;
top: 0;
height: 1px;
background: var(--glass-border);
}
}
/* The chat welcome (subtitle + suggestions): the same frosted glass as the
cards (the shared recipe above), stretched flush with the header and the
composer — a panel floating 40px inside the scene looked cut off. */
/* 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;
border-radius: 1.5rem;
border: 1px solid rgb(255 255 255 / 0.5);
background-color: rgb(255 255 255 / 0.32);
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 {
@ -776,6 +745,24 @@
}
}
/* 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;
@ -785,25 +772,10 @@
background-color: rgb(255 255 255 / 0.32);
}
/* The rail's lower half sits over the grass; weight the glass a little below
the middle so the bottom rows keep the same contrast as the top. */
.aero .sidebar-surface {
background-image:
linear-gradient(180deg, rgb(255 255 255 / 0) 42%, rgb(255 255 255 / 0.22) 100%),
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=3");
background-size: 100% 100%, 100% 100%, 100% 100%, cover;
background-position: 0 0, 0 0, 0 0, center 42%;
background-attachment: scroll, scroll, scroll, fixed;
background-repeat: no-repeat, no-repeat, no-repeat, no-repeat;
}
/* 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
@ -976,6 +948,90 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
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;
@ -1104,7 +1160,7 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
.glass-panel {
border: 1px solid var(--glass-border);
border-radius: 1.5rem;
border-radius: var(--radius-panel);
}
/* Nested glass: slightly more opaque than its parent, no extra blur. */
@ -1139,13 +1195,13 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
}
.card {
border-radius: 1.5rem; /* 24px */
border-radius: var(--radius-panel); /* 24px */
border: 1px solid var(--glass-border);
}
.gate-card {
border: 1px solid var(--glass-border);
border-radius: 1.75rem; /* 28px */
border-radius: var(--radius-panel);
}
/* Blur is expensive: halve it on phones (floating glass only). */
@ -1188,7 +1244,7 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
/* Inset grouped list ---------------------------------------------------- */
.list-group {
border-radius: 1rem;
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);
@ -1198,19 +1254,53 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
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: 0.95rem;
border-radius: var(--radius-control);
@apply backdrop-blur-sm;
background: var(--surface-sunken);
border: 1px solid var(--hairline);
}
.segmented > button {
flex: 1;
border-radius: 0.75rem;
border-radius: var(--radius-chip);
padding: 0.42rem 0.5rem;
font-size: 0.875rem;
font-weight: 600;
@ -1239,7 +1329,7 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
justify-content: center;
gap: 0.375rem;
min-height: 2.75rem;
border-radius: 0.75rem;
border-radius: var(--radius-chip);
padding: 0.42rem 0.5rem;
font-size: 0.875rem;
font-weight: 600;
@ -1301,7 +1391,7 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
/* Sidebar surface: one tall inset glass panel. */
.sidebar-surface {
border: 1px solid var(--glass-border);
border-radius: 1.5rem;
border-radius: var(--radius-panel);
}
/* Sidebar nav rows */
@ -1310,7 +1400,7 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
align-items: center;
gap: 0.625rem;
width: 100%;
border-radius: 0.625rem;
border-radius: var(--radius-chip);
padding: 0.5rem 0.625rem;
font-size: 13.5px;
font-weight: 500;
@ -1330,7 +1420,7 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
align-items: center;
gap: 0.625rem;
width: 100%;
border-radius: 0.75rem;
border-radius: var(--radius-chip);
padding: 0.5rem 0.625rem;
font-size: 13.5px;
font-weight: 600;
@ -1347,7 +1437,15 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
.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);
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
@ -1358,7 +1456,7 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
align-items: center;
justify-content: center;
gap: 0.5rem;
border-radius: 14px;
border-radius: var(--radius-control);
background-image: linear-gradient(135deg, #38bdf8 0%, #60a5fa 100%);
color: #06111f;
font-weight: 600;
@ -1389,7 +1487,7 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
align-items: center;
justify-content: center;
gap: 0.5rem;
border-radius: 14px;
border-radius: var(--radius-control);
background: var(--surface-strong);
color: var(--text);
border: 1px solid var(--glass-border);
@ -1407,7 +1505,7 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
align-items: center;
justify-content: center;
gap: 0.5rem;
border-radius: 14px;
border-radius: var(--radius-control);
background: transparent;
color: var(--text-muted);
font-weight: 500;