Liquid Glass UI Redesign — Design Document¶
Date: 2026-07-14
Status: Implemented — then extended/superseded in named areas (see the note below).
Scope: Every non-game, non-app surface of rmhstudios.com — the entire _site/
shell (all ~90 sidebar-layout pages, including every admin/backend page), the
shared primitive layer, and the in-scope top-level pages (login, legal, blog/news
readers, offline, security).
Companions: docs/design-language.md (current visual
system), docs/page-consistency.md (page recipes),
docs/misc/UI_REDESIGN.md (the previous, superseded
minimal-redesign vision).
Status (2026-07-21): IMPLEMENTED, then extended. This redesign shipped in full. Several areas were subsequently reworked or superseded by
docs/plans/2026-07-21-liquid-glass-v2-optics.md— “Liquid Glass v2 — True Refraction, Reflection & Liquid Motion”: the optics (turbulence → lens refraction + a border-box rim glint), the floating-shell layout restructure,LiquidTabssheets placed below heroes, the five-stop glass-clarity slider (replacing the reduce-transparency toggle), liquid morph/open transitions, device-tilt light, and glass-native user themes. Where the two docs disagree, the v2 doc is authoritative and the code is ground truth. This doc is retained for its rationale and history; its body is not rewritten.
For the implementing agent: read
docs/design-language.mdanddocs/page-consistency.mdfirst — this redesign evolves that system, it does not discard it. The--site-*token contract,components/ui/primitives,PageLayout, i18n, SEO, and a11y rules all still apply. Work phase-by-phase (§12), keeppnpm exec tsc --noEmit/pnpm lintclean at every phase, runpnpm i18n:extractafter adding strings, and never editrouteTree.gen.ts.
0. Vision¶
Today, Liquid Glass is the site’s default theme but not its design system. It
is implemented as a .style-liquid-glass class whose token overrides are propped
up by ~200 lines of fragile descendant-selector patches in app/globals.css
(frosting anything classed .bg-site-surface, special-casing the pricing panel,
the library tiles, autofill, drawers…). Components know nothing about glass;
glass is sprayed onto them from the outside.
This redesign inverts that: glass becomes the material system of the site. Every surface a user touches outside of games and apps — the sidebar, every page header, every card, dialog, table, form, admin queue, the library shelves, the creator studio tabs, the shop — is rebuilt as physically-plausible layered glass that:
Looks realistic — thickness, edge refraction, a consistent top-lit specular model, depth-graded blur and shadow, micro-imperfections (§4).
Reacts — specular highlights track the pointer, panes flex on press, chrome condenses on scroll, tab indicators flow like liquid (§5).
Stays fast — a strict elevation budget decides which elements get true
backdrop-filterand which get cheap translucent fills; repeated list items never blur; refraction is rationed to hero/chrome elements (§6).
Themes survive as tints of the glass (Glass Dark, Glass Light, Graphite,
Sepia, Nocturne) over per-theme aurora canvases; high-contrast remains fully
opaque and blur-free, and OS/user “reduce transparency” collapses everything to
opaque surfaces (§3.4, §10).
1. Scope¶
1.1 In scope¶
The material/token layer —
app/globals.css,stores/themeStore.ts,lib/appearance.ts,components/Providers.tsx, the anti-FOUC script inapp/routes/__root.tsx.All shared primitives — every file in
components/ui/(§7), plus shared composites: sonner Toaster config,site/CommandPalette,site/NotificationsPopover,site/CookieConsent,site/KeyboardShortcuts, the feed modals mounted in_site.tsx(WelcomeModal,WhatsNewModal,FreeMonthModal,LanguageFirstRunModal),ComposeModal,ShareModal,ProfileEditModal,moderation/ReportDialog,errors/RouteErrorFallback,errors/NotFound,MiniPlayer.The
_siteshell —app/routes/_site.tsx,components/feed/LeftSidebar,MobileSidebarShell,MobileNav,MobileHeader,MobileMenuButton,PageLayout,AnimatedMain,RightSidebar,components/feed/feed.css.Every
_site/page (~90 URLs — full checklist in Appendix A), including all 16 admin/backend URLs under_site/admin/, with dedicated deep treatments for Library (§9.2), Creator Studio (§9.3), and Store/Shop/Storefront/Membership (§9.4).Top-level non-game pages —
/login,/offline,/security, the four legal pages (/terms,/privacy,/cookies,/copyright), and the full-screen article readers/blog/$slug,/news/$slug(§9.10).The
/liquid-glassshowcase — repurposed as the design-lab page for the new system (§9.11).
1.2 Phase-gated (included, but last and lighter-touch)¶
The bespoke marketing landings — /adaptive-intelligence, /rmh-capital,
/rmh-pmc, /optimization — carry their own brand systems (own fonts/CSS).
They adopt the glass chrome (nav, cards, footers) while keeping their brand
palettes. Scheduled as the final visual phase (§12, Phase 7) so the core site
never waits on them.
1.3 Out of scope (do not touch)¶
Games and full apps and their internals:
altair,cookgame,daily,dream-rift,forest-explorer,house-always-wins,kowloon-knockout,laundry-sort,lights-out,neon-driftway,rmh-farming-sim,rochester-offensive,slice-it,synapse-storm,temple-of-joy,velum2099,versecraft,void-breaker,rmhtube,rmhmusic,rmhtype,rmhstudy,rmhcode,rmhbox,studio,strategies, plus the player/reader/editor internals (v.$slug,v.new,builds_.$slug,user-builds.$slug,library.$slug,library.albums.$albumId),secret/*,discord/*,embed.post.$id. Their hub/browse pages inside_site/are in scope (e.g./library,/create,/study,/rmhladder); their full-screen internals are not.THEME_EXCLUDED_ROUTESstays as-is.Route structure, data loading, API routes, backend services.
The coin economy, Stripe, auth flows (visual skin only on their pages).
2. Current state (what exists, what carries forward)¶
Asset |
Where |
Fate |
|---|---|---|
|
|
Kept. Extended with |
|
|
Promoted to |
|
|
Deleted after components migrate to explicit glass classes (§3.3). This is the single biggest cleanup of the project. |
|
|
Replaced by |
|
|
Kept and expanded into the canonical glass primitive module (§7.1). |
|
|
Kept, re-tuned for edge-weighted refraction (§4.3). |
|
|
Kept and generalized to the new glass classes + a user-facing settings toggle (§10). |
Motion systems: |
— |
Kept. New glass interactions use |
7-theme catalog, accent presets, anti-FOUC script, ThemeGallery |
|
Restructured: themes become glass tints; |
Known patch points that must die with this redesign (grep targets):
.style-liquid-glass (globals.css), .vibe-glass (feed.css + 4 consumers),
the scoped <style> painting --site-bg in
components/membership/MembershipPanel.tsx (.pricing-root), and the
opaque---site-surface color-mix assumption in components/library/library.css
(.lib-book__3d).
3. Architecture: from theme patch to material system¶
3.1 Why the current approach can’t scale¶
The current liquid-glass theme frosts surfaces from the outside:
.style-liquid-glass :is(.bg-site-surface, …) { backdrop-filter: blur(16px) }.
Consequences:
Fragile: it keys on literal utility class names; any component that composes its background differently silently loses (or gains) frost. The
.pricing-rootand.lib-volumepatches exist because of this.Slow: every
bg-site-surfaceelement gets its ownbackdrop-filterlayer — a feed with 30 cards pays for 30 backdrop readbacks. Blur cost should be a design decision per component, not a side effect of a background class.Unownable: components can’t express “I am chrome” vs “I am a list row”; the containing-block hazards of
backdrop-filter(it re-anchorsposition: fixeddescendants) are handled by ad-hoc pseudo-element tricks documented only in CSS comments.
3.2 New token group: the glass material contract¶
Add to :root in app/globals.css (every theme re-tints these, exactly like
the existing --site-* groups). Names stay in the --site- namespace so the
theme contract remains one flat list:
:root {
/* Canvas — the scene behind the glass. Every theme MUST define one; glass
over a flat color reads as gray plastic. (Already exists for liquid-glass;
generalized to all themes.) */
--site-canvas: /* per-theme layered radial gradients + base linear */;
/* Tint plates */
--site-glass-tint: rgba(255, 255, 255, 0.10); /* resting fill */
--site-glass-tint-strong: rgba(255, 255, 255, 0.17); /* hover / raised */
--site-glass-ink: color-mix(in srgb, var(--site-bg) 45%, transparent);
/* recessed fills (inputs) */
/* Backdrop treatment, by elevation tier (§4.4) */
--site-glass-blur-pane: 16px;
--site-glass-blur-chrome: 24px;
--site-glass-blur-overlay: 28px;
--site-glass-saturate: 160%;
/* Lighting */
--site-glass-rim: rgba(255, 255, 255, 0.28); /* top specular edge */
--site-glass-rim-soft: rgba(255, 255, 255, 0.10); /* hairline all-round */
--site-glass-light: rgba(255, 255, 255, 0.14); /* pointer highlight */
/* Depth shadows (compose into --site-shadow / --site-shadow-sm as today) */
--site-glass-depth: 0 24px 64px rgba(2, 10, 28, 0.60);
--site-glass-depth-sm: 0 4px 16px rgba(2, 10, 28, 0.28);
}
The existing --site-shadow / --site-shadow-sm keep their current
liquid-glass composition (inset rim + drop shadow) and are derived from the
tokens above, so Card, Dialog, etc. continue to pick up rim lighting through
shadow-site/shadow-site-sm with zero component changes.
3.3 Glass component classes (the elevation system)¶
New @layer components block in globals.css (or a new app/glass.css
imported from it). These are explicit opt-ins placed on components — never
descendant-matched:
Class |
Tier |
Backdrop |
Use for |
Replaces |
|---|---|---|---|---|
|
L1 |
none |
Repeated content: feed cards, list rows, table rows, grid tiles, admin queue items. Translucent tint + rim via |
the implicit frosting of every |
|
L2 |
|
Singular panels: hero sections, composer, membership tier cards, settings sections, sidebars-within-pages. Budgeted (§6.1). |
ad-hoc |
|
L3 |
|
Persistent chrome: desktop sidebar, sticky page headers, mobile top bars, mobile dock. |
|
|
L4 |
|
Floating UI: dialog content, popovers, menus, command palette, toasts, tooltips. More opaque so content never ghosts through text. |
|
|
— |
|
Dialog/drawer backdrops. |
dialog overlay’s |
|
— |
none |
Recessed surfaces (inputs, search fields, wells): |
input |
|
modifier |
— |
Adds hover tint-raise, press flex, and the pointer specular layer; pairs with |
|
|
modifier |
adds |
Edge refraction for hero/chrome elements only — max 2 per page (§4.3, §6.1). |
|
Rules carried over from the current implementation (encode as comments on the classes, they are load-bearing):
Never put a backdrop tier class on an ancestor of a
position: fixedelement —backdrop-filtercreates a containing block. The desktop<aside>keeps the existing::beforepseudo-element pattern (blur lives on the pseudo, the aside itself stays filter-free soLeftSidebar’s non-portaled fixed user menu keeps the viewport as its containing block). Bake this into.glass-chrome--asidevariant.Full-screen fixed takeovers stay opaque (e.g. the admin MDX editor): keep an explicit
.glass-opaqueescape hatch (background: var(--site-bg)).The mobile push-drawer panel goes opaque while
data-drawer-active(otherwise the sidebar ghosts through during the slide) — keep the existing[data-drawer-active]rule, retargeted at the new classes.Every glass class ships
-webkit-backdrop-filteralongsidebackdrop-filter(Safari).
All primitives set data-slot already; new glass variants hook the same
attributes so future themes can restyle.
3.4 Themes become glass tints¶
SITE_STYLES (in stores/themeStore.ts) is restructured — same mechanism
(.style-* class on <html>, :root = default), new catalog:
id |
Label |
Canvas |
Glass |
|---|---|---|---|
|
Glass Dark |
The current liquid-glass aurora (deep-ocean navy + blue/violet/teal/pink radials) |
The current liquid-glass tokens, promoted from |
|
Glass Light |
Daylight canvas: near-white base with soft pastel sky radials (blue/lilac/mint at low alpha) |
White frost at higher opacity ( |
|
Graphite Glass |
Monochrome smoke gradients over near-black |
Neutral gray tint, silver rims, desaturated (saturate 120%). |
|
Sepia Glass |
Warm parchment gradients |
Warm white tint, amber accent, golden rim. |
|
Nocturne Glass |
Deep-navy nightscape, sky-blue aurora |
Cool blue-shifted tint + rims. |
|
High Contrast |
Flat black (unchanged) |
No glass at all: opaque surfaces, no blur, 2px borders, yellow accent — token values unchanged from today. A global rule neutralizes every glass class under |
The
liquid-glasstheme id is retired; the existing self-heal inProviders.tsx(unknown persisted style →DEFAULT_STYLE) migrates stored prefs, and the/api/preferences/appearancevalidation derives fromSITE_STYLESso no API change is needed.DEFAULT_STYLEbecomes"default".THEME_BGcontinues to hold the opaque base color per theme (anti-FOUC and overscroll); each theme also defines--site-canvas, painted onbodywithbackground-attachment: fixedexactly as liquid-glass does today (already verified to degrade gracefully on iOS).Accent presets are unchanged (14 presets overriding
--site-accent*). New nicety: glass focus rings and the pointer light multiply with the accent (color-mix(in srgb, var(--site-accent) 20%, var(--site-glass-light))) so an accent choice subtly warms the glass.components/settings/ThemeGallery.tsxalready readsvar(--site-canvas, var(--site-bg))for the liquid-glass swatch — with every theme defining--site-canvasthe gallery previews become uniform for free.
4. The glass material: realism spec¶
The reason most “glassmorphism” looks like fog on plastic is uniform treatment. Real glass has thickness, a light source, and imperfections. Every glass element in this system is built from the same layer stack, and every deviation below is what sells the realism.
4.1 Layer stack (bottom → top)¶
Backdrop refraction (optional,
.glass-refract, Chromium only) — the backdrop is displaced through#glass-distortionbefore blurring, so edges of shapes behind the glass bend. §4.3.Backdrop blur + saturation —
blur() saturate()per elevation tier. Saturation ≥ 150% is mandatory: real glass concentrates color, it never grays it out.Tint plate —
--site-glass-tint. This is the body of the glass.Specular rim (via
--site-shadow*insets) — the top inner edge catches the light:inset 0 1px 0 var(--site-glass-rim), plus a 0.5px all-round hairlineinset 0 0 0 0.5px var(--site-glass-rim-soft). On hover the rim brightens (glass turning toward the light).Depth shadow —
--site-glass-depth(-sm)drop shadow. Blur radius and tint opacity increase together with elevation — that co-variance is what the eye reads as height.Pointer specular (interactive elements) — a radial highlight that tracks the cursor (§5.1).
Content — always fully opaque and never distorted. Text and images sit on glass; only the backdrop refracts. (This is why
GlassEffectlayers content atz-30above the refraction/tint/rim layers — keep that structure.)Micro-noise (panes L2+ only) — a tiled 64×64 data-URI noise PNG at 2–3% opacity on the tint plate. Kills gradient banding on large panes and reads as material texture. One shared background-image, zero runtime cost.
4.2 Lighting model — one sun¶
All specular cues assume light from the top, slightly left, sitewide:
Rim: top inner edge brightest; sides at ~35% of top; bottom edge gets a dark inner hairline (
inset 0 -1px 0 rgba(0,0,0,0.12)) — glass bottoms occlude, they don’t glow.Recessed elements (
.glass-inset, §7.2) invert this: dark top inner shadow, faint light bottom inner edge — a hole in the glass instead of a slab on it.Drop shadows fall straight down (0 y-positive offsets only, as today).
The pointer light (§5.1) is the one exception — it’s your light, not the sun’s, and only appears on hover.
Never mix directions. A single inconsistent bottom-lit card breaks the whole scene.
4.3 Refraction (the “realistic” differentiator)¶
Current state: #glass-distortion (feTurbulence → feGaussianBlur →
feDisplacementMap, scale=200) displaces the whole backdrop uniformly. Real
glass refracts hardest at its edges (thickness gradient), and a scale-200
full-surface wobble reads as heat haze.
Changes:
Keep
GlassFiltermounted once in__root.tsx; reducescaleto ~80 for chrome usage.Edge-weight the refraction layer with a CSS mask so the center of the pane is optically clean and only a ~14px inner ring displaces:
.glass-refract::before {
/* the refraction layer (replaces .liquid-glass-refract usage) */
content: "";
position: absolute; inset: 0; z-index: 0;
border-radius: inherit;
pointer-events: none;
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
/* only the rim band refracts — center stays clean */
-webkit-mask: linear-gradient(#000 0 0) content-box exclude,
linear-gradient(#000 0 0);
mask: linear-gradient(#000 0 0) content-box exclude,
linear-gradient(#000 0 0);
padding: 14px;
}
@supports (backdrop-filter: url(#glass-distortion)) {
.glass-refract::before {
-webkit-backdrop-filter: url(#glass-distortion) blur(2px);
backdrop-filter: url(#glass-distortion) blur(2px);
}
}
Firefox/Safari (no
url()inbackdrop-filter): the@supportsgate keeps them on the plain edge blur — the mask still gives them an “edge thickness” cue for free.Ration: ≤ 2
.glass-refractelements per page (§6.1). Intended users: the desktop sidebar pane, page-hero panes (library hero, membership panel, login card), never list items or scroll containers.(Optional, off by default)
.glass-refract--prismadds a 1px chromatic fringe on the rim via two offset low-alpha red/blue inner hairlines — for the featured membership tier and the design-lab page only.
4.4 Elevation tiers (recap table)¶
Tier |
Class |
Blur |
Tint |
Shadow |
Examples |
|---|---|---|---|---|---|
L0 |
— (canvas) |
— |
— |
— |
|
L1 |
|
none |
|
|
feed cards, rows, tiles |
L2 |
|
16px |
tint (+noise) |
|
heroes, panels, tier cards |
L3 |
|
24px |
bg-mix 32% |
border-b hairline |
sidebar, sticky headers, dock |
L4 |
|
28px |
bg-mix 62% |
|
dialogs, menus, palette, toasts |
L1 having no blur is deliberate and is both the performance backbone (§6) and physically right: thin glass shelves over an already-frosted scene don’t re-blur it; the tint + rim reads as glass because the canvas provides the color variance underneath.
5. Reactivity spec¶
All timings come from lib/motion.ts (DURATION, EASE, SPRING); the
springy overshoot cases use the existing GLASS_EASE
(cubic-bezier(0.175, 0.885, 0.32, 2.2)) exported from
components/ui/liquid-glass.tsx (move it to lib/motion.ts as EASE.glass).
Everything in this section is suppressed by prefers-reduced-motion (via the
existing global CSS block + MotionConfig) and by .perf-lite (§6.4).
5.1 Pointer-tracked specular highlight¶
The signature “reactive” behavior: a soft radial light follows the cursor across interactive glass.
Implementation: one document-level
pointermovelistener (newhooks/useGlassLight.ts, mounted once inProviders.tsx), rAF-throttled. On each frame it findsevent.target.closest('[data-glass-light]')and writes--glass-px/--glass-py(percent coords within the element) onto that element only; clears onpointerleave. No React re-renders — CSS custom properties only.CSS:
.glass-interactive { position: relative; }
.glass-interactive::after {
content: ""; position: absolute; inset: 0; z-index: 1;
border-radius: inherit; pointer-events: none;
background: radial-gradient(140px circle at var(--glass-px, 50%) var(--glass-py, -30%),
var(--site-glass-light), transparent 70%);
opacity: 0;
transition: opacity 0.18s var(--ease-standard);
}
.glass-interactive:hover::after { opacity: 1; }
Gates: wrapped in
@media (hover: hover) and (pointer: fine)— touch devices never pay for it. The hook subscribes only when that media query matches, unsubscribes under.perf-lite.Where: nav pills in
LeftSidebar, cards (Card, build/album/listing tiles, admin dashboard cards), buttons (outline/secondary/ghostvariants), membership tier cards, shop items. Applied by addingglass-interactive+data-glass-lightin the primitive, not per page.
5.2 Press flex & hover raise¶
Hover: background steps
--site-glass-tint→--site-glass-tint-strong, rim brightens (--site-glass-rimat 1.3× alpha via a:hovershadow swap), depth shadow grows ~20%. This replacesLIFT_CARD’s translate with a material response — glass gets lighter, not airborne. (Keep a 1px translateY for cards only; chrome never moves.)Press:
transform: scale(0.985)+ tint momentarily to--site-surface-activeequivalent; release springs back withEASE.glass. Buttons keep their existingactive:opacityfeedback removed in favor of this (opacity flicker fights the material illusion).Implemented inside
Button,Card, and.glass-interactive— pages get it for free.
5.3 Scroll-reactive chrome (condensing headers)¶
iOS-style: chrome is airier at rest, condenses when content scrolls under it.
PageLayoutrenders a 1px sentinel<div>above the scroll content; anIntersectionObservertogglesdata-scrolledon the sticky header.CSS (on
.glass-chrome[data-scrolled]): background mix 32% → 55%, blur 16px → 24px, bottom hairline fades in, height easesh-18→h-15(transitioningheightis fine here — it’s a single sticky element, not a layout-thrash risk; under reduced motion it snaps).Same pattern for
MobileTopBar. The mobile dock (§8.3) condenses on downward scroll (existing scroll direction detection inBackToTopshows the pattern) — shrinks label row, keeps icons.
5.4 Liquid state transitions¶
Tab indicators (Creator Studio
cstudio-tabs, settings sections, RMHLadder sub-nav): the active-tab background becomes a framer-motionlayoutIdglass capsule that flows between tabs (springSPRING.snappy). Under reduced motion it jumps.Dialogs/popovers: enter with
modalContentvariant plus a one-time blur ramp (backdrop-filter transitions fromblur(0)are janky — instead fade the scrim in and scale the pre-blurred pane from 0.96; this is whatdialog.cssalready does, keep its keyframe approach and just re-skin).Toasts: slide-in from bottom-left as small L4 panes; success toasts get a brief rim flash in
--site-success.NavigationProgress: restyle as a light refraction streak — accent core with a white specular cap (
box-shadow: 0 0 8px var(--site-accent)stays).Celebrations:
useCelebrationalready reads--site-accent/--site-successfrom the DOM — no change needed; confetti over glass looks great as-is.
6. Performance & optimization spec¶
backdrop-filter forces a backdrop readback + filter pass per element per
frame region; SVG displacement filters are the most expensive variant. The
whole optimization story is rationing, enforced by the elevation system.
6.1 Hard budgets (acceptance criteria for every phase)¶
Budget |
Limit |
|---|---|
Elements with any |
≤ 8 (3 chrome: sidebar, header, mobile nav/dock + ≤ 5 panes/overlays) |
|
0 — L1 |
|
≤ 2, chrome/hero only, never inside scroll containers |
Blur radius |
≤ 28px (cost scales with radius × area) |
|
Never persistent; only set during an active transition, removed after |
Nested backdrop-filters (glass inside glass both blurring) |
0 — inner elements use |
New layout-triggering animations |
0 (transform/opacity/background only; the §5.3 header height ease is the one sanctioned exception) |
6.2 Structural optimizations¶
The L1/L2 split is the big win vs today: the current theme frosts every
bg-site-surfaceelement; the redesign frosts named panes only. A feed page goes from ~30 backdrop layers to ~4.content-visibility: auto+contain-intrinsic-sizeon below-fold page sections that are already discrete (library shelves, admin tables, settings groups) — glass or not, skipped rendering is the cheapest rendering.The aurora canvas stays a single fixed background on
body— one paint, shared by every blur sample. Do not add per-section decorative gradients behind glass; they multiply blur cost for no visual gain.Pointer light writes CSS vars on one element at a time, rAF-throttled; repaint is confined to that element’s composited layer. No React state, no per-card listeners (event delegation from one root listener).
Scroll reactions are class/attribute toggles from IntersectionObserver — no scroll-position reads in rAF loops.
Skeletons/shimmer stay transform-based (
animate-shimmertranslates a gradient, already reduced-motion-safe).
6.3 Engine matrix¶
Engine |
Blur/saturate |
|
|
Notes |
|---|---|---|---|---|
Chromium |
✅ |
✅ |
✅ (118+) |
Full experience. |
Safari/WebKit |
✅ (needs |
❌ → |
✅ (recent) |
Watch rounded-corner bleed on filtered layers; |
Firefox |
✅ |
❌ → same fallback |
❌ (as of writing) → the settings toggle (§10) covers it |
Slightly weaker |
6.4 Device tiering — .perf-lite¶
One-time heuristic in Providers.tsx (alongside the theme effect):
const lite = (navigator.deviceMemory ?? 8) <= 4
|| navigator.hardwareConcurrency <= 4;
document.documentElement.classList.toggle('perf-lite', lite);
Under html.perf-lite: .glass-refract disabled, pointer light disabled,
L2 panes drop to L1 fills (chrome + overlays keep blur — they’re few and they
carry the identity). This is CSS-only demotion; no component logic branches.
6.5 Measurement & regression guardrails¶
The existing Core Web Vitals RUM (
lib/rum.ts→/api/rum) is the before/after ledger. Targets: INP p75 < 200ms unchanged or better, LCP within ±5% of pre-redesign, CLS delta 0.Per-phase manual trace: Chrome DevTools Performance on
/(feed),/library,/store,/admin/users— no frame > 16ms during steady scroll on a mid-tier device profile (4× CPU throttle), no long task > 50ms attributable topointermove.pnpm buildbundle check: the redesign is ~99% CSS + one small hook; flag any JS bundle growth > 5KB gzipped.
8. Shell redesign¶
8.4 Page header (PageLayout.tsx)¶
Sticky header:
.vibe-glass→.glass-chrome+ scroll condensation (§5.3).h1unchanged (display font, tracking). Breadcrumbs/back arrow unchanged.headerRightactions render as glass icon capsules (IconButton inherits).headerExtrarows (filter bars, tab strips) sit inside the chrome pane so the header reads as one slab of glass.The center column keeps
border-r border-site-border(a hairline glass edge against the canvas) and all width constants fromlib/layout-width.ts— no width changes.RightSidebarsections:bg-site-surface … borderboxes → L1.glass-fillcards; the OnlineNow pill and TodayWidget become small glass capsules.
8.5 The canvas¶
bodypaints--site-canvasper theme (fixed attachment; iOS degradation accepted). The aurora is re-tuned per theme (§3.4 table)..page-rootenter animation,html.nav-pop, View Transitions: unchanged.
9. Page-by-page coverage¶
Global rule: pages should need almost no bespoke work. The primitives
(§7), shell (§8), and elevation classes (§3.3) do the heavy lifting; a page’s
“redesign” is mostly (a) swapping ad-hoc bg-site-surface border … divs to
Card/.glass-fill/.glass-pane, (b) deleting page-local styling that fights
the material, (c) the specific touches below. Appendix A is the full checklist;
this section gives per-area direction.
9.2 Library (/library) — deep treatment¶
Design stance: books and photos are content, not glass (§4.1.7). The furniture is glass — shelves, plinths, chrome. Reference: a museum vitrine.
Hero (
lib-hero): becomes the page’s L2.glass-pane+.glass-refract(the page’s hero refract slot). Eyebrow,h1, lede unchanged typographically. The stats<dl>(Volumes / Pages / Albums / Collections) becomes an etched glass plate: numbers in display font, labelsfont-mono text-xs uppercase tracking-widest text-site-text-dim, separated by vertical light hairlines — engraved into the hero pane, not boxed onto it.Search (
lib-search): large.glass-insetwell; live-filter behavior unchanged; the clear button is a tiny glass disc.Shelves (
lib__shelf, Curated / Community uploads): each shelf row gets a glass ledge — a 10px high.glass-fillstrip under the row of spines with a soft vertical gradient streak (linear-gradientpseudo) suggesting the covers’ reflection. Book spines (BookSpine,lib-book__3d) keep their 3D cover treatment; fix the tile properly:library.csscurrentlycolor-mixes from--site-surfaceassuming opacity — replace with--site-glass-tinttokens and delete the.style-liquid-glass .lib-volumepatch. Page-count badge → micro-glass Badge; hover = pointer light + rim brighten (no scale jump — books are heavy).LibraryAlbums/LibraryCollections/LibraryBlogRow: tiles are L1 fills with opaque imagery; collection headers get the etched-label treatment; the blog row’s horizontal scroller gets edge fade masks (mask-image: linear-gradient) so cards dissolve at the pane edge.Admin affordances (context menu, drag-reorder, edit/hide/curate/delete, reported flag): context menu →
.glass-overlay; drag state lifts the spine withshadow-site+ slight tilt; reported flag = danger-tinted rim on the tile.UploadModal/LibraryEditModalinherit the new Dialog.The admin migrate-to-S3 banner: warning-tinted L1 strip (rim in
--site-warning).Out of scope here: the reader (
library.$slug) and album viewer (library.albums.$albumId) — top-level, excluded.
9.3 Creator Studio (/create) — deep treatment¶
Header (
cstudio-head): “Make anything.” sits directly on canvas (no pane) — large display type over the aurora is a statement the glass frames.Tab bar (
cstudio-tabs): the marquee §5.4 implementation — a sticky.glass-chromestrip (it’s persistent chrome while scrolling the gallery) whose active tab is a liquid glass capsule morphing between the five tabs (Pages / Games / Apps / User Builds / AI Personas) withlayoutId+ spring. Keeps WAI-ARIArole="tablist"+ roving arrow-key nav +?tab=mirroring exactly as-is. Each tab keeps its lucide icon; icons refract a hint of accent when active.Galleries (
PagesTab,CuratedBuildsTab,UserBuildsTab,PersonasTab): tiles are L1 fills; thumbnails opaque; title/meta rows on a bottom in-card gradient scrim (not blur — repeated elements). Pointer light on tiles. Infinite-scroll cursors and search unchanged; skeletons inherit §7.2.RankedSummary(Games tab): standings strip as an L2 pane with etched rank numerals; medal positions get success/warning-tinted rims.Personas chat (
/personas/$id): chat bubbles — user bubbles accent glass, persona bubbles L1 fill; input row.glass-inset; typing indicator = three tiny glass beads.Legacy galleries (
/builds,/v,/personasindex,/user-builds) are thin/redirect shells into the Studio — verify they inherit and delete any local styling.user-builds/submit+manage: standard form treatment (§9.8 pattern).Consolidate the four CSS files this page imports (
creator-studio.css,storefront.css,builds.css,vibe.css) down to token-based classes as encountered; delete rules the glass classes obsolete.
9.4 Store / Shop / Storefront / Membership — deep treatment¶
/store (MembershipPanel + ShopColumn), /store/$userid (StorefrontColumn),
/shop, /pricing (legacy shells), plus /predictions economy styling.
MembershipPanel: the current scoped
<style>painting.pricing-rootwith raw--site-bgis deleted (with its globals.css patch). Tier cards become L2.glass-panes on the canvas. The featured/current tier gets the premium treatment:.glass-refract(this page’s hero slot) + the optional--prismchromatic rim (§4.3.5) + a slow 8s specular sweep across the rim (paused under reduced motion, static under.perf-lite). Price numerals in display font; feature lists separated by light hairlines; the CTA is the accent glass Button.currentTierstate = success-tinted rim + etched “Current plan” label.ShopColumn (coin/cosmetics catalog): item grid = L1 fills; rarity is rim color — common: default rim; rare: accent-tinted rim; epic+: warm (warning) tinted rim — glass color communicates value without breaking the material. Owned items: etched check medallion overlay + reduced tint. Coin-balance chip: glass capsule +
AnimatedCount, docked in the sticky header viaheaderRight. Purchase confirm → newConfirmDialog; success → toast +useCelebration(already theme-aware).showHeroshop hero (/shop): L2 pane with the coin iconography refracting the aurora;/shopand/pricingotherwise inherit/storecomponents — verify no local surface styling remains.StorefrontColumn (
/store/$userid): seller header = L2 pane (avatar, name, stats etched); item grid inherits ShopColumn treatment; the not-found state uses the standardNotFound./predictions: market cards L1; live-odds bars = glass tubes filled with accent liquid (Slider track treatment); resolved markets get success/danger rims; the admin resolution flow is on/admin/predictions(§9.9).
9.5 Messaging & notifications (/messages, /messages/$id, /groups, /groups/$id, /notifications)¶
Conversation list rows: L1 + unread = accent left rim edge; presence dots unchanged.
Thread view: bubbles per §9.3 personas pattern; the message input bar is a docked L3 chrome strip (sticky bottom, one blur element) with an
.glass-insetfield;ChatPanel/EmojiPickerPanel/ReactionMenu→ §7.3./notifications: rows L1, type icons in small etched medallions; mark-read sweeps the row tint out.
9.6 Profile & personal (/profile/$id, /u/$userid, /u/$userid/post/$postid, /progress, /achievements, /recap, /wrapped)¶
Profile header: banner image opaque; the identity bar overlapping it = L2 pane (avatar with rim ring, handle, follow Button accent glass); stat row etched.
Tab strips (Posts/Replies/Media…) = liquid capsule tabs.
/progress: XP/streak bars = glass tubes with accent liquid; milestone markers = glass beads./achievements: badges become cast-glass medallions — locked ones frosted at 40% tint with etched outlines; unlocked ones full tint + rim./recap,/wrapped: these are showpieces — allowed one.glass-refracthero pane each + stat cards as L2 panes; keep their celebratory motion withinlib/motion.tstokens.
9.8 Settings (/settings, /settings/privacy, /settings/security)¶
Section groups become L2 panes (few per page, budget fine) with etched section labels; individual rows divided by light hairlines.
Controls inherit §7.2 (
Switchbeads,Selectwells).ThemeGallery: swatches now render each theme’s
--site-canvas+ miniature glass card mock; preview-on-hover behavior unchanged.New control (this project adds it): “Reduce transparency” toggle (§10) in Appearance, next to theme/accent pickers. Strings via
t("settings-reduce-transparency", { defaultValue: "Reduce transparency" })etc., thenpnpm i18n:extract./settings/security:PasskeyManager/SessionManagerrows L1; danger zone (DeleteAccountPanel) = danger-rimmed pane.
9.9 Admin & backend (all 16 URLs under /admin) — full coverage¶
Admin pages already share PageLayout + card/table patterns, so they inherit
most of the redesign structurally — but they are the most density-sensitive
surfaces. Admin rules:
Readability over spectacle: admin uses L1 fills and chrome only — no refraction, no prism, minimal panes. Data density wins.
Tables: header row = sticky
.glass-chromestrip within the scroll container; body rows = L1 with hairline dividers (divide-site-border); hover = tint raise only (no pointer light in dense tables — too busy); numeric cellsfont-mono.Queues (
/admin/reports,/admin/security-reports,/admin/library-quota,/admin/predictions,/admin/rideshare): queue cards = L1 with status-tinted rims (pending: warning; resolved: success; escalated/dangerous: danger). Action buttons: approve = accent glass, destructive = danger glass +useConfirm.Dashboard (
/admin/index): the 13 link cards become L1.glass-interactivetiles with pointer light, lucide icon in an etched medallion, count badges as glass capsules (report count keepsuseAdminReviewCount).CRUD editors (
/admin/blog/new,/admin/blog/$slug/edit,/admin/albums/*,/admin/announcements): form fields per §7.2; the MDX editor’s full-screen takeover keeps.glass-opaque(rule §3.3.2 — it must hide the page beneath). Bulk-upload dropzones = dashed-rim inset wells./admin/users: the table pattern + role/state Badges; impersonation or destructive controls always behinduseConfirm(existing behavior, keep)./admin/audit: monospace log rows, L1, timestamp column dim; filter bar inheaderExtrainside the header chrome./admin/analytics,/admin/library-storage: stat tiles = L2 panes (few), charts/meters recolor via tokens; storage meters = glass tube bars.The
admin/route.tsxgate is untouched (auth logic out of scope).
9.10 Top-level in-scope pages¶
/login: the flagship first impression. Full-viewport aurora canvas; a centered L2.glass-pane+.glass-refractcard holding the logo, OAuth provider buttons (glass capsules with brand icons at full color — content, not chrome), passkey button, and legal footnote. Provider buttons stack keeps current behavior/order. Error states as danger-rim inline strips, not toasts.Legal pages (
/terms,/privacy,/cookies,/copyright): long-form reading on a single wide L2 pane (“document under glass”) with an etched header; a sticky glass mini-ToC onlg+. Typography unchanged./blog/$slug,/news/$slug(article readers): hero image opaque; the article column is one L2 pane; pull-quotes get light-hairline left rims; code blocks = inset wells; share row = glass capsules. (Their in-shell list pages are covered in §9.7.)/offline: single centered pane + etched medallion (wifi-off icon); works with zero network (no external assets — noise texture is a data URI, aurora is CSS)./security: document-under-glass like legal, plus the disclosure contact card as an L1 fill withCopyButton.
9.11 The design lab (/liquid-glass)¶
Repurposed from a demo into the living reference: swatches of all elevation tiers, the lighting model, pointer light, refraction on/off comparison, reduced-transparency preview toggle, and per-theme canvas previews. The Unsplash background is replaced by the real canvas (removes the external dependency). This page is where reviewers verify §4/§5 acceptance criteria.
10. Accessibility¶
Contrast floors on glass: text tokens must clear WCAG AA against the brightest region of each theme’s canvas as seen through the relevant tier. The current Glass Dark values already encode this (
--site-text-mutedat 0.82 alpha, documented in globals.css) — preserve that comment and apply the same audit to Glass Light and each curated tint. Acceptance: axe/manual checks on/,/library,/store,/admin/reportsper theme.high-contrastneutralization (one global rule):
.style-high-contrast :is(.glass-fill, .glass-pane, .glass-chrome,
.glass-overlay, .glass-inset) {
background: var(--site-surface) !important;
-webkit-backdrop-filter: none !important;
backdrop-filter: none !important;
}
.style-high-contrast .glass-refract::before { display: none; }
Reduce transparency — three triggers, one behavior:
@media (prefers-reduced-transparency: reduce),html.reduce-transparency(the new user setting, persisted like the theme and synced via/api/preferences/appearance), andhtml.perf-lite(blur demotion only, keeps translucency). The media-query fallback block that exists today for.style-liquid-glassis generalized to the glass classes: opaque surfaces in the same hues, zero blur. Firefox users (no media query support) get the manual setting.Reduced motion: already comprehensively handled (global CSS neutralize +
MotionConfig); new behaviors comply by construction: pointer light is opacity-gated (itstransitioncollapses to 0.01ms → effectively instant, acceptable), liquid tabs jump, specular sweeps pause, header condensation snaps, flashcard flip crossfades.Focus: the global
:focus-visibleaccent outline stays the single source of truth. Audit: outline contrast ≥ 3:1 against glass fills per theme (the accent tokens already vary per theme; Glass Light may need a darker focus accent — test).Forced colors / Windows High Contrast: glass backgrounds go transparent under
forced-colors: active; addborder: 1px solidstructural borders on.glass-*so panes keep their shape when the UA strips backgrounds.RTL: nothing in the glass system is directional except the lighting model’s “slightly left” bias — which does not flip (physical light, not UI direction).
.rtl-flipusage unchanged.jsx-a11y warn count: no new warnings (CI-checked).
11. Theme runtime migration details¶
Ordered, concrete (all in Phase 1):
app/globals.css: move the.style-liquid-glasstoken block into:root(replacing the old Dark values); write new token blocks forlight/graphite/sepia/nocturneas glass tints (§3.4) and add--site-canvasto each; leavehigh-contrastvalues untouched; add the--site-glass-*group (§3.2) to:root+ per-theme overrides; add the glass component classes (§3.3); generalize the reduced-transparency block; add the high-contrast neutralization; delete the.style-liquid-glassscoped element rules once §7/§8 consumers are migrated.stores/themeStore.ts: newSITE_STYLES(6 entries, labels per §3.4),DEFAULT_STYLE = "default",THEME_BGvalues updated (default→#0d1b2e,light→ its new canvas base, …).components/Providers.tsx: add"liquid-glass"to the legacy-id self-heal list so persisted prefs migrate silently; add the.perf-liteheuristic (§6.4) and thereduce-transparencyclass application; sonnertoastOptionsre-skin (§7.3).app/routes/__root.tsxinlinethemeScript: no structural change (it derives fromTHEME_BG); verify the pre-hydration background matches the new values; apply persistedreduce-transparencypre-paint alongside the style class (same pattern as accent).lib/appearance.ts: accent presets unchanged; add the accent-tinted--site-glass-lightderivation toapplyAccent(one extra property)./api/preferences/appearance: validation derives fromSITE_STYLES— confirm thereduce-transparencyboolean is added to its zod schema and Prisma prefs shape (tiny migration).components/settings/ThemeGallery.tsx: renders from the new catalog; add the reduce-transparency toggle beside it.Docs: rewrite
docs/design-language.md§1–2 and §5–8 to describe the glass contract (do this in the final phase; this design doc is the spec until then), updatepage-consistency.mdchecklist items (.vibe-glass→.glass-chrome,pb-16→pb-24, glass-class dos/don’ts), touchcomponents/CLAUDE.mdandapp/CLAUDE.mdconventions lists.
12. Implementation phases¶
Each phase ends green: pnpm exec tsc --noEmit, pnpm lint (no new
warnings), pnpm exec vitest run, pnpm build, manual check of the §6.1
budgets on the phase’s pages, in Glass Dark + Glass Light + High Contrast +
reduced-motion + reduced-transparency.
Phase |
Contents |
Key files |
Exit criteria |
|---|---|---|---|
0. Foundations |
Glass tokens + component classes + noise asset; |
|
New classes exist + design lab demonstrates all tiers; zero visual change elsewhere |
1. Theme migration |
§11 items 1–7: tokens to |
|
All 6 themes render; |
2. Primitives |
Every |
|
Primitive gallery on design lab matches spec; budgets hold on a modal-open feed page |
3. Shell |
Sidebar, drawer, floating dock, PageLayout scroll-condensation, RightSidebar; delete |
|
Shell is glass on every |
4. Feed & social + messaging + profile |
§9.1, §9.5, §9.6 |
|
Feed scroll trace clean (no per-card blur); thread + DMs verified |
5. Library, Studio, Store |
§9.2, §9.3, §9.4 — the three deep treatments; delete |
|
The three flagship pages match spec; refract budget ≤2/page; rarity rims + tier prism verified |
6. Hubs + settings + admin |
§9.7, §9.8, §9.9 — every remaining |
hub routes + components, |
Appendix A checklist fully ticked for |
7. Top-level + marketing |
§9.10 login/legal/readers/offline/security; §1.2 marketing landings (glass chrome, brand palettes kept) |
|
Login showcase approved; readers AA-clean; marketing pages keep brand identity |
8. Cleanup & docs |
Delete |
|
|
Phases 4–7 are independently shippable behind normal review; 0–3 land in order. Do not start deleting old CSS (phase 8 items) early — the descendant patches keep unmigrated pages presentable during the transition.
13. Verification & QA matrix¶
Per phase, and in full before phase 8 closes:
Browsers: Chromium (full), Safari macOS + iOS (prefix + fallback paths, fixed-attachment degradation, rounded-corner filter bleed), Firefox (no-displacement fallback, manual reduce-transparency toggle).
Themes: all 6 — with special attention to Glass Light text contrast and High Contrast’s total glass neutralization.
Preferences: reduced motion, reduced transparency (media + setting), forced-colors, RTL (
arlocale spot-check on shell + library + store).Devices: desktop,
xs480px, iOS safe-area (dock inset +pb-safe), 4× CPU throttle scroll traces on/,/library,/store,/admin/users.Budgets: §6.1 table audited via DevTools layers panel on each flagship page.
Regression: the §3.3 containing-block cases — sidebar user menu position, admin MDX editor opacity, drawer slide opacity; autofill rendering on
/loginand/settings; skeleton/pending states;EmptyStateon every zero-data page encountered.CI: existing
web-ci.yml(typecheck/lint/tests/build/audit) plus visual spot-screenshots of the design-lab page per theme attached to the PR.
14. Risks & mitigations¶
Risk |
Mitigation |
|---|---|
Blur cost tanks low-end scroll perf |
The L1 no-blur rule for all repeated elements (the current implementation’s biggest cost is removed, not added); |
|
The explicit-class model makes every blur surface greppable (`glass-pane |
Glass Light fails contrast on bright aurora |
Token audit gate in §10; canvas radials capped in luminance; muted-text alpha tuned per theme like Glass Dark’s 0.82 |
Theme-id migration surprises signed-in users |
Self-heal already exists and is extended; account-sync API validation derives from |
Transition period looks inconsistent |
Old descendant patches stay until phase 8; phases are page-area-atomic so each PR ships a coherent area |
Safari filter/radius rendering bugs |
The |
Scope creep into games/apps |
|
15. Non-goals¶
No route moves, renames, or layout-width changes (
lib/layout-width.tsconstants frozen).No new fonts, no icon-set change (lucide stays), no
react-icons.No dark/light auto-switching by OS (explicit theme choice stays).
No component-library swap — Radix + CVA + the existing primitive set remain.
No redesign of emails, OG images, or the
rmhcodeCLI output.No behavioral changes to auth, payments, moderation, or data flows.
Appendix A — Full page checklist (implementation tracking)¶
Tick per page when it passes the §12 phase exit criteria.
Shell (Phase 3): _site.tsx aside/main structure · LeftSidebar ·
MobileSidebarShell · MobileNav→dock · MobileHeader/TopBar · PageLayout ·
AnimatedMain · RightSidebar · BackToTop · skip link visual
Feed/social (4): / · /explore · /search · /communities ·
/c/$slug · /tag/$tag · /thread/$rootId · /bookmarks · /drafts ·
/share
Messaging (4): /messages · /messages/$conversationId · /groups ·
/groups/$id · /notifications
Profile/personal (4): /profile/$id · /u/$userid ·
/u/$userid/post/$postid · /progress · /achievements · /recap ·
/wrapped
Library (5): /library (hero · search · blog row · albums · collections ·
curated shelf · community shelf · admin affordances · upload/edit modals) ·
/playlists
Creator Studio (5): /create (tab bar · Pages · Games+Ranked · Apps ·
User Builds · Personas) · /personas/$id · /builds · /v · /personas ·
/user-builds · /user-builds/manage · /user-builds/submit · /ranked ·
/leaderboard · /analytics
Store/economy (5): /store (MembershipPanel · ShopColumn) ·
/store/$userid · /shop · /pricing · /predictions · /wallet
(redirect — verify only)
Hubs (6): /study · /study/browse · /study/$deckId · /homes ·
/homes/listing/$id · /homes/manage · /homes/saved · /homes/submit ·
/homes/watches · /rmhladder (+shell) · /rmhladder/jobs ·
/rmhladder/jobs/$jobId · /rmhladder/alerts · /rmhladder/companies ·
/rmhladder/health · /rmhladder/pipeline · /rmhladder/resume ·
/rmhladder/review · /rmhladder/settings · /rideshare ·
/rideshare/ride · /rideshare/drive · /developer · /developer/docs ·
/developer/docs/$page · /roadmap · /quotes · /news · /blog ·
/music-trivia
Settings (6): /settings (+ ThemeGallery + new reduce-transparency
toggle) · /settings/privacy · /settings/security
Admin — all backend pages (6): /admin (dashboard) · /admin/users ·
/admin/user-builds · /admin/reports · /admin/security-reports ·
/admin/library-quota · /admin/library-storage · /admin/announcements ·
/admin/predictions · /admin/analytics · /admin/rideshare ·
/admin/audit · /admin/albums · /admin/albums/$id · /admin/blog ·
/admin/blog/new · /admin/blog/$slug/edit
Top-level (7): /login · /terms · /privacy · /cookies ·
/copyright · /security · /offline · /blog/$slug · /news/$slug ·
/liquid-glass (design lab, Phase 0) · marketing: /adaptive-intelligence ·
/rmh-capital · /rmh-pmc · /optimization
Global overlays (2–3): Dialog/ConfirmDialog · Toaster · Tooltip · CommandPalette · NotificationsPopover · user menu · ComposeModal · ShareModal · ProfileEditModal · ReportDialog · Welcome/WhatsNew/FreeMonth/LanguageFirstRun · CookieConsent · KeyboardShortcuts · MiniPlayer · EmojiPicker/ReactionMenu · NotFound/RouteErrorFallback
Appendix B — Reference CSS starter (Phase 0 seed)¶
Normative reference for the implementing agent — adapt, don’t paste blindly. Assumes the §3.2 tokens exist.
/* ============ Glass elevation system ============ */
@layer components {
.glass-fill {
background: var(--site-glass-tint);
/* rim + resting depth come from the shadow token, as Card does today */
box-shadow: var(--site-shadow-sm);
border: var(--site-border-width) solid var(--site-border);
border-radius: var(--site-radius);
}
.glass-pane {
background: var(--site-glass-tint);
box-shadow: var(--site-shadow-sm);
border: var(--site-border-width) solid var(--site-border);
border-radius: var(--site-radius);
-webkit-backdrop-filter: blur(var(--site-glass-blur-pane))
saturate(var(--site-glass-saturate));
backdrop-filter: blur(var(--site-glass-blur-pane))
saturate(var(--site-glass-saturate));
background-image: var(--glass-noise); /* data-URI, 2–3% opacity */
}
.glass-chrome {
background: color-mix(in srgb, var(--site-bg) 32%, transparent);
-webkit-backdrop-filter: blur(var(--site-glass-blur-chrome)) saturate(170%);
backdrop-filter: blur(var(--site-glass-blur-chrome)) saturate(170%);
}
.glass-chrome[data-scrolled] {
background: color-mix(in srgb, var(--site-bg) 55%, transparent);
}
/* Fixed asides: blur on ::before so fixed descendants keep the viewport
as containing block (LeftSidebar user menu). NEVER put backdrop-filter
on the aside itself. */
.glass-chrome--aside { background: color-mix(in srgb, var(--site-bg) 40%, transparent); }
.glass-chrome--aside::before {
content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
-webkit-backdrop-filter: blur(var(--site-glass-blur-chrome)) saturate(170%);
backdrop-filter: blur(var(--site-glass-blur-chrome)) saturate(170%);
}
.glass-overlay {
background: color-mix(in srgb, var(--site-bg) 62%, transparent);
box-shadow: var(--site-shadow);
border: var(--site-border-width) solid var(--site-border);
border-radius: var(--site-radius);
-webkit-backdrop-filter: blur(var(--site-glass-blur-overlay)) saturate(180%);
backdrop-filter: blur(var(--site-glass-blur-overlay)) saturate(180%);
}
.glass-scrim {
background: rgb(0 0 0 / 0.5);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
}
.glass-inset {
background: var(--site-glass-ink);
border: var(--site-border-width) solid var(--site-border);
border-radius: var(--site-radius-sm);
box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.35),
inset 0 -1px 0 var(--site-glass-rim-soft);
}
.glass-opaque { background: var(--site-bg); } /* full-screen takeovers */
/* Interactivity (§5.1–5.2) */
.glass-interactive { position: relative; transition:
background var(--site-transition-speed), box-shadow var(--site-transition-speed),
transform var(--site-transition-speed); }
.glass-interactive:hover { background: var(--site-glass-tint-strong); }
.glass-interactive:active { transform: scale(0.985); }
@media (hover: hover) and (pointer: fine) {
.glass-interactive::after {
content: ""; position: absolute; inset: 0; z-index: 1;
border-radius: inherit; pointer-events: none;
background: radial-gradient(140px circle at
var(--glass-px, 50%) var(--glass-py, -30%),
var(--site-glass-light), transparent 70%);
opacity: 0; transition: opacity 0.18s;
}
.glass-interactive:hover::after { opacity: 1; }
}
}
/* ============ Global degradations ============ */
@media (prefers-reduced-transparency: reduce) { /* + html.reduce-transparency */
:is(.glass-fill, .glass-pane, .glass-chrome, .glass-chrome--aside,
.glass-overlay, .glass-inset) {
background: var(--site-surface-opaque, var(--site-surface));
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
.glass-chrome--aside::before { display: none; }
.glass-refract::before { display: none; }
}
html.perf-lite :is(.glass-pane) { /* demote panes to fills */
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
html.perf-lite .glass-refract::before { display: none; }
html.perf-lite .glass-interactive::after { display: none; }
@media (forced-colors: active) {
:is(.glass-fill, .glass-pane, .glass-chrome, .glass-overlay, .glass-inset) {
border: 1px solid; /* keep structure when backgrounds are stripped */
}
}
// hooks/useGlassLight.ts — sketch (§5.1). Mounted once in Providers.tsx.
export function useGlassLight() {
useEffect(() => {
if (!matchMedia('(hover: hover) and (pointer: fine)').matches) return;
if (document.documentElement.classList.contains('perf-lite')) return;
let raf = 0;
let last: HTMLElement | null = null;
const onMove = (e: PointerEvent) => {
if (raf) return;
raf = requestAnimationFrame(() => {
raf = 0;
const el = (e.target as Element | null)
?.closest<HTMLElement>('[data-glass-light]') ?? null;
if (last && last !== el) {
last.style.removeProperty('--glass-px');
last.style.removeProperty('--glass-py');
}
if (el) {
const r = el.getBoundingClientRect();
el.style.setProperty('--glass-px', `${((e.clientX - r.left) / r.width) * 100}%`);
el.style.setProperty('--glass-py', `${((e.clientY - r.top) / r.height) * 100}%`);
}
last = el;
});
};
document.addEventListener('pointermove', onMove, { passive: true });
return () => {
document.removeEventListener('pointermove', onMove);
if (raf) cancelAnimationFrame(raf);
};
}, []);
}