diff --git a/README.md b/README.md
index 7cb72e6..eca681d 100644
--- a/README.md
+++ b/README.md
@@ -1423,6 +1423,39 @@ Things I chose while building, with the reasoning:
(measured: 268..1280 at a 1280 window) — a flat pane floating 40px inside the scene read as
"cut off next to the sidebar".
+91. **The chat chrome floats: one gutter, one radius scale, one glass recipe.** The header and
+ the composer were full-bleed bands inside the main pane — flush to the viewport's top, right
+ and bottom edges, with the composer's bottom corners cropped off-screen — while the rail
+ floated with a 12px margin. `--layout-gutter` (12px, the rail's own inset) now spaces the
+ rail, the header, the empty-state card and the composer identically on all four sides, and
+ the radius scale (`--radius-panel` 24px, `--radius-control` 14px, `--radius-chip` 12px,
+ `--radius-pill`) replaced the ad-hoc values across the shared classes (`.card`, `.glass-panel`,
+ `.sidebar-surface`, buttons, nav rows, `.segmented`, chips), 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 of decision 87 are gone.
+
+ In 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: the rail top→bottom and the card/composer
+ left→right stay within ~10 RGB units (the old rail ran clear blue into a muddy green smear,
+ and the composer picked up the river's teal). Chat-specific fixes: **send** is a filled
+ primary tile shaped exactly like attach (dimmed while the input is empty — it used to be a
+ pale outline that read as disabled), the suggestion/context **chips** use the shared `.chip`
+ token (40px target, 44px on phones, visible rim, full-contrast text, the raw model id in the
+ tooltip), the AI note reads at 12px `--text-muted` (`.ai-disclaimer`), the sidebar's selected
+ row carries the accent border + accent icon (it looked like one more tinted action button),
+ the **theme toggle** is back in Aero — the moon flips to Dark, the Settings picker brings Aero
+ back — and the empty state is centered exactly between the header and the composer (the
+ trailing spacer only exists under a conversation now). Three fixes live outside CSS: the
+ wallpaper's **HUD-circle decoration** in the top-left corner (it peeked through the 12px
+ gutters on wide windows and read as a glitch) was healed out of `background.webp` and
+ `background-blur.webp`; the orbs and bubbles ride one masked `.ice-floats` layer that fades
+ them out under the chrome bands and the rail's strip, so none is sliced by a panel edge (still
+ parked under reduced motion); and the panel shadows/highlights come from the shared glass
+ tokens. The chat suite pins the geometry (12px gutter on all sides, 24px corners, the even
+ wash on all four panels, send-vs-attach shape, chips/input radii) and mobile pins the phone
+ chrome plus the Aero toggle.
+
90. **Copy pass on the shell (`lib/i18n-dict.ts`).** A user-authored wording pass: the library heading
is now "Your freezer" (matching the Czech "Tvůj mrazák"), the key gate's subtitle speaks about
grades ("První krok k dobrým známkám"), and several Czech strings read more naturally ("Schovat /
diff --git a/app/globals.css b/app/globals.css
index b5fbdf6..7d348ea 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -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;
diff --git a/components/AiDisclaimer.tsx b/components/AiDisclaimer.tsx
index c4fd869..5019274 100644
--- a/components/AiDisclaimer.tsx
+++ b/components/AiDisclaimer.tsx
@@ -17,8 +17,10 @@ export function AiDisclaimer({ className, plain = false }: { className?: string;
return (
// hint-line: Aero gives this its own glass chip (it sits on the wallpaper).
// Where the note already sits on a glass panel (the chat composer), `plain`
- // keeps it plain centered text like the other themes.
-
+ // keeps it plain centered text like the other themes. The size and colour
+ // come from the shared .ai-disclaimer token, so it reads the same (and
+ // clears AA) in every theme.
+
{t("common.aiDisclaimer")}
);
diff --git a/components/IceBackdrop.tsx b/components/IceBackdrop.tsx
index c87a0c7..f53d910 100644
--- a/components/IceBackdrop.tsx
+++ b/components/IceBackdrop.tsx
@@ -26,31 +26,33 @@ export function IceBackdrop() {
-
-
-
-
-
- {/* Aero-only bubbles: display:none unless the aero theme is on, so they
- cost nothing in the other themes (see .ice-bubble in globals.css). */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ {/* Aero-only bubbles: display:none unless the aero theme is on, so they
+ cost nothing in the other themes (see .ice-bubble in globals.css). */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx
index 7c86f14..69ed9b7 100644
--- a/components/Sidebar.tsx
+++ b/components/Sidebar.tsx
@@ -285,31 +285,30 @@ function NavContent({ collapsed = false }: { collapsed?: boolean }) {
{!collapsed ? {t("common.settings")} : null}
- {/* Frutiger Aero has no light/dark flip (see the theme picker in
- Settings), so this row goes away while it is on. */}
- {theme === "aero" ? null : (
-
- )}
+ {/* Light/dark flip — in Aero the moon jumps to Dark (the picker in
+ Settings brings Aero back). The row is shared by all three themes
+ now, so the rail always carries a theme control. */}
+
);
diff --git a/components/ThemeToggle.tsx b/components/ThemeToggle.tsx
index ff800d1..2fa290d 100644
--- a/components/ThemeToggle.tsx
+++ b/components/ThemeToggle.tsx
@@ -7,18 +7,17 @@ import { useTheme } from "@/lib/theme";
import { cn } from "@/lib/utils";
/**
- * The light/dark flip. Frutiger Aero has no such switch (the theme picker in
- * Settings owns it), so this renders nothing while aero is on — the state
- * comes from the shared store in lib/theme, so a change in Settings hides
- * this immediately.
+ * The light/dark flip. Frutiger Aero keeps the same control in the same
+ * spot: it has no dark variant of its own, so from Aero the moon flips to
+ * Dark (the theme picker in Settings is how Aero comes back) — the toggle
+ * used to disappear there, which left the chat header and the rail with no
+ * theme control at all.
*/
export function ThemeToggle({ className, plain = false }: { className?: string; plain?: boolean }) {
const { theme, toggle } = useTheme();
const { t } = useI18n();
const dark = theme === "dark";
- if (theme === "aero") return null;
-
return (