# Midsummer theme — "Små grodorna" — design spec Date: 2026-06-19 Repo: MosswartOverlord (React frontend in `frontend/`) Status: approved design, ready for implementation plan ## Goal A "SUPER epic" Swedish-midsummer takeover of the Overlord dashboard, themed around *Små grodorna* (the little frogs) — fitting because Asheron's Call mosswarts are frog-men. Full visual takeover with a dancing maypole, frog + flower-crown player markers, a "Glad midsommar!" banner, and a frog-hop easter egg. Per-browser 🐸 toggle, **default ON**, with an unmuted jingle. ## Scope In scope: the React frontend only (`frontend/`). The classic v1 frontend (`static/classic/`) and legacy vanilla pages are dead and explicitly NOT themed. Out of scope: backend changes, DB, the plugin. No server-side flag — the theme is a pure client concern toggled per browser. ## Approach: scoped overlay, not a rewrite A single attribute `data-midsummer` on `` (`document.documentElement`) gates the entire theme. All midsummer styling lives in a NEW stylesheet `frontend/src/styles/midsummer.css`, every rule scoped under `:root[data-midsummer] …`, layered on top of the untouched base `map-layout.css`. Removing the attribute fully reverts the UI — the base theme remains the single source of truth. Rejected alternative: swapping in a full second stylesheet (à la the old `christmastheme/`). Too heavy and it drifts from the base theme on every future change. The scoped overlay avoids duplication. ## State & toggle - `frontend/src/hooks/useMidsummer.ts` — a hook backed by a tiny context so every component reads one source of truth. - Reads `localStorage["mo-midsummer"]`; **absent ⇒ enabled (default ON)**. Only the literal string `"off"` disables it. - On change, sets/removes `data-midsummer` on `document.documentElement` and persists `"on"`/`"off"`. - Exposes `{ enabled, toggle, soundOn, toggleSound }`. - Provider mounted at the top of `App.tsx` so both the default app and the `?view=dashboard` page inherit it. - 🐸 toggle button added to the sidebar tool-links (`components/sidebar/SidebarWindowButtons.tsx`), label reflects state. ## Components (all gated by `enabled`) ### 1. Maypole — `components/midsummer/Maypole.tsx` - Rendered as a sibling of `PlayerDots` **inside `.ml-map-group`** in `MapView.tsx` (so it pans/zooms with the world automatically), only when `imgSize.w > 0 && enabled`. - Positioned via `worldToPx(MAYPOLE_EW, MAYPOLE_NS, imgW, imgH)`. **Default location: map center** (the midpoint of the Dereth image / a central hub). Coordinate is a single named constant, trivial to move. - Visual: a midsommarstång (pole + flowered cross-bar + ribbons) built in CSS/SVG — no image asset — so it inherits theme colors and stays crisp at any zoom. - Carries its OWN ring of CSS-animated decorative frogs circling the pole (keyframe rotation on a wrapper). The spectacle is independent of live data, so it always looks alive even with nobody online. Real player dots near the pole read as "joining the dance" by proximity. - Pure CSS animation (transform-based) for 60fps; respects `prefers-reduced-motion` (ring holds still). ### 2. Frog + flower-crown player dots - No change to `PlayerDots.tsx` data flow. Under `[data-midsummer]`, `midsummer.css` decorates `.ml-dot` with a wildflower-crown ring via a `::before` pseudo-element, and turns the hovered/selected dot (`.ml-dot-selected`) into a little frog (pseudo-element eyes + green body). - Falls back gracefully: if a dot has an inline `backgroundColor`, the crown sits on top; the frog variant overrides the fill only on select/hover. ### 3. Glad midsummer banner + confetti — `components/midsummer/MidsummerBanner.tsx` - A festive top strip ("Glad midsommar! 🐸") rendered at the app shell level (in `MapLayout.tsx`, and on the dashboard page) when `enabled`. - One-shot snaps-glass / flower confetti burst on first load **per session** (guarded by `sessionStorage`), a lightweight self-removing CSS-particle effect (no library). Honors `prefers-reduced-motion` (skips the burst). ### 4. Frog-hop easter egg (replaces the rickroll) - `Sidebar.tsx:62-80` currently appends a fullscreen `/rick.mp4` overlay + shake on title click. Replace it with a *Små grodorna* hop: clicking the title toggles a body class running a bounce/hop keyframe across the UI for a few seconds, with frogs hopping across the screen. Self-cleans; clicking again re-triggers without stacking. The `rick.mp4` reference is removed. - This easter egg is active regardless of the theme toggle (it's a gag, not a palette), but uses the same frog assets. ### 5. Jingle — `hooks/useMidsummerSound.ts` - Plays the *Små grodorna* melody **once** (not looping — a looping jingle on a left-open dashboard is grating). - **No audio asset**: the melody is synthesized with WebAudio oscillators from the public-domain folk tune (note frequencies in code). This removes any licensing question and ships nothing for the service worker to cache. - Browser reality: WebAudio cannot start before a user gesture, so the tune fires on the **first user interaction (any click) or the moment the 🐸/🔊 control is used** — never silently on page-paint. A 🔇 control disables it; preference persisted (`soundOn`, default on). - Single module-level `AudioContext`, reused and `resume()`d on gesture — no per-play allocation (the audit flagged per-notification `AudioContext` leaks elsewhere; don't repeat that pattern). ## File plan New: - `frontend/src/hooks/useMidsummer.ts` (context + hook) - `frontend/src/hooks/useMidsummerSound.ts` - `frontend/src/styles/midsummer.css` - `frontend/src/components/midsummer/Maypole.tsx` - `frontend/src/components/midsummer/MidsummerBanner.tsx` - `frontend/src/components/midsummer/FrogToggle.tsx` - `frontend/src/components/midsummer/confetti.ts` (tiny helper) - (no audio asset — jingle is WebAudio-synthesized) Edited: - `App.tsx` — wrap in `MidsummerProvider`; import `midsummer.css`. - `components/map/MapView.tsx` — mount `` inside `.ml-map-group`. - `components/map/MapLayout.tsx` — mount ``. - `components/map/Sidebar.tsx` — replace rickroll block with frog-hop. - `components/sidebar/SidebarWindowButtons.tsx` — add 🐸 toggle (+ 🔊). - `components/PlayerDashboardFullPage.tsx` — render banner/toggle so the new-tab dashboard matches. ## Decisions (locked) - Maypole location: **map center** (named constant, easy to relocate). - Jingle: **plays once**, unmuted, fires on first gesture. - Toggle default: **ON**, per-browser via `localStorage`. - Auto-date-gating: NOT implemented (user chose manual toggle). A future enhancement could default the toggle from the date (~Jun 19–25). ## Deploy `bash deploy-frontend.sh && git add static/ && git commit && git push`, then `git pull` on the host (bind-mounted `static/`). No container restart. No new runtime assets (jingle is synthesized), so the service worker needs no changes. ## Testing / verification - Toggle off ⇒ `data-midsummer` removed, UI identical to today (base theme intact). Toggle on ⇒ full takeover. Preference survives reload. - Maypole sits at map center and stays pinned to the world through pan/zoom; frogs animate; `prefers-reduced-motion` stops motion. - Player dots show crowns; hover/select shows frog. - Banner shows once per session; confetti does not re-fire on every render. - Easter egg hops and self-cleans; no `rick.mp4` request remains. - Jingle plays once after first interaction; 🔇 stops it; no audio-context leak across repeated toggles. - `npm run build` succeeds; bundle includes the new chunk. ## Risks / caveats - **Default-on + shared dashboard**: anyone opening it during the demo gets the full theme. That's intended; the 🐸 toggle is one click to calm it. - **Unmuted autoplay** is gesture-gated by browsers — communicated above; not a bug. - **Animation perf**: the map already re-renders on high-frequency telemetry; keep all midsummer animation pure-CSS/transform and outside React state so it doesn't add re-renders. Cap decorative frog count. - **Asset licensing**: use a clearly royalty-free / public-domain audio clip and note its source in the plan.