Skip to content

fix: correct auto-foreground compositing and text hue - #26

Merged
WeekendSuperhero merged 12 commits into
mainfrom
weekendsuperhero/auto-fore-ground-fix
Aug 26, 2026
Merged

fix: correct auto-foreground compositing and text hue#26
WeekendSuperhero merged 12 commits into
mainfrom
weekendsuperhero/auto-fore-ground-fix

Conversation

@WeekendSuperhero

@WeekendSuperhero WeekendSuperhero commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Linear

Fixes INSET-0

Summary

  • Fixes chakra and crystal compositing models by properly including solidify, wash, and gloss layers.
  • Evaluates foreground ink against the composited surface hue rather than raw tint hue to eliminate drift.
  • Resolves a hue jump bug for veiled cards on opaque pages.
  • Promotes fixed backdrops to their own compositing layer to prevent Chromium scroll artifacts.
  • Extracts core foreground solving logic into lib/oklch-utils.ts.

Changes

Compositing & Auto-foreground

  • Updated the chakra surface model to include its translucent body, the solidify floor, and the gloss stack.
  • Fixed crystal floor lightness and capped crystal and gloss chroma appropriately.
  • Shifted auto-foreground to track composited surface hue (surface.h) to align text ink with the visual surface.
  • Extracted foregroundRamp, pickTonalInBand, and readableTonal to lib/oklch-utils.ts.

Styles & Presets

  • Excluded .glass-veil from opaque page foreground remaps in styles.css to fix the hue jump.
  • Added @utility glass-backdrop-layer (will-change: transform) to background containers to fix Chromium scroll issues.
  • Tuned light mode --glass-wash-l via .dark overrides.

Testing

  • Added auto-foreground-hue-match.test.tsx to assert text ink follows surface hue.
  • Added auto-foreground-layer-caps.test.tsx and auto-foreground-adaptive-clip.test.tsx.
  • Updated surface parity tests to mirror corrected compositing stacks.

Test plan

  • Inspect sheer cards on tinted presets (e.g., rose) to ensure text contrast and hue matching are accurate.
  • Verify veiled cards on opaque pages no longer exhibit a foreground hue jump.
  • Scroll over sheer backgrounds in Chrome to confirm backdrop scroll artifacts are gone.
  • Run the test suite to ensure all APCA contrast and gamut sweeps pass.

PR description generated by Agent

I'd flagged two; reading the CSS turned up four, and a fifth structural one.

| # | model had | CSS actually paints |
|---|---|---|
| 1 | crystal floor `L100` | `--glass-crystal-l: 96` (the token comment even says *"100 carries zero chroma"*) |
| 2 | crystal chroma `tintC × 0.6` | `--glass-tint-c-hi × 0.6`, capped at 0.017 — up to **6×** too colorful (tourmaline asked 0.0636 against a 0.0102 ceiling) |
| 3 | gloss ink chroma `0` | `min(tintC × --glass-gloss-tint, --glass-gloss-c-max)` — the gloss is **tinted**, and its color was dropped from every crystal/chakra band |
| 4 | chakra = `chakra-l → solidify` | `page → chakra-bg@--glass-chakra-a → solidify → wash → gloss` |

**#4 is the big one.** `--glass-chakra-bg` is *translucent* (α 0.62/0.58), so the page shows through it, and `--glass-chakra-stack-bg` is the same stack crystal bakes — chakra takes the tint wash and the gloss too. The old model dropped the page, the body's own alpha, the entire wash, **and** the gloss. Confirmed against `utilities.css:56`: `background-image: [hover], [accent], [material stack], [solidify]`, and later image layers paint *below* earlier ones, so solidify is the bottom-most image layer, sitting directly on the background-color.

That layer order means **solidify paints over the chakra body**, so `--glass-chakra-l` — documented as *"body/table lightness — its own dial"* — carries only **13% of its nominal authority**:

```
solidify  chakra-a   L@88   L@12   authority
0.70      0.62       90.5   80.8    13%  ← ships
0.70      1.00       89.8   74.1    21%
0.00      0.62       91.9   58.7    44%
0.00      1.00       89.8   33.1    75%
```

A full L88→L12 sweep moves the surface just 9.6 L, and both ends stay light-side of the ~L70 crossover — so the text never flips polarity. That's a CSS design question (should chakra reduce or drop solidify, as `opaque` does?), not something the model should paper over.

`tests/auto-foreground.test.tsx` had a test asserting that sweep *does* flip the text. It only passed because the old model treated `chakra-l` as an opaque base. I didn't just delete it — I split it in two: one asserting the direction and magnitude it genuinely has (darker body → darker ink, moving >5 L), and a new one that drops solidify and opaques the body so `chakra-l` drives 75% and the flip is real. That keeps the original intent — *the tier must band against its own surface* — testable.

The two surface-parity failures were self-inflicted: that file keeps its **own mirror** of the models, annotated *"Kept in step with the applyTiers() call sites"*. I updated the mirror; it wasn't a threshold relaxation.

**155 tests pass** (was 154), lint, typecheck, theme invariants, gamut, and the APCA oracle all green. Impact is small on crystal (≤0.2 L, chroma slightly up now the gloss carries its tint) and real on chakra — up to **2.1 L darker** on the jewels, and 3.4 L *lighter* on peridot, since it now sees the wash.

`check-contrast` still fails on the same five presets from before — amethyst/rose/tourmaline/goldstone margins plus `destructive` under the body floor. Untouched by this work; those are normal-surface, and the alpha values to clear them are still the ones from last turn. Want me to apply those now?

I should flag one thing I deliberately left out: the gloss **streak** (0.15α) and **glow** (0.2α). The glow is past its 70% fade before it reaches the title zone, and the streak's mean there is a geometry estimate, not a measurement — both would only add light, so omitting them is the conservative direction. And I still haven't verified the gamma-sRGB compositing assumption against a real browser paint; there's no Playwright here.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 25, 2026

Copy link
Copy Markdown

Deploying sistine with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2aaa7c0
Status: ✅  Deploy successful!
Preview URL: https://c59009e4.sistine.pages.dev
Branch Preview URL: https://weekendsuperhero-auto-fore-g.sistine.pages.dev

View logs

@agent-pr-updater agent-pr-updater Bot changed the title weekendsuperhero/auto fore ground fix fix: correct chakra and crystal auto-foreground compositing Aug 25, 2026
```
✓ preset contrast — 42 preset/mode pairs clear the 75 Lc body floor (tightest destructive light at 76.5)
✓ tint gamut — worst spread 1.88× (max 4.6), worst overage 1.88× (max 4.6)
✓ theme invariants — 121 rules, 17 presets value-synced, theme.json in sync
✓ 155 tests · lint · typecheck · APCA oracle (max |ΔLc| 0.000) · registry smoke (71 items)
```

`--glass-wash-l` only, light mode, five presets:

| preset | light wash | dark |
|---|---|---|
| amethyst | 55 → **57** | pinned 55 |
| rose | 64 → **67** | pinned 64 |
| tourmaline | 67 → **69.5** | pinned 67 |
| goldstone | 63 → **67** | pinned 63 |
| destructive | 63 → **68.5** | new `.dark` block, pinned 63 |

Your constraints held: **`--glass-tint-a` untouched**, opaque cards untouched, dark mode byte-identical.

**Your light-mode-only framing was load-bearing.** It doesn't change the math — every number was already `dark: false` — but it does change the *mechanism*. `presets.css` loads at `globals.css:21`, after `tokens.css` at 16, and at equal specificity source order wins. So a preset's base block already overrode `.dark { --glass-wash-l: 58% }` and applied in **both** modes. Scoping to light required explicitly pinning today's values into the `.dark` blocks (specificity 0,2,0), not just editing the base.

**And it killed my recommended plan.** I was about to split `--glass-tint-c` per mode. `applyTint` at `glass-tint-switcher.tsx:285` inlines `--glass-tint-c` onto `<html>` unconditionally, which shadows *both* blocks — the exact trap that file documents for alpha at lines 273–276, where alpha is passed as `null` specifically so the split survives. That fix would have silently done nothing on any page with the switcher mounted. `--glass-wash-l` is never inlined, so it's the one knob that can differ per mode and actually render.

Since I couldn't move chroma, the raised washes ask slightly more than the gamut holds and clip per-channel. Measured cost: **≤0.6° hue, ≤5% chroma**, and gamut overage stayed at 1.88× — my changes land near 1.1×, so they didn't become the worst case. The contrast numbers already account for it, since the model clips.

`registry/theme/globals.css` is **generated** by `scripts/build-theme.mjs`, not hand-maintained — my first pass hand-edited it, which `check-theme` correctly flagged as stale. Regenerated via `registry:check`; `public/r/theme.json` updated too.

Uncommitted: `presets.css`, `registry/theme/globals.css`, `public/r/theme.json`. I see you committed the compositing work as `21ae7bd`.

Two open items I'd still flag, both unchanged by this: the **ink-vs-surface hue mismatch** (up to 6° on goldstone/rose), and that none of this delivers tonal ink at L30 — that still needs the light-wash-per-mode character decision we mapped out.
…parate change on top.

`--glass-tint-h` describes the **wash**. What a reader sees is that wash composited over the near-neutral solid/solidify floor — and that mix doesn't travel a radial path through the hue plane, because the floor has almost no hue to interpolate toward. So the surface lands a few degrees off the declared angle, while the ink was being generated at the raw token. Text sat at a hue its own surface never occupied.

`components/auto-foreground.tsx` — the foreground ramp is now seeded from the **composited surface hue**, and each adaptive tier (`readableForeground`) solves at *its own* surface's hue rather than `tintH`. Icons follow the same rule when `iconHue` is null; an explicitly pinned hue or harmonic still wins, since those are deliberate choices.

Measured on the shipped light presets:

| preset | tint-h → surface h | before | after |
|---|---|---|---|
| rose | 8 → 2.5 | `#30010f` | `#300113` |
| goldstone | 22 → 17.1 | `#310105` | `#310108` |
| lapis | 268 → 271.7 | `#05014f` | `#0a014d` |
| peridot | 128 → 126.6 | `#2d4303` | `#2f4203` |

Contrast is untouched — worst ΔLc is **+0.08**. This is a correctness fix, not a visual change.

New `tests/auto-foreground-hue-match.test.tsx`. I checked it actually catches the bug rather than just passing: reverting to `tintH` fails **6 of its assertions**, with the ink sitting at exactly `8.00 / 22.00 / 75.00 / 145.00` while surfaces were at `3.30 / 18.50 / 73.93 / 149.13`.

It includes a control worth calling out: **opaque must stay at `tint-h`**. That surface is solid-painted with nothing composited over it, so it genuinely has no drift — the assertion guards against a future change blanket-rotating every tier instead of tracking each surface.

```
✓ 163 tests (15 files, was 155/14) · lint · typecheck
✓ preset contrast — tightest destructive light at 76.5
✓ tint gamut · theme invariants (121 rules) · APCA oracle 0.000 · registry smoke (71 items)
```

Uncommitted: `components/auto-foreground.tsx`, the new test, and `public/r/auto-foreground.json` (regenerated).

One caveat on scope: the residual spread *between* materials is now ≤5.5° (opaque at the tint hue, the three sheer surfaces a few degrees off). At the ink's chroma (~0.08) that's well under a JND, so I treated matching each ink to its own surface as more correct than forcing all four to agree — but if you'd rather every material's text share one angle, that's a different call and easy to switch.

The remaining open item is unchanged: tonal ink at L30 still needs the light-wash-per-mode character decision.
@WeekendSuperhero WeekendSuperhero added the muse:review Request a Muse review round (re-add to re-run) label Aug 25, 2026
@github-actions github-actions Bot added muse:reviewing Muse review round in progress and removed muse:review Request a Muse review round (re-add to re-run) labels Aug 25, 2026
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

🤖 Muse — Security · ✅ pass

Round 3 · reviewed 2aaa7c0 · 0 open · 0 resolved this round

Summary: Reviewed the PR for security issues. The changes exclusively affect frontend CSS styling, layer promotion, and UI contrast calculations. No untrusted input is processed and no security boundaries are crossed.

Re-run: add the muse:review label · force same-SHA re-run: muse:force

@github-actions github-actions Bot added the security:clear Muse security review: no blocking findings label Aug 25, 2026
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

🤖 Muse — Test Coverage · ✅ pass

Round 2 · reviewed ca0442c · 0 open · 2 resolved this round

Summary: The test coverage is comprehensive. The PR resolves previous gaps by adding dedicated tests for the hue match fix, adaptive clip logic, and layer caps with correct assertions. Both previous findings regarding missing icon variables and zeroed layer chromas are fully resolved. Remaining CSS and token changes are stylistic and do not require additional tests.

✅ Resolved this round (2)
  • TST-1-1 — tests/auto-foreground-hue-match.test.tsx asserts --foreground-ui, --foreground-ui-crystal, and --foreground-ui-chakra against composited hue.
  • TST-1-2 — tests/auto-foreground-surface-parity.test.tsx mocks a non-zero --glass-tint-c and updates expected models to verify exact chroma caps.

Re-run: add the muse:review label · force same-SHA re-run: muse:force

@github-actions github-actions Bot added the tests:sufficient Muse test review: coverage adequate for this diff label Aug 25, 2026
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

🤖 Muse — Code Review · ✅ pass

Round 3 · reviewed 2aaa7c0 · 0 open · 0 resolved this round

Summary: The CSS specificity fix successfully resolves the overriding issue by lifting --glass-solid-a out of the zero-specificity :where() block. The updates to the frontend tester correctly observe data-glass attribute changes and re-seed the tester's surface model without clobbering user-dragged slider values.

Re-run: add the muse:review label · force same-SHA re-run: muse:force

@github-actions github-actions Bot added review:approved Muse code review: no blocking findings muse:approved All Muse review agents positive and removed muse:reviewing Muse review round in progress labels Aug 25, 2026
@github-actions github-actions Bot removed muse:approved All Muse review agents positive review:approved Muse code review: no blocking findings security:clear Muse security review: no blocking findings tests:sufficient Muse test review: coverage adequate for this diff labels Aug 26, 2026
…sest fit in the system.

Five materials (`glass`, `frosted`, `crystal`, `chakra`, `opaque`), four tier sets (`""`, `-opaque`, `-crystal`, `-chakra`). Only crystal, chakra and opaque remap `--foreground*` to their own set — **`glass` and `frosted` both fall through to `""`**.

The stale claim was `auto-foreground.tsx:391`: *"Run twice"*, from when there were only two sets. It runs four times. Fixed, along with the parity test's docstring.

`glass` and `frosted` aren't the same surface. Frosted paints `--glass-frosted-bg` — the same 5-stop sheet lifted by `--glass-frost-boost` (0.1 light / 0.06 dark), so roughly double the alpha.

And the paint order is `bg-color → solidify → wash → **sheet**` (the sheet is first in the `background-image` list, so topmost). `glassSolidSurface` stops at the wash — **neither sheet is modeled at all**:

| preset | modelled | + glass sheet | + frosted sheet | body Lc: model / glass / frosted |
|---|---|---|---|---|
| lapis | L87.4 | L88.7 | L89.8 | 79.7 / 82.2 / **84.2** |
| goldstone | L86.9 | L88.3 | L89.3 | 77.8 / 80.4 / **82.4** |
| rose | L87.1 | L88.5 | L89.5 | 78.1 / 80.6 / **82.7** |

So text is banded against a floor darker than what renders — the **safe** direction, and no tier breaches its band ceiling of 90. But it means every glass surface runs 2.2–2.6 Lc over-contrasted and frosted 4.2–4.6, which is part of why light-mode text sits darker than it needs to.

I've documented this rather than changed it, because it's a design call and the numbers are all in-band. Two options if you want it tightened:

- **Model the sheet in the `""` surface** — fixes glass, but one model can't serve both sheets, so frosted stays loose (just less so).
- **Give frosted a fifth tier set** — the complete fix. Needs the new suffix in `applyTiers`, a `[data-material="frosted"]:not(.glass-veil)` remap, and a mean-alpha term for the sheet gradient (same treatment `GLOSS_TOP_A` gets for the crystal gloss).

The second is the one that makes the count five-for-five. Worth noting it would make frosted text *lighter*, which is the direction you've wanted all along.

Everything still green: 171 tests, lint, typecheck, contrast, gamut, theme, APCA, registry smoke.
A **Foreground solve** section in `lib/oklch-utils.ts` — the right home because `auto-foreground` already declares `@sistine/oklch-utils` as a registry dependency, so it ships with no new wiring:

```
TONAL_MIN_L / TONAL_MAX_L / LC_MARGIN / LC_AIM_KNOWN
showThrough(...alphas)        the backdrop's surviving weight
boostBand(band, boost, aim)   target lifts; floors/ceilings never move
foregroundRamp(opts)          build + tonal clip, neutral-exempt
pickTonalInBand(...)          clipped pick + legibility guard
readableTonal(...)            adaptive twin + the same clip
```

`auto-foreground.tsx` shrank by ~190 lines and is now the *caller*, not the owner. `foreground-tester.tsx` and `check-contrast.mjs` call the same functions.

The two files built **different-sized ramps**: the component picks from `count + 1` (the readable half), the tester from `2 × count + 1` — offering steps past the base that the surface can never make legible. That's now a documented `levels` default rather than a coincidence. It hadn't bitten on amethyst, but it was live.

I checked the shared code is load-bearing rather than assuming it: disabling the clip inside `foregroundRamp` fails **4 DOM tests**. My first attempt at that check silently no-op'd — biome had reformatted the line my patch anchored on, so the assertion never fired and it "passed" against an unmodified file. Redone against the real text.

Then drove Safari across five presets spanning both modes, both gamut arcs, and a neutral:

```
amethyst (dark)      all tiers MATCH
lapis (light)        all tiers MATCH
goldstone (light)    all tiers MATCH
turquoise (dark)     all tiers MATCH
selenite (light)     all tiers MATCH
```

`lapis` first reported a mismatch — but that was my extraction: Bd and Fn resolve to the same index there, so the panel renders one **combined** marker (`Fn/Bd`) and my exact-text lookup missed it. Both tokens were identical. Fixed the probe, not the code.

```
✓ 175 tests · lint · typecheck · contrast (77.1, unchanged) · gamut · APCA 0.000
✓ theme (120 rules) · registry smoke (71 items) · patterns
```

The guard's number is bit-identical before and after, which is the signal the refactor changed no behaviour.

Uncommitted: `lib/oklch-utils.ts`, both components, `check-contrast.mjs`, one test, and the two regenerated registry JSONs.
…s below.

`styles.css` had an asymmetry. Two adjacent rules for the same page style, only one carving out `.glass-veil`:

```css
[data-glass="opaque"] :where(.glass):not([data-material]):not(.glass-veil) { --srf-solidify: none; }
[data-glass="opaque"] :where(.glass):not([data-material])                  { --foreground: var(--foreground-opaque); … }
```

Crystal and chakra both exclude veiled. And the comment three lines above the second rule already gives the reason — *"a veiled floor is neutral"* — it just wasn't applied to the foreground remap.

A veiled card doesn't paint `--glass-opaque-bg`; `glass-veil` moves the background-colour channel to the neutral `--veil-floor`. So those cards were getting ink solved for a surface they don't have. It showed as a **hue jump**, and the reason is the work from earlier in this session: the opaque floor is the one surface with nothing composited over it, so it sits at the raw `--glass-tint-h` (22.0) while every sheer surface lands a few degrees off (14.6).

I split the rule so the **edge** treatment still applies to every adaptive panel — a veiled card under the opaque page should still read as a solid panel — while only the foreground remap is carved out. Verified live on both sides:

```
.glass (plain)      → opaque tokens (22.0)   CORRECT
.glass.glass-veil   → base tokens   (14.6)   CORRECT
borders: identical on both
```

Your instinct that it "might point to a real issue elsewhere" was right — it's a page-style bug, not a `/colors` bug, and it affects any veiled card on an opaque page.

What I did verify: **`opaque` is the only material that sets `--srf-filter: none`.** Every other one runs a live `backdrop-filter: blur(…) saturate(…)`, and the page's backdrop is a `position: fixed` layer with no compositing promotion. That matches your symptom exactly and is a well-known WebKit pairing.

What I couldn't do is confirm it. Two attempts, both dead ends, and I want to be clear I initially misread one:

- **Screenshots**: I claimed I'd caught it, then found the settled frame was byte-identical to the mid-scroll one (both 627.8 kB). That pink panel was just the goldstone tint I'd left set. A screenshot samples a settled frame, so it structurally cannot catch a per-frame artifact.
- **Frame timing**: median 10 ms with blur on *and* off, zero frames over 20 ms. But `scrollTo` in rAF drives scrolling on the **main thread**, and backdrop-filter artifacts are specific to **compositor-driven** scrolling (trackpad/wheel momentum) — which this tooling can't emulate. So the test never exercised the failing path.

The standard mitigation is promoting the fixed backdrop layer (`transform: translateZ(0)` or `will-change: transform`) so the compositor keeps it on its own layer and the blur samples it cheaply. It's one element per background component, low cost. But it's a rendering change I'd be shipping blind, and speculative compositing hints can trade one artifact for another.

If you want, I can add it and you scroll-test — that's a 30-second confirmation for you and unverifiable for me. Otherwise a screen recording of the glitch would let me narrow it properly.

```
✓ 175 tests · lint · theme (121 rules) · registry
```
… was chasing and points at layer promotion, which the two engines handle differently.

The fixed backdrop is **not promoted**. Measured on the deployed page:

```
DIV.fixed.inset-0.-z-10   will-change: auto   transform: none   contain: none
```

It sits at `z-index: -10` beneath the entire document, and every sheer material has to re-sample it. `opaque` is the **only** material setting `--srf-filter: none` — and the only one that doesn't glitch. That correlation is exact.

I also checked the ancestor chain for backdrop-root triggers (`filter`, `opacity < 1`, `mask`, `will-change`, `contain`, `isolation`, `transform`) — **none**, so the backdrop root is the document. That rules out the misresolved-backdrop-root class of bug and leaves layer promotion.

Chromium and WebKit differ exactly here: WebKit tends to promote fixed layers aggressively, Chromium doesn't unless hinted. That's consistent with it reproducing in one and not the other.

A documented `@utility glass-backdrop-layer { will-change: transform }` in `utilities.css`, applied to all **10** `fixed inset-0 -z-10` containers across the four background components.

I checked the precondition first rather than assuming: promoting with `will-change`/`transform` makes an element a containing block for fixed descendants, which would silently reparent them. That backdrop has **zero** `position: fixed` descendants and no `background-attachment: fixed`, so it's safe — and I wrote that precondition into the comment so anyone adding children knows to re-check.

Verified it actually lands (a Tailwind `@utility` that never emits would pass every check silently):

```
will-change=transform   fixedDescendants=0
backdrop rect = 1305x664 at (0,0)   ← layout unchanged
```

**I still haven't reproduced the glitch or confirmed the fix** — there's no Chromium on this machine (checked `/Applications`, `PATH`, and `node_modules/.bin`). This is an evidence-backed diagnosis, not a verified repair, and layer promotion can occasionally trade one artifact for another. It needs your eyes in Chrome.

If it doesn't clear it, the next thing I'd look at is the animated pattern children — `pattern-background.tsx` puts `will-change: transform` on several elements *inside* the backdrop, and a backdrop-filter sampling a region containing many composited sublayers is its own Chromium failure mode.

```
✓ 175 tests · lint · typecheck · theme (122 rules) · patterns · registry
```

Both fixes are uncommitted, alongside the `styles.css` opaque fix from before.
@WeekendSuperhero WeekendSuperhero added the muse:review Request a Muse review round (re-add to re-run) label Aug 26, 2026
@github-actions github-actions Bot removed the muse:review Request a Muse review round (re-add to re-run) label Aug 26, 2026
@github-actions github-actions Bot added muse:reviewing Muse review round in progress tests:sufficient Muse test review: coverage adequate for this diff labels Aug 26, 2026
@agent-pr-updater agent-pr-updater Bot changed the title fix: correct chakra and crystal auto-foreground compositing fix: correct auto-foreground compositing and text hue Aug 26, 2026
@github-actions github-actions Bot added security:clear Muse security review: no blocking findings review:approved Muse code review: no blocking findings muse:approved All Muse review agents positive and removed muse:reviewing Muse review round in progress labels Aug 26, 2026
…ixed this one: I'd fixed the card's *text*, you were describing its *surface*. Two separate causes, both now found.

The opaque page style's `--glass-solid-a: 1` sat inside a `:where(…)` block — **zero specificity** — competing against `:root { --glass-solid-a: 0.65 }` at (0,1,0). Root won every time.

Every other declaration in that block survives because nothing else declares them; they're material *slots*. `--glass-solid-a` is the only one with a competing `:root` value, so it's the only one that lost. Measured: `--glass-solid-a` resolved to `.65` on `<html>` under `[data-glass="opaque"]`, identical to every other page style.

Moved it to a bare `[data-glass="opaque"]` selector — (0,1,0), ties `:root`, wins on source order since `globals.css` imports `styles.css` after `tokens.css`. It's a plain literal so it just inherits into the tint/toast scopes.

**This isn't a demo bug.** The comment right above that block says it exists so veiled menus and toasts "stay legible on an opaque page" — that has never worked. Any veiled overlay on an opaque page was staying translucent.

`useState(0.65)` — hardcoded, never read from the page. So even with the CSS fixed, the card's inline `--glass-solid-a: 0.65` overrode it. Worse, it *modelled* the surface at 0.65 too, so the panel's Lc numbers were wrong under an opaque page while claiming to match production.

Now seeded from the live `--glass-solid-a`, re-seeding only when the **page's** value changes so a dragged slider isn't clobbered.

Third cause underneath that: the tester's `MutationObserver` watched `class`, `data-glass-tint`, `style` — but **not `data-glass`**, the page style itself. So it never noticed page-style changes at all. Added.

```
translucent  root=.65  card inline=0.65  cardBg=oklch(0.18 0 0 / 0.65)
opaque       root=1    card inline=1     cardBg=oklch(0.18 0 0)        ← solid
translucent  root=.65  card inline=0.65  cardBg=oklch(0.18 0 0 / 0.65) ← re-seeds back
```

Screenshot confirms it: no starfield through the card, slider reads "Solid 100%".

One thing that is *not* a bug: the card stays neutral grey rather than tinted, where the panels around it are tinted. That's `glass-veil` doing its job — a neutral solid ground — and the tester uses it deliberately, because the veiled floor is the surface `glassSolidSurface` models. Tinting it would make the Lc numbers stop matching production.

```
✓ 175 tests · lint · typecheck · contrast · gamut · APCA · theme (123 rules) · registry
```
@github-actions github-actions Bot removed muse:approved All Muse review agents positive review:approved Muse code review: no blocking findings security:clear Muse security review: no blocking findings tests:sufficient Muse test review: coverage adequate for this diff labels Aug 26, 2026
@WeekendSuperhero WeekendSuperhero added the muse:review Request a Muse review round (re-add to re-run) label Aug 26, 2026
@github-actions github-actions Bot added muse:reviewing Muse review round in progress review:approved Muse code review: no blocking findings security:clear Muse security review: no blocking findings and removed muse:review Request a Muse review round (re-add to re-run) labels Aug 26, 2026
@WeekendSuperhero
WeekendSuperhero merged commit be76ff7 into main Aug 26, 2026
19 checks passed
@WeekendSuperhero
WeekendSuperhero deleted the weekendsuperhero/auto-fore-ground-fix branch August 26, 2026 14:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

muse:reviewing Muse review round in progress review:approved Muse code review: no blocking findings security:clear Muse security review: no blocking findings

Development

Successfully merging this pull request may close these issues.

1 participant