From 2e6d4d042a326e38e793ae043832376dcf3ae78b Mon Sep 17 00:00:00 2001 From: Void Date: Tue, 10 Nov 2026 03:31:52 +0000 Subject: [PATCH] auth: footer band review fixes: one block, no pill or border, color-mix fallback, sRGB test The first footer block had spliced into the sign-in card's rule (a Y-only mask duplicate that overrode the side fade, a pill radius and border on the footer); the footer is now one rule. The test mixes in encoded sRGB as Chrome does. Co-Authored-By: Claude Sonnet 5.5 --- src/app/features/scene/skyPlate.ts | 4 +-- src/app/features/scene/skyTheme.test.ts | 23 +++++++++---- src/app/pages/auth/styles.css.ts | 45 +++---------------------- 3 files changed, 23 insertions(+), 49 deletions(-) diff --git a/src/app/features/scene/skyPlate.ts b/src/app/features/scene/skyPlate.ts index 2b9f4484..86429ba5 100644 --- a/src/app/features/scene/skyPlate.ts +++ b/src/app/features/scene/skyPlate.ts @@ -12,7 +12,7 @@ export const SKY_PLATE_FALLBACK = { * The sign-in footer's tint: how much of the plate's fill (--sky-bubble) is mixed over the blurred scene behind the * footer line. The footer is a blurred, tinted band with soft edges, not a plate: the scene shows through it. The * footer ink is the plate's, so the tint has to bring the scene behind it to where that ink clears 4.5:1 - * (skyTheme.test.ts). The browser mixes in sRGB, which gives less contrast over dark terrain than the linear mix the test - * computes, so the value carries margin: measured on the drawn pixels (Chrome, GTX 1070), 0.75 gave 4.49 at Dusk. + * (skyTheme.test.ts). The browser mixes in sRGB, so the test does too (skyTheme.test.ts): over pure black 0.80 gives 4.36 and 0.82 gives 4.55, and + * the darkest terrain drawn is 0.0045, not black; measured on the drawn pixels the footer clears 6.2. */ export const FOOTER_TINT = 0.82; diff --git a/src/app/features/scene/skyTheme.test.ts b/src/app/features/scene/skyTheme.test.ts index dd46117b..3229cf36 100644 --- a/src/app/features/scene/skyTheme.test.ts +++ b/src/app/features/scene/skyTheme.test.ts @@ -162,17 +162,28 @@ describe('regions that sit on the plate', () => { }); describe('the sign-in footer band', () => { - // The footer line is the plate's ink on a band that mixes FOOTER_TINT of the plate's fill over the blurred scene. - // The scene behind it is the terrain: as dark as 0, as light as the sky state's lightest backdrop. Ink contrast is - // lowest at one of those two ends, so both are checked. - const tinted = (fill: string, behind: number) => - FOOTER_TINT * luminance(fill) + (1 - FOOTER_TINT) * behind; + // The footer line is the plate's ink on a band that mixes FOOTER_TINT of the plate's fill over the blurred scene, + // as the browser does: per channel, in encoded sRGB (not in linear light, which flatters dark scenes). The scene + // behind it is the terrain, taken as grey: as dark as 0, as light as the sky state's lightest backdrop. Ink + // contrast is lowest at one of those two ends, so both are checked. + const encode = (l: number) => (l <= 0.0031308 ? 12.92 * l : 1.055 * l ** (1 / 2.4) - 0.055); + const channels = (hex: string) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255); + const hexOf = (c: number[]) => + `#${c + .map((v) => + Math.round(v * 255) + .toString(16) + .padStart(2, '0') + ) + .join('')}`; + const bandOver = (fill: string, behind: number) => + hexOf(channels(fill).map((f) => FOOTER_TINT * f + (1 - FOOTER_TINT) * encode(behind))); it.each(fixed)( '%s: the ink clears AA over the darkest and the lightest scene behind it', (state, id) => { const { Other, OtherText } = SKY[id].bubbles; [0, skyBackdrop(state).lightest].forEach((behind) => { - expect(against(OtherText, tinted(Other, behind))).toBeGreaterThanOrEqual(AA); + expect(contrast(OtherText, bandOver(Other, behind))).toBeGreaterThanOrEqual(AA); }); } ); diff --git a/src/app/pages/auth/styles.css.ts b/src/app/pages/auth/styles.css.ts index d3e60dca..772f02a2 100644 --- a/src/app/pages/auth/styles.css.ts +++ b/src/app/pages/auth/styles.css.ts @@ -92,57 +92,20 @@ globalStyle(`${scene} ${AuthCardContent}`, { // The footer line sits on the terrain, which is dark at its foot in every sky (luminance 0.02 to 0.04 at dusk), where // the sky's dark ink is unreadable. Over the scene it gets a blurred band tinted with the plate's fill and the plate's -// ink and link, faded out at its edges so it reads as haze and not as a bar: the scene shows through and round it. -// The tint (FOOTER_TINT) is checked against the ink in skyTheme.test.ts. +// ink, faded out at its edges so it reads as haze and not as a bar: the scene shows through and round it. A browser +// without color-mix gets the plate's opaque fill, as the chat bubbles do (message/layout/layout.css.ts). The tint (FOOTER_TINT) is checked against the ink in skyTheme.test.ts. const FADE_Y = 'linear-gradient(to bottom, transparent, #000 16px, #000 calc(100% - 16px), transparent)'; const FADE_X = 'linear-gradient(to right, transparent, #000 24px, #000 calc(100% - 24px), transparent)'; +const tint = `color-mix(in srgb, var(--sky-bubble, #0d1a26) ${FOOTER_TINT * 100}%, transparent)`; globalStyle(`${scene} ${AuthFooter}`, { paddingTop: config.space.S500, paddingBottom: config.space.S500, - backgroundColor: `color-mix(in srgb, var(--sky-bubble, #0d1a26) ${ - FOOTER_TINT * 100 - }%, transparent)`, + backgroundColor: ['var(--sky-bubble, #0d1a26)', tint], backdropFilter: 'blur(12px)', - WebkitBackdropFilter: 'blur(12px)', maskImage: `${FADE_Y}, ${FADE_X}`, - WebkitMaskImage: `${FADE_Y}, ${FADE_X}`, maskComposite: 'intersect', - WebkitMaskComposite: 'source-in', - color: 'var(--sky-bubble-text, #b7c7d9)', - borderRadius: config.radii.R500, - // A faint inner edge, so a pane this clear still reads as a surface. - borderColor: 'rgba(183, 199, 217, 0.14)', -}); -globalStyle(`${scene} body:not(.gamecult-theme) ${AuthCard}`, { - vars: { '--tc-link': '#99c7ff' }, -}); -globalStyle(`${scene} ${AuthHeader}`, { - backgroundColor: 'transparent', -}); -globalStyle(`${scene} ${AuthCardContent}`, { - paddingTop: config.space.S500, - paddingBottom: config.space.S400, - gap: config.space.S500, -}); - -// The footer line sits on the terrain, which is dark at its foot in every sky (luminance 0.02 to 0.04 at dusk), where -// the sky's dark ink is unreadable. Over the scene it gets a blurred band tinted with the plate's fill and the plate's -// ink and link, faded out at its edges so it reads as haze and not as a bar: the scene shows through and round it. -// The tint (FOOTER_TINT) is checked against the ink in skyTheme.test.ts. -globalStyle(`${scene} ${AuthFooter}`, { - paddingTop: config.space.S500, - paddingBottom: config.space.S500, - backgroundColor: `color-mix(in srgb, var(--sky-bubble, #0d1a26) ${ - FOOTER_TINT * 100 - }%, transparent)`, - backdropFilter: 'blur(12px)', - WebkitBackdropFilter: 'blur(12px)', - maskImage: - 'linear-gradient(to bottom, transparent, #000 16px, #000 calc(100% - 16px), transparent)', - WebkitMaskImage: - 'linear-gradient(to bottom, transparent, #000 16px, #000 calc(100% - 16px), transparent)', color: 'var(--sky-bubble-text, #b7c7d9)', }); globalStyle(`${scene} ${AuthFooter} a`, {