+ );
+}
diff --git a/frontend/src/components/MosaicBackdrop.tsx b/frontend/src/components/MosaicBackdrop.tsx
new file mode 100644
index 0000000..05b8c1e
--- /dev/null
+++ b/frontend/src/components/MosaicBackdrop.tsx
@@ -0,0 +1,58 @@
+import { useEffect, useMemo, useState } from "react";
+
+/**
+ * DEV-ONLY, OPT-IN backdrop: a heavily blurred + darkened + desaturated mosaic of the real
+ * thumbnails currently on screen, so dark "glass" surfaces have actual content to refract.
+ * Deliberately muted (see the --mosaic-* vars in index.css) — a soft texture, never a collage.
+ *
+ * Thumbnails are sampled straight from the rendered DOM (the feed/channel image tags), so it
+ * needs no data wiring and works on whatever page mounts it. Looks richest on the feed; on a
+ * thumbnail-less page it simply falls back to the plain background.
+ *
+ * Toggled by the GlassTuner. Not shipped to prod (the tuner that mounts it is localhost-gated).
+ */
+export default function MosaicBackdrop({ cols = 8, rows = 6 }: { cols?: number; rows?: number }) {
+ const [srcs, setSrcs] = useState([]);
+
+ // Snapshot the on-screen thumbnails once per mount (toggling the tuner off/on re-samples on a
+ // content-rich page). Looks richest on the feed; elsewhere it just falls back to the plain bg.
+ useEffect(() => {
+ const found = new Set();
+ document.querySelectorAll("img").forEach((img) => {
+ const s = img.currentSrc || img.src;
+ if (!s) return;
+ // YouTube thumbnails / channel avatars — skip our own tiny UI icons/data URIs.
+ if (/ytimg\.com|ggpht\.com|googleusercontent\.com/.test(s)) found.add(s);
+ });
+ setSrcs([...found]);
+ }, []);
+
+ useEffect(() => {
+ document.documentElement.dataset.mosaic = "1";
+ return () => {
+ delete document.documentElement.dataset.mosaic;
+ };
+ }, []);
+
+ const tiles = useMemo(() => {
+ const n = cols * rows;
+ if (srcs.length === 0) return [];
+ return Array.from({ length: n }, (_, i) => srcs[i % srcs.length]);
+ }, [srcs, cols, rows]);
+
+ return (
+
+
+ {tiles.map((src, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/frontend/src/index.css b/frontend/src/index.css
index d66cc3e..e735122 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -4,6 +4,29 @@
:root {
--font-scale: 1.06;
+
+ /* ===== Glass tunables — every value that materially drives the look lives here so it can be
+ tuned in ONE place (and live-driven by the dev GlassTuner during UAT). Defaults below equal
+ the historical hard-coded values, so nothing changes until a value is bumped. When the user
+ signs off on tuned values, bake the finals right here. ===== */
+ --glass-blur: 30px; /* backdrop blur radius (2026 sweet spot is ~8–16px; 30 is heavy) */
+ --glass-saturate: 1.8; /* backdrop saturation */
+ --glass-dark-bright: 1.4; /* extra brightness lift applied to .glass/.glass-menu in dark mode only */
+ --glass-surface-alpha: 78%; /* .glass panel translucency (higher = more solid = "adaptive") */
+ --glass-card-alpha: 84%; /* .glass-card translucency */
+ --glass-menu-alpha: 92%; /* .glass-menu translucency */
+ --glass-border-alpha: 78%; /* .glass border opacity */
+ --glass-inset: 16%; /* top specular highlight strength (#fff N%) */
+ --glass-edge: 0%; /* extra edge-light mixed into the TOP border (#fff N%) — 0 = off */
+ --glass-scrim: 0%; /* under-content scrim: darkens surfaces toward --bg for text contrast — 0 = off */
+ --ambient: 1; /* multiplier on the ambient accent backdrop pools */
+
+ /* Optional muted thumbnail-mosaic backdrop (off unless mounts). Conservative
+ defaults: heavy blur + low saturation + darkened so it reads as subtle texture, never a rainbow. */
+ --mosaic-blur: 64px;
+ --mosaic-sat: 0.7;
+ --mosaic-bright: 0.5;
+ --mosaic-opacity: 0.5;
}
/* Make native controls (date picker, scrollbars) follow the theme. */
@@ -26,9 +49,9 @@ body {
subtle but a touch richer (three soft pools, incl. a bottom glow) now that the nav,
header and dialogs are all frosted glass and refract it. */
background:
- radial-gradient(1100px 620px at 10% -10%, color-mix(in srgb, var(--accent) 20%, transparent), transparent 60%),
- radial-gradient(1000px 720px at 115% 10%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 55%),
- radial-gradient(900px 640px at 50% 118%, color-mix(in srgb, var(--accent) 11%, transparent), transparent 60%),
+ radial-gradient(1100px 620px at 10% -10%, color-mix(in srgb, var(--accent) calc(20% * var(--ambient)), transparent), transparent 60%),
+ radial-gradient(1000px 720px at 115% 10%, color-mix(in srgb, var(--accent) calc(14% * var(--ambient)), transparent), transparent 55%),
+ radial-gradient(900px 640px at 50% 118%, color-mix(in srgb, var(--accent) calc(11% * var(--ambient)), transparent), transparent 60%),
var(--bg);
background-attachment: fixed;
}
@@ -37,34 +60,44 @@ body {
.glass {
/* Frosted overlay surface: translucent enough that the blurred backdrop shows
through (the glassy look), opaque enough that the background isn't distracting.
- The strong blur turns whatever is behind into soft colour, not a sharp image. */
- background: color-mix(in srgb, var(--surface) 78%, transparent);
- backdrop-filter: blur(30px) saturate(1.8);
- -webkit-backdrop-filter: blur(30px) saturate(1.8);
- border: 1px solid color-mix(in srgb, var(--border) 78%, transparent);
+ The strong blur turns whatever is behind into soft colour, not a sharp image.
+ Layer 1 (top) = the optional scrim; layer 2 = the translucent surface tint. */
+ background:
+ linear-gradient(color-mix(in srgb, var(--bg) var(--glass-scrim), transparent), color-mix(in srgb, var(--bg) var(--glass-scrim), transparent)),
+ color-mix(in srgb, var(--surface) var(--glass-surface-alpha), transparent);
+ backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
+ -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
+ border: 1px solid color-mix(in srgb, var(--border) var(--glass-border-alpha), transparent);
+ border-top-color: color-mix(in srgb, #fff var(--glass-edge), color-mix(in srgb, var(--border) var(--glass-border-alpha), transparent));
box-shadow:
- inset 0 1px 0 color-mix(in srgb, #fff 16%, transparent),
+ inset 0 1px 0 color-mix(in srgb, #fff var(--glass-inset), transparent),
0 18px 44px -16px rgba(0, 0, 0, 0.6);
}
.glass-card {
/* No backdrop-filter here: cards render in bulk (feed grid) over a solid background
where blur adds almost nothing visually but is GPU-expensive and triggers reflow.
Translucency + border + depth keep the look; blur is reserved for .glass overlays. */
- background: color-mix(in srgb, var(--card) 84%, transparent);
+ background:
+ linear-gradient(color-mix(in srgb, var(--bg) var(--glass-scrim), transparent), color-mix(in srgb, var(--bg) var(--glass-scrim), transparent)),
+ color-mix(in srgb, var(--card) var(--glass-card-alpha), transparent);
border: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
+ border-top-color: color-mix(in srgb, #fff var(--glass-edge), color-mix(in srgb, var(--border) 65%, transparent));
box-shadow:
- inset 0 1px 0 color-mix(in srgb, #fff 8%, transparent),
+ inset 0 1px 0 color-mix(in srgb, #fff calc(var(--glass-inset) / 2), transparent),
0 8px 22px -14px rgba(0, 0, 0, 0.45);
}
.glass-menu {
/* Floating menus/popovers hover over undimmed content (no backdrop scrim like dialogs),
so they must be near-opaque to stay readable — only a hint of translucency + the blur. */
- background: color-mix(in srgb, var(--surface) 92%, transparent);
- backdrop-filter: blur(30px) saturate(1.8);
- -webkit-backdrop-filter: blur(30px) saturate(1.8);
+ background:
+ linear-gradient(color-mix(in srgb, var(--bg) var(--glass-scrim), transparent), color-mix(in srgb, var(--bg) var(--glass-scrim), transparent)),
+ color-mix(in srgb, var(--surface) var(--glass-menu-alpha), transparent);
+ backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
+ -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
+ border-top-color: color-mix(in srgb, #fff var(--glass-edge), color-mix(in srgb, var(--border) 80%, transparent));
box-shadow:
- inset 0 1px 0 color-mix(in srgb, #fff 14%, transparent),
+ inset 0 1px 0 color-mix(in srgb, #fff calc(var(--glass-inset) - 2%), transparent),
0 18px 44px -16px rgba(0, 0, 0, 0.6);
}
.glass-hover:hover {
@@ -80,9 +113,9 @@ body {
(The full "videos behind glass everywhere" effect is Phase 2 / end-of-project polish.) */
html[data-theme="dark"] body {
background:
- radial-gradient(1100px 620px at 10% -10%, color-mix(in srgb, var(--accent) 34%, transparent), transparent 60%),
- radial-gradient(1000px 720px at 115% 10%, color-mix(in srgb, var(--accent) 24%, transparent), transparent 55%),
- radial-gradient(900px 640px at 50% 118%, color-mix(in srgb, var(--accent) 18%, transparent), transparent 60%),
+ radial-gradient(1100px 620px at 10% -10%, color-mix(in srgb, var(--accent) calc(34% * var(--ambient)), transparent), transparent 60%),
+ radial-gradient(1000px 720px at 115% 10%, color-mix(in srgb, var(--accent) calc(24% * var(--ambient)), transparent), transparent 55%),
+ radial-gradient(900px 640px at 50% 118%, color-mix(in srgb, var(--accent) calc(18% * var(--ambient)), transparent), transparent 60%),
var(--bg);
background-attachment: fixed;
}
@@ -90,8 +123,8 @@ html[data-theme="dark"] body {
the frosted sheen shows even over dark UI (over colourful content it just looks richer). */
html[data-theme="dark"] .glass,
html[data-theme="dark"] .glass-menu {
- backdrop-filter: blur(30px) saturate(1.7) brightness(1.4);
- -webkit-backdrop-filter: blur(30px) saturate(1.7) brightness(1.4);
+ backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)) brightness(var(--glass-dark-bright));
+ -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)) brightness(var(--glass-dark-bright));
}
/* Toasts surface bottom-left (by the nav's notification bell), off the conventional
top-right; a brighter rim draws the eye there. Most needed in dark mode, where the
@@ -100,6 +133,37 @@ html[data-theme="dark"] .toast-card {
border-color: color-mix(in srgb, #fff 50%, transparent);
}
+/* ===== Optional muted thumbnail-mosaic backdrop (mounted by ) =====
+ Gives dark glass real content to refract. Kept conservative: heavy blur + low saturation +
+ darkened, so it reads as a soft texture behind everything, never a colourful collage. When it
+ is present, the ambient accent pools on are dimmed so the two don't fight. */
+.mosaic-backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: -1;
+ overflow: hidden;
+ pointer-events: none;
+}
+.mosaic-backdrop .mosaic-tiles {
+ position: absolute;
+ inset: -10%;
+ display: grid;
+ gap: 0;
+ filter: blur(var(--mosaic-blur)) saturate(var(--mosaic-sat)) brightness(var(--mosaic-bright));
+ opacity: var(--mosaic-opacity);
+}
+.mosaic-backdrop .mosaic-tiles > span {
+ display: block;
+ background-size: cover;
+ background-position: center;
+}
+html[data-mosaic="1"] body {
+ background: var(--bg);
+}
+html[data-perf="1"] .mosaic-backdrop {
+ display: none;
+}
+
/* Performance mode (Settings → Appearance) drops the expensive blur + soft shadows. */
html[data-perf="1"] .glass,
html[data-perf="1"] .glass-menu,