Merge pull request 'auth: a blurred, tinted, soft-edged band behind the sign-in footer (4.5:1 in Day, Dusk, Night)' (#61) from void/auth-footer-tint into dev
Some checks failed
Rat dev preview / Build dev preview (push) Has been cancelled

This commit is contained in:
Void 2026-11-10 03:45:34 +00:00
commit 30ae5154f5
3 changed files with 61 additions and 1 deletions

View file

@ -7,3 +7,12 @@ export const SKY_PLATE_FALLBACK = {
text: '#b7c7d9',
link: '#99c7ff',
} as const;
/**
* 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, 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;

View file

@ -7,7 +7,7 @@ import { PageContent, PageNavContent } from '../../components/page/style.css';
import { CallStatus } from '../call-status/styles.css';
import { MembersDrawer } from '../room/MembersDrawer.css';
import { RoomViewTyping } from '../room/RoomViewTyping.css';
import { SKY_PLATE_FALLBACK } from './skyPlate';
import { FOOTER_TINT, SKY_PLATE_FALLBACK } from './skyPlate';
import { SkyPlate, plateRegions } from './skyPlate.css';
import { getResolvedSkyState, setSkyClock, skyBackdrop } from './skyState';
import type { SkyBackdrop } from './skyState';
@ -160,3 +160,31 @@ describe('regions that sit on the plate', () => {
expect(plateRegions.some((r) => r.includes(':not(:has('))).toBe(true);
});
});
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,
// 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(contrast(OtherText, bandOver(Other, behind))).toBeGreaterThanOrEqual(AA);
});
}
);
});

View file

@ -1,5 +1,6 @@
import { globalStyle, style } from '@vanilla-extract/css';
import { DefaultReset, color, config, toRem } from 'folds';
import { FOOTER_TINT } from '../../features/scene/skyPlate';
export const AuthLayout = style({
minHeight: '100%',
@ -88,3 +89,25 @@ globalStyle(`${scene} ${AuthCardContent}`, {
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, 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: ['var(--sky-bubble, #0d1a26)', tint],
backdropFilter: 'blur(12px)',
maskImage: `${FADE_Y}, ${FADE_X}`,
maskComposite: 'intersect',
color: 'var(--sky-bubble-text, #b7c7d9)',
});
globalStyle(`${scene} ${AuthFooter} a`, {
color: 'var(--sky-bubble-text, #b7c7d9)',
});