feat(season): Halloween skin for the whole site - #384
Open
TheMeinerLP wants to merge 5 commits into
Open
TheMeinerLP wants to merge 5 commits into
TheMeinerLP wants to merge 5 commits into
Conversation
Extend scripts/md3-tokens.mjs with a SEASONS register: each season is a set of seeds run through the same Fidelity recipe as the base scheme and written to assets/css/seasons.css as one html[data-season="…"] block. The block redeclares every MD3 role and custom colour, plus the fixed brand colours outside the roles (gradients, the glow's raw magenta and cyan), so nothing keeps its base value mid-costume. seasons.css is a file of its own because the token tests parse tailwind.css whole and a later declaration wins there. Tests hold the file to the generator, require every role and decoration colour, and run the contrast pairs against each season. Halloween: violet, pumpkin and poison green, every pair at AA. Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
A new season layer decides once, on the server, whether a season is in effect: NUXT_PUBLIC_SEASON first (the kill switch that needs no deploy), then the calendar in Berlin time. Halloween runs 20 October to 2 November. app.vue sets data-season, swaps favicon and theme-color, and renders a decoration layer: cobwebs in the top corners and, from md up and only when motion is welcome, three drifting bats. The layout hands the season's logo to the navigation, so the two domains stay apart. ?season=halloween / ?season=none preview or hide a season in the browser after hydration: the cached HTML may not read the query. The base favicon moves into app.head with a key. nuxt-seo-utils added its own icon link otherwise, so every page already carried two. Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
Add the add-halloween-season OpenSpec change (proposal, specs, design, tasks), carve calendar-driven seasons out of the MD3 design's "no runtime themes" non-goal, and document seasons in AGENTS.md and the tailwind-design skill. Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
launchpad | 3653a8e | Commit Preview URL Branch Preview URL |
Sep 28 2026, 01:20 PM |
scripts/cross-browser-colors.mjs compiles tailwind.css with its own stylesheet loader, which looked up every @import inside the tailwindcss package. The new `@import './seasons.css'` therefore failed with ENOENT. Resolve relative imports next to tailwind.css, as tests/helpers/theme.ts already does. Claude-Session: https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s
* feat(season): add winter, new year and spring to the register 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 * feat(season): split the decoration per season 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 * feat(season): add seasonal logos and favicons 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 * test(season): run colour effect checks per season 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 * docs(season): list all seasons in agents guide 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 * feat(season): let snowflakes and petals fall across the viewport 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 * feat(season): tidy the season register and decoration frame 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 * feat(season): show corner decoration below the navigation 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 * feat(season): draw proper six-fold snowflakes 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 * feat(season): give the new year a midnight surface 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 * feat(season): calm the new year and spring accents 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 * feat(season): recolour the seasonal logos throughout 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 * feat(season): light a gentle firework for the new year 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 * feat(season): pick a calmer, clearer new year gold 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 * feat(season): draw a real firework and a sparkle cluster for new year 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 * feat(season): make the spring branch bloom 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
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
A calendar-driven Halloween skin for the whole site, 20 October – 2 November (Berlin time).
scripts/md3-tokens.mjsgains aSEASONSregister. Each season's seeds (Halloween: violet#5B2A86, pumpkin#FF7518, poison green#7CB518) go through the same Fidelity recipe as the base scheme and land inassets/css/seasons.cssas onehtml[data-season="halloween"]block. The block redeclares every role, every custom colour and the fixed brand colours outside the roles (gradients, the connect box's magenta/cyan glow).seasonlayer.useSeason()decides once, on the server, fromNUXT_PUBLIC_SEASONand then the calendar.app.vuesetsdata-season, swaps favicon andtheme-color, and rendersSeasonDecor.mdup, up to three drifting bats, and only undermotion-safe. The layer isaria-hidden,pointer-events-noneand sits below the navigation (z-30).?season=halloweenor?season=noneapplies in the browser after hydration. The cached HTML may not read the query (request-independent-render.spec.ts).NUXT_PUBLIC_SEASON=noneturns the season off in production without a deploy.Rationale
The OpenSpec change
add-halloween-seasonholds the plan: proposal, two specs, design and tasks. The central choice is to generate a season from seeds instead of hand-picking hex values. That makes a season complete by construction, and the existing contrast pairs cover it: every pair passes AA in both schemes.seasons.cssis a separate file on purpose. The token tests parsetailwind.csswhole and a later declaration wins, so a season written there would be read back as the base scheme.Also in here
nuxt-seo-utilsadded its own icon link forpublic/favicon.svgunlessapp.headalready had one, so every page carried tworel="icon"links before this change. The base icon now lives inapp.headwithkey: 'favicon', andapp.vuereplaces it by key.adopt-md3-design-systemdesign now says that calendar-driven seasons are an exception to its "no runtime themes" non-goal.Known, not caused by this change
SiteFooter.vuecomputes the copyright year withnew Date()on both sides. A browser clock in a different year than the server produces a hydration warning on every page, with or without a season.color-contrastfails on the home page'sbg-white/70pill, with and without the season.Verification
pnpm test: 84 files, 526 testspnpm buildpnpm quality: at the baselinepnpm preview:data-seasonfor any query.?season=halloweenrecolours the page after load and swaps favicon,theme-colorand logo.rel="icon".main: desktop and mobile scores were identical with and without?season=halloween(desktop a11y 96, perf 99).Screenshots (light, dark, phone, logo comparison) follow as a comment.
https://claude.ai/code/session_012ZzvomAnwjAS3et96Avp7s