fix(design-system): compose rim + elevation tokens as chained box-shadow

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 <var>`, 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 <GlassSurface>'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 <cursoragent@cursor.com>
This commit is contained in:
Randall Stillwell 2026-06-03 22:45:53 -05:00
parent 40e00e57c6
commit 1b5db44b7e

View file

@ -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 <GlassSurface>'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 `<GlassSurface>` 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 <GlassSurface>
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.