feat(season): add winter, new year and spring skins - #386
Merged
Merged
Conversation
Three new seasons join Halloween: winter (1-26 Dec), new-year (27 Dec - 6 Jan, wrapping the year end) and spring (20 Mar - 20 Apr), each with its own colour seeds in scripts/md3-tokens.mjs and generated block in assets/css/seasons.css. The register is sorted by calendar start and a new pure overlappingSeasons() brute-forces a leap year to guarantee no two windows claim the same day, adjacent windows excepted. The halloween block in seasons.css stays byte-identical. Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
SeasonDecor becomes a dispatcher: it keeps the shared frame (aria-hidden, pointer-events-none, fixed inset-0 z-30) and renders one explicitly-imported child per season id. The halloween content moves unchanged into SeasonDecorHalloween. New SeasonDecorWinter (ice crystals + up to six falling snowflakes), SeasonDecorNewYear (stars + up to five twinkling, flash-free light points) and SeasonDecorSpring (blossom branches + up to four drifting petals) follow the same motion-safe:md:block opt-in as the halloween bats. Two new @theme animations back the moving pieces: --animate-season-fall (translate/rotate with an opacity fade at the cycle's end) and --animate-season-twinkle (opacity/scale, 4.5s per cycle, nowhere near the WCAG 2.3.1 flash limit). Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
Each new season gets its own logo and favicon, derived from public/images/logo.svg the way halloween's are: every fill hue is remapped from the base brand's primary/secondary/tertiary/brand-orange/ brand-purple seeds to the season's own, saturation and lightness kept, so the shading survives. The logo also carries one small motif — an ice crystal for winter, a sparkle for new-year, a blossom for spring — the favicon carries none. Both are SVGO-optimised and smaller than the base assets they replace. navigation-logo.spec.ts now runs over every registered season instead of hard-coding halloween. Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
Replaces the halloween-only colour-effect block in tests/design-system/seasons.spec.ts with a COLOUR_EFFECT table keyed by season id, covering the hue ranges from each season's spec. A completeness check requires a table row for every registered season, the light-scheme luminance check now runs for all of them, and winter keeps its own 30°-from-error assertion. Two new tests hold the --animate-season-* tokens to the >=3s cycle and transform/opacity-only keyframes that spec seasonal-theming requires. Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
AGENTS.md's seasonal-skins section and the NUXT_PUBLIC_SEASON comment in nuxt.config.ts now list all four seasons and their windows instead of only halloween. Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
season-fall moved a flake by a percentage of its own box (~130% of a 12px element), so it barely drifted. The end state now translates 45vh — viewport-relative, not element-relative — and the start moves to -2rem so the flake enters from just above the frame. Rotate and the opacity fade at both ends of the cycle are unchanged, transform/opacity only, same 24s duration. Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
SEASONS now formats as a normal multi-line array instead of the one-element-per-line-with-no-indent shape ESLint's autofix left behind. overlappingSeasons computes everyDayOfTheYear() once per call instead of once per pair. SeasonDecor's doc comment no longer claims a season missing from DECORATIONS is a build error — the mapping is not typed against season ids, so a gap there renders a blank frame and is caught by the "mapping" test in season-decor.spec.ts instead. Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
Halloween's webs, new year's stars and spring's blossom branches sat at -top-1 (halloween, spring) or -top-1 (new year), largely covered by the 64px navigation bar (z-50). They now start at top-16, below it, and new year's and spring's corner motifs grow to h-28/sm:h-36 so they still read clearly. New year's twinkling light points switch from bg-tertiary (silver, close to no chroma — read as grey dust) to bg-secondary (gold). Winter's crystal keeps its old offset for now; its own position and size need more than a one-line move (see the next commit). Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
The winter crystal and falling snowflake read as arrows and an asterisk: the crystal's arms sat on an uneven ~33° grid with V-shaped tips, and the flake was three plain strokes. Both are now a proper six-fold shape — arms on an exact 60° grid from the centre, with side branches partway along each arm, shortening towards the tip. The corner crystal also moves below the navigation (top-16, following the other seasons) and out towards the viewport edge (-left-6/-right-6 instead of -1, capped at lg:h-24/w-24) so it no longer overlaps the carousel frame in the ~1024-1280px range where the content gutter (lg:px-8) is at its narrowest. The new year star and spring branch were checked for the same kind of geometry problem and rendered cleanly — no change needed there. Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
new-year's dark surface came out #141317 — practically neutral black, no midnight-violet undertone, because Fidelity derives its neutral palette from the primary seed but flattens its chroma almost to zero. scripts/md3-tokens.mjs gains an optional per-season neutralChroma: when set, the neutral and neutral-variant palettes are built from TonalPalette.fromHueAndChroma(primary's HCT hue, chroma) instead of Fidelity's own. The neutral-variant palette uses chroma + 4, matching the small extra tint Fidelity's own neutral-variant carries over neutral. Seasons without the parameter are byte-identical — verified against halloween and winter. new-year sets neutralChroma: 16, tuned by comparing a few values (8-12 stayed too grey, 20+ started to compete with on-surface); its dark surface is now #141123 and themeColor follows. Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
new-year's gold (#D4AF37, HCT chroma 47.8) and spring's blossom pink (#E86A9A, chroma 60.1) generated a secondary-container so saturated the active navigation pill read as screaming yellow/pink in light mode, even though their chroma looked unremarkable next to the base secondary's (51.2). Both seeds keep their HCT hue (inside the spec's hue bands: new-year 35-55°, spring 320-355°) with chroma roughly halved and a slightly lower tone. new-year's brand-orange follows its secondary, as it did before. Resulting secondary-container / on-secondary-container: new-year light #f7e1a6 / #736334 dark #56481b / #cbb780 spring light #ffb9ce / #7b4658 dark #6c394b / #eaa5bb Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
Winter, new year and spring's logos and favicons kept patches of the base mark's orange, magenta and violet, and each covered five different seed hues (primary/secondary/tertiary/brand-orange/ brand-purple), so spring in particular read as a rainbow rather than a season. Recoloured from scratch: every fill is bucketed into one of three regions along the feather (base/quill, middle vane, tip — split by each path's own transform translation, verified against a debug render) and mapped to that region's primary/secondary/tertiary tonal palette at a tone derived from the fill's own lightness. That leaves each logo with one coherent 2-3 hue story — winter frost-blue-to-lilac with a gold accent, new year midnight-violet-to-silver with a gold accent, spring green-to-yellow with a blossom-pink accent — and no leftover base hue survives, verified by eye against a side-by-side render. Each logo's motif (a snow crystal, a sparkle, a blossom) now sits on the feather's tip instead of floating beside it. Favicons keep the recolour without a motif, as before. Both optimised with SVGO, all six files smaller than the 26.47 KiB base asset (20.8-21.1 KiB). Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
Up to four firework buckets join the corner stars and light points (spec new-year-season, "Neujahrs-Deko mit sanftem Feuerwerk"): a ten-spoke spark ring per bucket, sitting at the left/right page edges outside the reading column (further out again from lg), colour cycling through secondary/tertiary/brand-orange — never red, never a full-surface brightening. --animate-season-burst (tailwind.css) opens a bucket over 15% of a 7s cycle (1.05s, clearing the spec's 1s floor for the fade-in alone) via scale + opacity only, then glimmers it out with a slight droop and holds it fully faded for the rest of the cycle. A 1.75s stagger across the four buckets means at most one is ever open. Each bucket stays within h-28/lg:h-36 (112px/144px), under the spec's 160px cap. Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
The previous calming pass halved chroma (~24) to fix a screaming secondary-container, but that made the rendered `secondary` role — used broadly for buttons, chips and pills — read as muddy khaki/olive rather than gold: a dark tone at low chroma desaturates into brown, it doesn't calm down. secondary keeps roughly its original chroma (#B8922E, HCT hue 88, chroma 42.6 — close to winter's own gold at 48.6, which nobody objected to). Only the seed's own moved; the rendered secondary role is nearly identical to before (#775a00 / #ecc159), and the container is a touch calmer than the original #D4AF37's (#fed268 / #8c6b00 vs. #fed65b / #af8d11). Custom brand-orange gets its own seed instead of following secondary: #E2B64A, a bright festive gold reserved for decoration (fireworks, stars). It isn't harmonised (customColor blend:false), so it can glow without dragging a UI role's text contrast down with it — though HCT's gamut caps how much brighter it can get at this hue before clipping, so in practice it renders close to secondary in the light scheme and more clearly brighter in dark (#f0c048 vs. secondary's #ecc159). Hue unchanged at ~45° (rendered), inside the spec's 35-55° band either way; contrast and colour-effect tests stay green without a baseline change. Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
The firework buckets were ten lines all meeting at a centre hub, each ending in a dot — a ball-and-stick molecule model, not a firework, and coloured khaki from the old gold seed. Redrawn as sixteen rays on a 22.5° grid starting away from the centre (inner radius ~30% of the viewBox), alternating long and short, with a small dot just beyond each long ray's tip; no line crosses the centre. Two buckets use the brighter, unharmonised brand-orange, one each tertiary and secondary for variety. Bucket placement now keeps to the page gutter properly: left-[max(0.5rem,2%)]/right-[...] instead of a bare 2% that could drift, sized h-24/lg:h-28/xl:h-36 (all ≤160px, spec's cap), vertical spread 25-75%. The corner star shrinks from h-28/sm:h-36 to h-16/sm:h-20 and moves to text-brand-orange/50 (from text-secondary/25, which read as one big khaki cutout), with two small satellite stars alongside it so the corner reads as a sparkle cluster. Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
The corner branch was a green twig with plain green dots along it — recognisable as a vine, not a blossom branch. Each dot is now a five-petal blossom (five small circles on a pentagon around a centre, petals in text-secondary/60, a tiny text-tertiary-container centre — tertiary itself has the same dark-in-light-scheme problem secondary had before the gold fix, tertiary-container stays a steady gold in both schemes), plus two small leaves along the curve. The falling petals switch from text-secondary/40 to text-secondary-container/70: secondary is tuned dark for text contrast in the light scheme (#854e61, read as muddy mauve-grey against the pale green surface), where secondary-container stays a clear pink (#ffb9ce light, a dustier #6c394b in dark — still rose-toned, not grey). Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
launchpad | 4bd6c45 | Commit Preview URL Branch Preview URL |
Sep 24 2026, 11:37 AM |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Extends the seasonal-theming mechanism from #384 with three more seasons, each Berlin-time and non-overlapping:
Each season gets its own decoration component (
SeasonDecorWinter/SeasonDecorNewYear/SeasonDecorSpring), dispatched fromSeasonDecor.vuealongside the existing Halloween one. A new pureoverlappingSeasons()check guards the register against two seasons ever claiming the same calendar day. The colour generator (scripts/md3-tokens.mjs) gains an optional per-seasonneutralChroma, used fornew-yearso its dark surface reads midnight-violet instead of near-black. Logos and favicons for all three new seasons are recoloured from the base mark (region + lightness based, no leftover base hues). New year also gets a gentle firework: bursts fade in over ≥1s and never flash more than once per second (WCAG 2.3.1).Rationale
The Halloween skin (#384) proved the mechanism (register, generator, decoration dispatcher, preview/kill-switch) works for one season; this change makes it season-agnostic and ships three more so the site is dressed for winter, the new year and spring, not just October. Winter in particular must be in production before 1 December 2026.
Depends on
#384 (
feat/halloween-season) — this PR is based on it and should be rebased ontomainonce #384 merges.How to test
Preview deployment URL: see the Cloudflare Pages check/comment on this PR once it appears.
?season=winter,?season=new-year,?season=springto preview each skin ahead of its window;?season=noneto force it off.md) and only whenprefers-reduced-motionis not set to reduce — with reduced motion, nothing should move.Checks
pnpm test: 594/594 passingpnpm quality: ESLint/TypeScript error counts unchanged fromquality-baseline.json(no rise)pnpm build: succeedsOpen
?season=winter/new-year/spring) are still pending.Implements the OpenSpec change
add-winter-new-year-spring-seasons.https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s