feat(seasonal): Halloween redesign — webs, a spider, bats, a ghost; no tint (#239)
CI / Build & Quality Checks (pull_request) Successful in 1m36s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 13s
CI / Playwright smoke (e2e) (pull_request) Canceled after 31s

Draft for human review (#239 must be approved and closed by a human).

- Removed the full-screen purple wash, fog and the blurred green "moon" that
  sat over the member list and made it unreadable.
- Corner cobwebs (generated radial threads + sagging rings), thin and
  readable in both themes, smaller on phones.
- A spider on a thread in the empty space under the member list (near the
  top on phones), bobbing and swaying.
- Every 30 s a group of four bats flaps across the top of the screen (a real
  bat silhouette; wing beat without flattening them into dashes).
- Every 50 s a small, see-through ghost rises and fades (outlined so it
  shows on light themes; text stays readable through it).
- Theme-aware ink via --hw-ink/--hw-web: dark silhouettes on light themes,
  pale dusk-violet on dark ones.
- Reduced motion: webs and the spider at rest. Settings swatch: night sky,
  crescent moon with a bat across it, a corner web.

Rendered and inspected: light/dark/phone at the bat and ghost moments,
reduced on desktop and phone, the Settings swatch; 60 fps on vs off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
Lotus CI
2026-09-25 14:50:35 -04:00
co-authored by Claude Opus 5.5
parent 6bb3f2af27
commit 14731f2229
3 changed files with 339 additions and 262 deletions
@@ -1,56 +1,84 @@
import { keyframes } from '@vanilla-extract/css';
import { keyframes, style } from '@vanilla-extract/css';
/**
* Halloween overlay keyframes. Every animation touches ONLY `transform` and
* `opacity` so the compositor can run them on the GPU without layout/paint.
* keyframes() returns the generated animation-name string, applied inline.
* Halloween overlay styles. Motion is transform/opacity only.
*
* [#239] Colours come from two custom properties set on the overlay root so
* the bats/spider stay dark silhouettes on light themes and turn to a pale
* dusk-violet on dark themes (a black bat on a black theme is invisible).
*/
/** Slow breathing of the sickly moon-glow vignette. */
export const animMoonPulse = keyframes({
'0%': { transform: 'scale(1)', opacity: '0.55' },
'50%': { transform: 'scale(1.06)', opacity: '0.8' },
'100%': { transform: 'scale(1)', opacity: '0.55' },
export const halloweenRoot = style({
position: 'absolute',
inset: 0,
vars: {
'--hw-ink': 'oklch(0.24 0.04 300)',
'--hw-web': 'oklch(0.42 0.02 290 / 0.42)',
'--hw-eye': 'oklch(0.72 0.18 55)',
},
selectors: {
'body.dark-theme &, body.butter-theme &': {
vars: {
'--hw-ink': 'oklch(0.72 0.06 300)',
'--hw-web': 'oklch(0.85 0.02 290 / 0.36)',
'--hw-eye': 'oklch(0.8 0.17 60)',
},
},
},
});
/** Low fog band: drifts sideways while gently rising and swelling. */
export const animFogDrift = keyframes({
'0%': { transform: 'translate3d(-12%, 6%, 0) scale(1.1)', opacity: '0' },
'15%': { opacity: '0.5' },
'50%': { transform: 'translate3d(6%, -2%, 0) scale(1.25)', opacity: '0.65' },
'85%': { opacity: '0.45' },
'100%': { transform: 'translate3d(18%, 4%, 0) scale(1.1)', opacity: '0' },
/** A bat group crosses the top of the screen in ~9 s, then a long calm. */
export const animBatFlight = keyframes({
'0%': { transform: 'translate3d(-10vw, 0, 0)', opacity: '0' },
'1%': { opacity: '1' },
'29%': { opacity: '1' },
'30%': { transform: 'translate3d(110vw, -6vh, 0)', opacity: '0' },
'100%': { transform: 'translate3d(110vw, -6vh, 0)', opacity: '0' },
});
/** A bat flaps slowly across the sky in a shallow arc. */
export const animBatGlide = keyframes({
'0%': { transform: 'translate3d(-12vw, 8vh, 0) scale(0.9)', opacity: '0' },
'10%': { opacity: '0.7' },
'45%': { transform: 'translate3d(45vw, -4vh, 0) scale(1)' },
'80%': { transform: 'translate3d(85vw, 6vh, 0) scale(0.95)', opacity: '0.6' },
'100%': { transform: 'translate3d(112vw, 2vh, 0) scale(0.9)', opacity: '0' },
/** Up-and-down bobbing on each bat, so the group doesn't move as one block. */
export const animBatBob = keyframes({
'0%': { transform: 'translate3d(0, 0, 0)' },
'50%': { transform: 'translate3d(0, 14px, 0)' },
'100%': { transform: 'translate3d(0, 0, 0)' },
});
/** The bat's wings beat — fast vertical squash of the wing element. */
export const animWingFlap = keyframes({
'0%': { transform: 'scaleY(1) scaleX(1)' },
'50%': { transform: 'scaleY(0.35) scaleX(1.08)' },
'100%': { transform: 'scaleY(1) scaleX(1)' },
/** Wing beat: the silhouette squashes vertically around the body. */
export const animBatFlap = keyframes({
'0%': { transform: 'scaleY(1)' },
'50%': { transform: 'scaleY(0.62)' },
'100%': { transform: 'scaleY(1)' },
});
/** Will-o'-wisp ember: floats upward, swaying, pulsing in brightness. */
export const animEmberFloat = keyframes({
'0%': { transform: 'translate3d(0, 0, 0) scale(0.7)', opacity: '0' },
'12%': { opacity: '0.85' },
'35%': { transform: 'translate3d(14px, -28vh, 0) scale(1)' },
'65%': { transform: 'translate3d(-12px, -55vh, 0) scale(0.9)', opacity: '0.7' },
'90%': { opacity: '0.25' },
'100%': { transform: 'translate3d(8px, -82vh, 0) scale(0.6)', opacity: '0' },
/** The spider bobs on its thread (thread and spider move together). */
export const animSpiderBob = keyframes({
'0%': { transform: 'translate3d(0, 0, 0)' },
'50%': { transform: 'translate3d(0, 18px, 0)' },
'100%': { transform: 'translate3d(0, 0, 0)' },
});
/** Soft twinkle for embers — independent opacity flicker layered on top. */
export const animEmberTwinkle = keyframes({
'0%': { opacity: '0.6' },
'50%': { opacity: '1' },
'100%': { opacity: '0.6' },
/** A slow sway of the spider itself on the end of the thread. */
export const animSpiderSway = keyframes({
'0%': { transform: 'rotate(-6deg)' },
'50%': { transform: 'rotate(6deg)' },
'100%': { transform: 'rotate(-6deg)' },
});
/**
* A small ghost rises from the lower part of the screen, drifting sideways,
* and fades out — ~8 s in a 50 s cycle.
*/
export const animGhost = keyframes({
'0%': { transform: 'translate3d(0, 0, 0) scale(0.9)', opacity: '0' },
'3%': { opacity: '0.8' },
'8%': { transform: 'translate3d(6vw, -20vh, 0) scale(1)' },
'13%': { opacity: '0.75' },
'16%': { transform: 'translate3d(-2vw, -42vh, 0) scale(1.05)', opacity: '0' },
'100%': { transform: 'translate3d(-2vw, -42vh, 0) scale(1.05)', opacity: '0' },
});
/** The ghost's tail wisps, a gentle wobble. */
export const animGhostWobble = keyframes({
'0%': { transform: 'rotate(-4deg)' },
'50%': { transform: 'rotate(4deg)' },
'100%': { transform: 'rotate(-4deg)' },
});