diff --git a/frontend/src/components/PlayerDashboardFullPage.tsx b/frontend/src/components/PlayerDashboardFullPage.tsx
index a86f3958..c7ef7e21 100644
--- a/frontend/src/components/PlayerDashboardFullPage.tsx
+++ b/frontend/src/components/PlayerDashboardFullPage.tsx
@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import { useLiveData } from '../hooks/useLiveData';
import { PlayerDashboardContent } from './windows/PlayerDashboardWindow';
+import { MidsummerBanner } from './midsummer/MidsummerBanner';
/**
* Fullscreen "Player Dashboard" page — rendered when the React app loads
@@ -37,6 +38,7 @@ export const PlayerDashboardFullPage: React.FC = () => {
return (
+
👥 Player Dashboard
{count} online
diff --git a/frontend/src/components/map/MapLayout.tsx b/frontend/src/components/map/MapLayout.tsx
index 73b11215..800b665c 100644
--- a/frontend/src/components/map/MapLayout.tsx
+++ b/frontend/src/components/map/MapLayout.tsx
@@ -7,6 +7,7 @@ import { WindowRenderer } from '../windows/WindowRenderer';
import { RareNotification } from '../effects/RareNotification';
import { DeathNotification } from '../effects/DeathNotification';
import { usePlayerColors } from '../../hooks/usePlayerColors';
+import { MidsummerBanner } from '../midsummer/MidsummerBanner';
import type { DashboardState } from '../../hooks/useLiveData';
interface Props {
@@ -41,6 +42,7 @@ export const MapLayout: React.FC = ({ data }) => {
return (
+
{
+ const { enabled } = useMidsummer();
+
+ useEffect(() => {
+ if (!enabled) return;
+ if (sessionStorage.getItem(CONFETTI_FLAG)) return;
+ sessionStorage.setItem(CONFETTI_FLAG, '1');
+ if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
+ burstConfetti();
+ }
+ }, [enabled]);
+
+ if (!enabled) return null;
+ return (
+
+ 🐸 Glad midsommar! 🌼 Små grodorna, små grodorna… 🥂
+
+ );
+};
diff --git a/frontend/src/components/midsummer/confetti.ts b/frontend/src/components/midsummer/confetti.ts
new file mode 100644
index 00000000..04c400f8
--- /dev/null
+++ b/frontend/src/components/midsummer/confetti.ts
@@ -0,0 +1,17 @@
+const EMOJIS = ['🐸', '🌼', '🌸', '🥂', '🌿'];
+
+/** One-shot falling-emoji burst. Self-removes after the animation. */
+export function burstConfetti(count = 28): void {
+ const layer = document.createElement('div');
+ layer.className = 'ms-confetti';
+ for (let i = 0; i < count; i++) {
+ const p = document.createElement('span');
+ p.textContent = EMOJIS[i % EMOJIS.length];
+ p.style.left = Math.floor(Math.random() * 100) + 'vw';
+ p.style.animationDelay = (Math.random() * 0.6).toFixed(2) + 's';
+ p.style.fontSize = 12 + Math.floor(Math.random() * 14) + 'px';
+ layer.appendChild(p);
+ }
+ document.body.appendChild(layer);
+ window.setTimeout(() => layer.remove(), 4200);
+}
diff --git a/frontend/src/styles/midsummer.css b/frontend/src/styles/midsummer.css
index a4616e50..8dbb8a95 100644
--- a/frontend/src/styles/midsummer.css
+++ b/frontend/src/styles/midsummer.css
@@ -106,3 +106,36 @@
line-height: 1;
pointer-events: none;
}
+
+.ms-banner {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 50;
+ margin-top: 6px;
+ padding: 4px 16px;
+ border-radius: 14px;
+ background: rgba(20, 64, 31, 0.92);
+ border: 1px solid #7ed957;
+ color: #eafbe0;
+ font-size: 0.8rem;
+ white-space: nowrap;
+ pointer-events: none;
+}
+.ms-confetti {
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ overflow: hidden;
+ z-index: 999998;
+}
+.ms-confetti span {
+ position: absolute;
+ top: -32px;
+ line-height: 1;
+ animation: ms-fall 3.6s linear forwards;
+}
+@keyframes ms-fall {
+ to { transform: translateY(112vh) rotate(360deg); opacity: 0.25; }
+}