feat(glass): variable-drive the glass system + dev GlassTuner (Phase 0/0b)

Foundation for the app-wide glass-consistency epic:
- index.css: every look-driving value (blur/saturate/dark-brightness/panel-card-
  menu opacity/border/inset/edge/scrim/ambient + mosaic) is now a CSS custom
  property defaulting to the historical hard-coded value — one-place tunable, no
  visual change until a value is bumped.
- GlassTuner.tsx: dev-only (localhost-gated) always-on corner panel that live-
  drives those vars, with treatment presets (Adaptive/Gradient+/Edge+scrim),
  per-scheme palette editor, muted thumbnail-mosaic toggle, localStorage persist,
  and Copy-CSS export. Deleted once the tuned finals are baked in.
- MosaicBackdrop.tsx: opt-in muted (blur+darken+desaturate) mosaic of on-screen
  thumbnails so dark glass has real content to refract.
This commit is contained in:
npeter83 2026-07-12 19:14:40 +02:00
parent 59aa727715
commit 22cce807a0
4 changed files with 520 additions and 20 deletions

View file

@ -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 ~816px; 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 <MosaicBackdrop/> 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 <MosaicBackdrop/>) =====
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 <body> 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,