From 1b5db44b7ecf907ef8b682d11f1f8d8e8e123c0d Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Wed, 3 Jun 2026 22:45:53 -0500 Subject: [PATCH] fix(design-system): compose rim + elevation tokens as chained box-shadow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User reported after PR #98 ("backdrop-filter now applies") that the dashboard glass panels still looked identical to solid cards. Browser CDP diagnostic on production found the root cause: the .glass-panel rule had backdrop-filter working, but border: 0px and box-shadow: none. Why both were missing: 1) `border: 1px solid var(--rim-light-outer)` — the --rim-light-outer token is a complete box-shadow declaration (`0 0 0 1px #2d181014`), NOT a color value. When passed to `border: 1px solid `, the first whitespace-separated token of the shadow string ("0") parses as border-width = 0px, yielding no border at all. 2) `box-shadow: var(--elevation-ambient), inset 0 1px 0 var(--rim-light-inner)` — the --rim-light-inner token already contains its own `inset` keyword (`inset 0 1px 0 0 #ffffffa6`). Wrapping it in another `inset 0 1px 0 ...` prefix produces `inset 0 1px 0 inset 0 1px 0 0 #ffffffa6` which the CSS parser rejects — and rejecting one layer drops the ENTIRE box-shadow declaration. That's why `getComputedStyle().boxShadow === 'none'`. Fix: the tokens are designed to be *chained* into a single `box-shadow` declaration, exactly the way 's `boxShadow: shadowParts.join(', ')` composes them (see components/ui/GlassSurface.js lines 5-15). Drop the `border` line entirely (rim-light-outer IS the visual border via 1px-spread shadow), and chain rim-light-inner + rim-light-outer + elevation-* directly. Verified by `npm run build` — compiled CSS now has all three: - background - backdrop-filter (+ -webkit- prefix from autoprefixer) - box-shadow with all 4 layers correctly chained This will produce a visible: top inset rim highlight, dark hairline outer ring, soft ambient drop shadow — i.e. an actual glass card. Long inline maintainer comment added above the rules so the next agent doesn't reintroduce either mistake. Co-authored-by: Cursor --- styles/globals.css | 52 ++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/styles/globals.css b/styles/globals.css index 4f6a67f..2b03d9f 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -439,31 +439,47 @@ body { from Tailwind (the class itself doesn't set border-radius because consumers vary between rounded-xl and rounded-2xl). ============================================================ */ -/* IMPORTANT — backdrop-filter quirks with Next.js 16's Lightning CSS: +/* IMPORTANT — composing rim + elevation tokens correctly. + The --rim-light-outer / --rim-light-inner / --elevation-* tokens are + COMPLETE box-shadow declarations (each contains offset/spread/color or + the `inset` keyword), not raw color values. They are designed to be + *chained into a single box-shadow*, exactly the way 's + `boxShadow: shadowParts.join(', ')` composes them. Do NOT use + `border: 1px solid var(--rim-light-outer)` — that parses + `border-width` from the first token of the shadow string ("0"), which + yields a 0px border. And do NOT wrap --rim-light-inner in another + `inset 0 1px 0` prefix — the token already contains its own `inset` + keyword, so wrapping produces invalid syntax that the browser drops + the entire box-shadow declaration for. PR #98 had both mistakes, + which is why the panels in PR #97/#98 had neither visible border nor + drop shadow despite the backdrop-filter being correct. + + Backdrop-filter quirks with Next.js 16's Lightning CSS (also relevant): 1) `backdrop-filter: blur(var(--X)) saturate(var(--Y))` is silently - stripped from the compiled CSS (same root cause that emptied - `.mobile-nav-backdrop` in production). Use LITERAL values. - 2) Writing both `backdrop-filter` and `-webkit-backdrop-filter` with - identical values triggers Lightning CSS's de-dup; it keeps only the - `-webkit-` form which Chrome/Edge/Firefox ignore. Write only the - unprefixed `backdrop-filter` and let Lightning CSS's autoprefixer - add the vendor prefix per browserslist (it does for ios_saf <18). - If the blur radii ever need to change, change them here AND in the - matching --glass-blur-* tokens (kept for `` JSX consumers - which set `backdropFilter` via inline style — those are unaffected by - the Lightning CSS optimizer). */ + stripped (var() inside function args breaks the optimizer). Use + LITERAL values here. Tokens stay in :root for the + JSX primitive which sets backdropFilter via inline style. + 2) Writing both `backdrop-filter` AND `-webkit-backdrop-filter` with + identical values triggers Lightning CSS's buggy de-dup; it keeps + only the `-webkit-` form which modern browsers ignore. Write only + the unprefixed form; Lightning CSS's autoprefixer adds the prefix + per browserslist (ios_saf <18 still needs it). */ .glass-panel { - background-color: var(--glass-surface-mid); + background: var(--glass-surface-mid); backdrop-filter: blur(12px) saturate(180%); - border: 1px solid var(--rim-light-outer); - box-shadow: var(--elevation-ambient), inset 0 1px 0 var(--rim-light-inner); + box-shadow: + var(--rim-light-inner), + var(--rim-light-outer), + var(--elevation-ambient); } .glass-panel-strong { - background-color: var(--glass-surface-high); + background: var(--glass-surface-high); backdrop-filter: blur(20px) saturate(180%); - border: 1px solid var(--rim-light-outer); - box-shadow: var(--elevation-pronounced), inset 0 1px 0 var(--rim-light-inner); + box-shadow: + var(--rim-light-inner), + var(--rim-light-outer), + var(--elevation-pronounced); } /* Gradient text effects — Deck Hearth warm palette only. -- 2.45.2