diff --git a/docs/superpowers/plans/2026-04-11-phase1-engagement-ux.md b/docs/superpowers/plans/2026-04-11-phase1-engagement-ux.md
new file mode 100644
index 0000000..5195894
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-11-phase1-engagement-ux.md
@@ -0,0 +1,1720 @@
+# Phase 1: Engagement & UX-Polish — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Transform the WM 2026 Tippspiel from functional to fun — new dashboard, bottom nav, smart spielplan, emotional animations, rich profile.
+
+**Architecture:** Mobile-first SPA. React + Vite frontend served as static files by Express backend. CSS Modules with CSS custom properties for theming. Raw PostgreSQL queries via `pg` pool. No test framework exists — tasks include build verification via `tsc` and manual smoke tests.
+
+**Tech Stack:** React 18, React Router v6, CSS Modules, Vite 5, Express 4, PostgreSQL (Supabase), `canvas-confetti` (new), `framer-motion` (new).
+
+**Design Spec:** `docs/superpowers/specs/2026-04-11-phase1-engagement-ux-design.md`
+
+---
+
+## File Structure Overview
+
+### New Files
+| File | Purpose |
+|------|---------|
+| `frontend/src/pages/DashboardPage.tsx` | New startseite with hero, stats, nudges |
+| `frontend/src/pages/DashboardPage.module.css` | Dashboard styles |
+| `frontend/src/components/BottomNav.tsx` | Mobile bottom navigation bar |
+| `frontend/src/components/BottomNav.module.css` | Bottom nav styles |
+| `frontend/src/components/ConfettiReveal.tsx` | Punkte-reveal overlay with confetti |
+| `frontend/src/components/ConfettiReveal.module.css` | Reveal animation styles |
+| `frontend/src/components/Toast.tsx` | Toast notification component |
+| `frontend/src/components/Toast.module.css` | Toast styles |
+| `frontend/src/components/StatsRing.tsx` | Donut chart SVG for profile |
+| `frontend/src/components/StatsRing.module.css` | Stats ring styles |
+| `frontend/src/hooks/useStreak.ts` | Hook to fetch/display streak |
+| `frontend/src/hooks/useRankChange.ts` | Hook to detect rank changes (localStorage) |
+| `frontend/src/hooks/useRevealQueue.ts` | Hook to queue unseen punkte-reveals |
+| `backend/src/routes/dashboard.ts` | GET /api/dashboard — bundled dashboard data |
+
+### Modified Files
+| File | Changes |
+|------|---------|
+| `frontend/src/App.tsx` | Remove AgentChat, add BottomNav, new routes, slim header |
+| `frontend/src/App.module.css` | Slim header, remove old nav, add bottom-nav spacing |
+| `frontend/src/index.css` | New animation tokens, streak colors |
+| `frontend/src/api/client.ts` | Add `getDashboard()`, `getStreak()`, remove agent methods |
+| `frontend/src/pages/MatchesPage.tsx` | Smart Sections (Heute/Morgen/Woche), remove stage filter buttons |
+| `frontend/src/pages/MatchesPage.module.css` | Section headers, accordion styles |
+| `frontend/src/components/MatchCard.tsx` | 5 visual states, countdown, streak badge |
+| `frontend/src/components/MatchCard.module.css` | State-specific styles (glow, pulse, grayout) |
+| `frontend/src/components/TipModal.tsx` | Remove Expertenblick, add success animation |
+| `frontend/src/components/TipModal.module.css` | Slimmer modal, success pulse |
+| `frontend/src/pages/ProfilePage.tsx` | Stats ring, tip history, fun stats |
+| `frontend/src/pages/ProfilePage.module.css` | Rich profile layout |
+| `frontend/src/pages/LeaderboardPage.tsx` | Minor: streak badge on entries |
+| `backend/src/index.ts` | Mount dashboard route, remove agent route |
+| `backend/src/routes/matches.ts` | Add streak count to response |
+| `backend/src/routes/tips.ts` | Return streak after tip submission |
+
+### Removed Files
+| File | Reason |
+|------|--------|
+| `frontend/src/components/AgentChat.tsx` | KI-Agent removed per spec |
+| `frontend/src/components/AgentChat.module.css` | KI-Agent removed per spec |
+| `backend/src/routes/agent.ts` | Agent endpoint removed per spec |
+
+---
+
+## Task 1: Cleanup — Remove AgentChat & Agent Route
+
+Remove the KI-Agent chat widget and backend route. This is the safest first step — pure deletion with no dependencies.
+
+**Files:**
+- Delete: `frontend/src/components/AgentChat.tsx`
+- Delete: `frontend/src/components/AgentChat.module.css`
+- Delete: `backend/src/routes/agent.ts`
+- Modify: `frontend/src/App.tsx`
+- Modify: `backend/src/index.ts`
+
+- [ ] **Step 1: Remove AgentChat from App.tsx**
+
+In `frontend/src/App.tsx`, remove the import and usage:
+
+```tsx
+// DELETE these lines:
+import AgentChat from './components/AgentChat';
+// ...
+{/* Fußball-Experte Chat-Widget – immer sichtbar */}
+
+```
+
+- [ ] **Step 2: Delete AgentChat component files**
+
+```bash
+rm frontend/src/components/AgentChat.tsx frontend/src/components/AgentChat.module.css
+```
+
+- [ ] **Step 3: Remove agent route from backend**
+
+In `backend/src/index.ts`, find and remove:
+```ts
+// DELETE: import and route mount for agent
+import agentRouter from './routes/agent';
+// ...
+app.use('/api/agent', agentRouter);
+```
+
+Then delete the route file:
+```bash
+rm backend/src/routes/agent.ts
+```
+
+- [ ] **Step 4: Build and verify**
+
+```bash
+cd frontend && npx tsc --noEmit && cd ../backend && npx tsc --noEmit
+```
+
+Expected: No type errors. The agent endpoint is not referenced anywhere else.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add -A && git commit -m "refactor: remove KI-Agent chat widget and backend route
+
+Agent/Expertenblick was a nice-to-have that distracted from the core tipping flow.
+Removes AgentChat.tsx, AgentChat.module.css, and /api/agent routes."
+```
+
+---
+
+## Task 2: Simplify TipModal — Remove Expertenblick
+
+Strip the Expertenblick accordion from the TipModal. Keep: teams, picker, tendency, confirm button.
+
+**Files:**
+- Modify: `frontend/src/components/TipModal.tsx`
+- Modify: `frontend/src/components/TipModal.module.css`
+
+- [ ] **Step 1: Remove Expertenblick from TipModal.tsx**
+
+In `TipModal.tsx`, remove:
+- All state related to insight (`insightOpen`, `insightText`, `insightLoading`, `insightAudio`, etc.)
+- The `fetchInsight()` function and audio playback logic
+- The entire `insightWrapper` / `insightToggleRow` JSX section
+- Imports for Sparkles, ChevronDown icons if only used by insight
+
+Keep:
+- Match header (teams + flags — but remove `groupBadge` and `kickoffBlock`)
+- Score picker section (`pickerSection`)
+- Tendency bar (`tendencyBar`)
+- Save button (`saveBtn`)
+- Cancel button
+
+- [ ] **Step 2: Slim down the modal header**
+
+Remove the group badge and kickoff time from the modal (already visible on the card):
+
+```tsx
+// KEEP only teams row:
+
+
+
+

+
+
{match.homeTeam.shortName}
+
+
+ vs
+
+
+
+

+
+
{match.awayTeam.shortName}
+
+
+```
+
+- [ ] **Step 3: Clean up unused CSS**
+
+In `TipModal.module.css`, remove all styles prefixed with `insight` (`insightWrapper`, `insightToggleRow`, `insightToggle`, `insightIcon`, `insightChevron`, `insightPanel`, `insightDialogue`, etc.). Also remove `matchHeader`, `groupBadge`, `kickoffBlock`, `kickoffDate`, `kickoffTime` if no longer referenced.
+
+- [ ] **Step 4: Build and verify**
+
+```bash
+cd frontend && npx tsc --noEmit
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add -A && git commit -m "refactor: simplify TipModal — remove Expertenblick and redundant header
+
+Modal now shows only: teams, score picker, tendency bar, confirm button.
+Reduces modal from 367 to ~150 lines."
+```
+
+---
+
+## Task 3: Bottom Navigation Bar + Slim Header
+
+Replace header-based navigation with a fixed bottom bar (mobile-first). Header becomes logo-only.
+
+**Files:**
+- Create: `frontend/src/components/BottomNav.tsx`
+- Create: `frontend/src/components/BottomNav.module.css`
+- Modify: `frontend/src/App.tsx`
+- Modify: `frontend/src/App.module.css`
+- Modify: `frontend/src/index.css` (add safe-area variable)
+
+- [ ] **Step 1: Create BottomNav component**
+
+Create `frontend/src/components/BottomNav.tsx`:
+
+```tsx
+import { NavLink } from 'react-router-dom';
+import { Home, Trophy, User } from 'lucide-react';
+import styles from './BottomNav.module.css';
+
+// ⚽ is an emoji, not a lucide icon — used directly
+export default function BottomNav() {
+ const linkClass = ({ isActive }: { isActive: boolean }) =>
+ isActive ? styles.tabActive : styles.tab;
+
+ return (
+
+ );
+}
+```
+
+- [ ] **Step 2: Create BottomNav styles**
+
+Create `frontend/src/components/BottomNav.module.css`:
+
+```css
+.bottomNav {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ height: 60px;
+ padding-bottom: env(safe-area-inset-bottom, 0px);
+ background: color-mix(in srgb, var(--bg-deep) 92%, transparent);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border-top: 1px solid rgba(var(--primary-rgb, 75, 183, 248), 0.15);
+ z-index: 100;
+}
+
+.tab, .tabActive {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2px;
+ padding: 6px 16px;
+ font-size: 11px;
+ text-decoration: none;
+ color: var(--text-muted);
+ transition: color 0.2s;
+}
+
+.tabActive {
+ color: var(--primary);
+}
+
+.tab:hover {
+ color: var(--text-secondary);
+}
+
+.emojiIcon {
+ font-size: 20px;
+ line-height: 1;
+}
+
+/* Desktop: hide bottom nav, use header nav */
+@media (min-width: 768px) {
+ .bottomNav {
+ display: none;
+ }
+}
+```
+
+- [ ] **Step 3: Update App.tsx — slim header + bottom nav + new routes**
+
+Rewrite `frontend/src/App.tsx`:
+- Import `BottomNav`
+- Header: show only logo + dev badge + theme toggle on mobile; full nav on desktop
+- Add new route `/spiele` → MatchesPage, `/` → DashboardPage (placeholder for now, use MatchesPage temporarily)
+- Admin route: only render NavLink if user has editor role (for now, keep it in header desktop nav with a gear icon)
+- Add `` before closing ``
+- Add `padding-bottom: 70px` to main content area to account for bottom nav
+
+Key changes to `App.tsx`:
+```tsx
+import BottomNav from './components/BottomNav';
+import { Settings } from 'lucide-react';
+
+// In header nav (desktop only):
+
+
+// Routes:
+} />
+} />
+} />
+} />
+} />
+
+// After , before DevPanel:
+
+```
+
+- [ ] **Step 4: Update App.module.css**
+
+```css
+/* Hide desktop nav on mobile */
+.nav {
+ display: none;
+}
+
+@media (min-width: 768px) {
+ .nav {
+ display: flex;
+ /* existing styles */
+ }
+}
+
+/* Add bottom padding for fixed bottom nav */
+.main {
+ padding-bottom: 70px;
+}
+
+@media (min-width: 768px) {
+ .main {
+ padding-bottom: 0;
+ }
+}
+
+/* Admin link: icon only */
+.adminLink {
+ display: flex;
+ align-items: center;
+ color: var(--text-muted);
+ text-decoration: none;
+}
+.adminLink:hover { color: var(--text-secondary); }
+```
+
+- [ ] **Step 5: Build and smoke test**
+
+```bash
+cd frontend && npx tsc --noEmit && npm run build
+```
+
+Manual: open `http://192.168.1.60:3301?devUser=1` on mobile viewport — bottom nav should appear with 4 tabs. Desktop should still show header nav.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add -A && git commit -m "feat: add bottom navigation bar (mobile-first)
+
+Fixed bottom nav with Home/Spiele/Rangliste/Profil tabs.
+Desktop keeps header nav. Admin hidden behind gear icon.
+Main content padded to avoid overlap with bottom nav."
+```
+
+---
+
+## Task 4: Backend — Dashboard Endpoint + Streak Calculation
+
+New `/api/dashboard` endpoint that returns hero match, user stats, streak, and nudges in a single request.
+
+**Files:**
+- Create: `backend/src/routes/dashboard.ts`
+- Modify: `backend/src/index.ts` (mount route)
+- Modify: `frontend/src/api/client.ts` (add `getDashboard()`)
+- Modify: `backend/src/types/index.ts` (add types)
+
+- [ ] **Step 1: Define dashboard types**
+
+Add to `backend/src/types/index.ts`:
+
+```ts
+export interface DashboardResponse {
+ hero: {
+ match: {
+ id: number;
+ homeTeam: { name: string; shortName: string; crest: string | null };
+ awayTeam: { name: string; shortName: string; crest: string | null };
+ utcDate: string;
+ status: string;
+ minutesUntilKickoff: number;
+ };
+ userTip: { home: number; away: number } | null;
+ tippable: boolean;
+ } | null;
+ stats: {
+ rank: number | null;
+ totalPoints: number;
+ streak: number;
+ };
+ nudges: Array<{
+ type: 'untipped' | 'leader' | 'result';
+ text: string;
+ matchId?: number;
+ }>;
+}
+```
+
+- [ ] **Step 2: Create dashboard route**
+
+Create `backend/src/routes/dashboard.ts`:
+
+```ts
+import { Router, Request, Response } from 'express';
+import { query } from '../db/client';
+import { logger } from '../services/logger';
+
+const router = Router();
+
+router.get('/', async (req: Request, res: Response): Promise => {
+ const userId = req.staffbaseUser?.sub;
+ if (!userId) { res.status(401).json({ error: 'Unauthorized' }); return; }
+
+ try {
+ // 1. Hero match: next tippable or next upcoming
+ const heroResult = await query(
+ `SELECT m.id, m.utc_date, m.status, m.stage, m.group_name,
+ m.home_team_name, m.home_team_short, m.home_team_crest,
+ m.away_team_name, m.away_team_short, m.away_team_crest,
+ t.tip_home, t.tip_away,
+ EXTRACT(EPOCH FROM (m.utc_date - NOW())) / 60 AS minutes_until
+ FROM matches m
+ LEFT JOIN tips t ON t.match_id = m.id AND t.user_id = $1
+ WHERE m.status IN ('SCHEDULED', 'TIMED')
+ ORDER BY m.utc_date ASC
+ LIMIT 1`,
+ [userId]
+ );
+
+ const heroMatch = heroResult[0] || null;
+ const hero = heroMatch ? {
+ match: {
+ id: heroMatch.id,
+ homeTeam: { name: heroMatch.home_team_name, shortName: heroMatch.home_team_short, crest: heroMatch.home_team_crest },
+ awayTeam: { name: heroMatch.away_team_name, shortName: heroMatch.away_team_short, crest: heroMatch.away_team_crest },
+ utcDate: heroMatch.utc_date,
+ status: heroMatch.status,
+ minutesUntilKickoff: Math.round(heroMatch.minutes_until),
+ },
+ userTip: heroMatch.tip_home != null ? { home: heroMatch.tip_home, away: heroMatch.tip_away } : null,
+ tippable: heroMatch.minutes_until > 5,
+ } : null;
+
+ // 2. User stats from leaderboard
+ const statsResult = await query(
+ `SELECT rank, total_points FROM leaderboard WHERE user_id = $1`,
+ [userId]
+ );
+ const userStats = statsResult[0];
+
+ // 3. Streak: count consecutive tipped matches (by kickoff date, most recent first)
+ const streakResult = await query<{ streak: string }>(
+ `WITH ordered_matches AS (
+ SELECT m.id, m.utc_date,
+ CASE WHEN t.id IS NOT NULL THEN true ELSE false END AS has_tip
+ FROM matches m
+ LEFT JOIN tips t ON t.match_id = m.id AND t.user_id = $1
+ WHERE m.utc_date <= NOW()
+ AND m.status IN ('FINISHED', 'IN_PLAY')
+ ORDER BY m.utc_date DESC
+ )
+ SELECT COUNT(*) AS streak
+ FROM (
+ SELECT has_tip,
+ ROW_NUMBER() OVER () AS rn
+ FROM ordered_matches
+ ) sub
+ WHERE has_tip = true
+ AND rn = (SELECT MIN(rn) FROM (
+ SELECT has_tip, ROW_NUMBER() OVER () AS rn FROM ordered_matches
+ ) s WHERE s.rn <= sub.rn AND s.has_tip = true)
+ AND NOT EXISTS (
+ SELECT 1 FROM (
+ SELECT has_tip, ROW_NUMBER() OVER () AS rn FROM ordered_matches
+ ) s2 WHERE s2.rn < sub.rn AND s2.has_tip = false
+ )`,
+ [userId]
+ );
+
+ // Simpler streak approach: iterate from most recent backward
+ const allMatches = await query<{ has_tip: boolean }>(
+ `SELECT CASE WHEN t.id IS NOT NULL THEN true ELSE false END AS has_tip
+ FROM matches m
+ LEFT JOIN tips t ON t.match_id = m.id AND t.user_id = $1
+ WHERE m.utc_date <= NOW() AND m.status IN ('FINISHED', 'IN_PLAY')
+ ORDER BY m.utc_date DESC`,
+ [userId]
+ );
+ let streak = 0;
+ for (const m of allMatches) {
+ if (m.has_tip) streak++;
+ else break;
+ }
+
+ // 4. Nudges
+ const nudges: Array<{ type: string; text: string; matchId?: number }> = [];
+
+ // Untipped matches today
+ const untippedToday = await query<{ count: string }>(
+ `SELECT COUNT(*) AS count
+ FROM matches m
+ LEFT JOIN tips t ON t.match_id = m.id AND t.user_id = $1
+ WHERE m.utc_date::date = CURRENT_DATE
+ AND m.status IN ('SCHEDULED', 'TIMED')
+ AND t.id IS NULL`,
+ [userId]
+ );
+ const untippedCount = parseInt(untippedToday[0]?.count || '0');
+ if (untippedCount > 0) {
+ nudges.push({
+ type: 'untipped',
+ text: `📅 Heute noch ${untippedCount} ${untippedCount === 1 ? 'Spiel' : 'Spiele'} ohne Tipp`,
+ });
+ }
+
+ // Leader info
+ const leader = await query<{ full_name: string; total_points: string }>(
+ `SELECT full_name, total_points FROM leaderboard ORDER BY rank ASC LIMIT 1`
+ );
+ if (leader[0] && leader[0].full_name) {
+ nudges.push({
+ type: 'leader',
+ text: `🏆 ${leader[0].full_name} führt mit ${leader[0].total_points} Punkten`,
+ });
+ }
+
+ // Latest result with points
+ const latestResult = await query(
+ `SELECT m.home_team_short, m.away_team_short, m.score_home, m.score_away, t.points
+ FROM tips t
+ JOIN matches m ON m.id = t.match_id
+ WHERE t.user_id = $1 AND t.points IS NOT NULL
+ ORDER BY m.utc_date DESC LIMIT 1`,
+ [userId]
+ );
+ if (latestResult[0]) {
+ const r = latestResult[0];
+ nudges.push({
+ type: 'result',
+ text: `🎯 Letzte Auswertung: ${r.points} Punkte für ${r.home_team_short} ${r.score_home}:${r.score_away} ${r.away_team_short}`,
+ matchId: r.match_id,
+ });
+ }
+
+ res.json({
+ hero,
+ stats: {
+ rank: userStats ? parseInt(userStats.rank) : null,
+ totalPoints: userStats ? parseInt(userStats.total_points) : 0,
+ streak,
+ },
+ nudges,
+ });
+ } catch (error) {
+ logger.error('Dashboard failed', { error });
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+export default router;
+```
+
+- [ ] **Step 3: Mount dashboard route in backend**
+
+In `backend/src/index.ts`, add:
+
+```ts
+import dashboardRouter from './routes/dashboard';
+// After other route mounts:
+app.use('/api/dashboard', dashboardRouter);
+```
+
+- [ ] **Step 4: Add getDashboard to frontend API client**
+
+In `frontend/src/api/client.ts`, add the interface and method:
+
+```ts
+export interface DashboardData {
+ hero: {
+ match: {
+ id: number;
+ homeTeam: { name: string; shortName: string; crest: string | null };
+ awayTeam: { name: string; shortName: string; crest: string | null };
+ utcDate: string;
+ status: string;
+ minutesUntilKickoff: number;
+ };
+ userTip: { home: number; away: number } | null;
+ tippable: boolean;
+ } | null;
+ stats: {
+ rank: number | null;
+ totalPoints: number;
+ streak: number;
+ };
+ nudges: Array<{ type: string; text: string; matchId?: number }>;
+}
+
+// In api object:
+getDashboard: () => request('/dashboard'),
+```
+
+- [ ] **Step 5: Build and verify**
+
+```bash
+cd backend && npx tsc --noEmit && cd ../frontend && npx tsc --noEmit
+```
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add -A && git commit -m "feat: add /api/dashboard endpoint with hero match, stats, streak, nudges
+
+Single endpoint returns everything the dashboard needs:
+- Next tippable match with user's tip
+- Rank, points, and streak counter
+- Contextual nudges (untipped today, leader, last result)"
+```
+
+---
+
+## Task 5: Dashboard Page
+
+New startseite replacing the 104-match list as the landing page.
+
+**Files:**
+- Create: `frontend/src/pages/DashboardPage.tsx`
+- Create: `frontend/src/pages/DashboardPage.module.css`
+- Modify: `frontend/src/App.tsx` (route `/` → DashboardPage)
+
+- [ ] **Step 1: Create DashboardPage component**
+
+Create `frontend/src/pages/DashboardPage.tsx`:
+
+```tsx
+import { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { api, DashboardData } from '../api/client';
+import styles from './DashboardPage.module.css';
+
+interface Props {
+ devUser?: number;
+}
+
+export default function DashboardPage({ devUser }: Props) {
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ api.getDashboard()
+ .then(setData)
+ .catch(() => {})
+ .finally(() => setLoading(false));
+ }, [devUser]);
+
+ if (loading) return Laden...
;
+ if (!data) return Dashboard konnte nicht geladen werden.
;
+
+ const { hero, stats, nudges } = data;
+
+ const formatCountdown = (minutes: number): string => {
+ if (minutes < 0) return 'Läuft';
+ if (minutes < 60) return `in ${Math.round(minutes)} Min`;
+ if (minutes < 1440) return `in ${Math.round(minutes / 60)}h`;
+ const days = Math.round(minutes / 1440);
+ return `in ${days} ${days === 1 ? 'Tag' : 'Tagen'}`;
+ };
+
+ return (
+
+ {/* Hero: Next match */}
+ {hero ? (
+
navigate('/spiele')}
+ role="button"
+ tabIndex={0}
+ >
+
+ {hero.tippable ? 'Nächstes Spiel' : 'Kommendes Spiel'}
+
+ {formatCountdown(hero.match.minutesUntilKickoff)}
+
+
+
+
+ {hero.match.homeTeam.crest && (
+

+ )}
+
{hero.match.homeTeam.shortName}
+
+
vs
+
+ {hero.match.awayTeam.crest && (
+

+ )}
+
{hero.match.awayTeam.shortName}
+
+
+ {hero.userTip ? (
+
+ Dein Tipp: {hero.userTip.home}:{hero.userTip.away} ✓
+
+ ) : hero.tippable ? (
+
+ ) : null}
+
+ ) : (
+
+ Keine anstehenden Spiele
+
+ )}
+
+ {/* Stats tiles */}
+
+
+ {stats.rank ?? '—'}
+ Dein Rang
+
+
+ {stats.totalPoints}
+ Punkte
+
+
+
+ {stats.streak > 0 ? `${stats.streak}🔥` : '0'}
+
+ Streak
+
+
+
+ {/* Nudges */}
+ {nudges.length > 0 && (
+
+ {nudges.map((nudge, i) => (
+ {
+ if (nudge.type === 'untipped') navigate('/spiele');
+ if (nudge.type === 'leader') navigate('/rangliste');
+ }}
+ >
+ {nudge.text}
+
+ ))}
+
+ )}
+
+ );
+}
+```
+
+- [ ] **Step 2: Create DashboardPage styles**
+
+Create `frontend/src/pages/DashboardPage.module.css` with hero card (gradient background, glassmorphism), stats row (3-column grid), nudge list. Use existing CSS variables from `index.css`. Hero gets a subtle gradient border and elevated shadow.
+
+Key patterns:
+```css
+.dashboard { padding: 16px; max-width: 600px; margin: 0 auto; }
+.hero { background: var(--surface-mid); border-radius: var(--radius-lg); padding: 20px; cursor: pointer; }
+.heroCountdown { color: var(--gold); font-weight: 700; }
+.heroTip { background: var(--surface-high); border-radius: var(--radius-sm); padding: 8px 12px; color: var(--gold); }
+.heroTipBtn { /* btn-primary style */ }
+.statsRow { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; margin: 12px 0; }
+.statTile { background: var(--surface-mid); border-radius: var(--radius-md); padding: 12px; text-align: center; }
+.statValue { font-size: 1.5rem; font-weight: 700; color: var(--gold); }
+.nudge { background: var(--surface-low); border-radius: var(--radius-sm); padding: 12px; cursor: pointer; }
+```
+
+- [ ] **Step 3: Wire up in App.tsx**
+
+```tsx
+import DashboardPage from './pages/DashboardPage';
+
+// Change route:
+} />
+} />
+```
+
+- [ ] **Step 4: Build and smoke test**
+
+```bash
+cd frontend && npm run build
+```
+
+Open app — `/` should show dashboard, `/spiele` shows the match list.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add -A && git commit -m "feat: add Dashboard as new startseite
+
+Hero card with next match + countdown, stats tiles (rank, points, streak),
+and contextual nudges. Replaces 104-match list as landing page."
+```
+
+---
+
+## Task 6: Smart Sections in Spielplan
+
+Restructure MatchesPage from flat list to time-grouped accordion sections.
+
+**Files:**
+- Modify: `frontend/src/pages/MatchesPage.tsx`
+- Modify: `frontend/src/pages/MatchesPage.module.css`
+
+- [ ] **Step 1: Add section grouping logic**
+
+Replace the current date/group-based grouping in `MatchesPage.tsx` with time-relative sections:
+
+```tsx
+type Section = {
+ key: string;
+ label: string;
+ matches: Match[];
+ defaultOpen: boolean;
+ highlight: boolean;
+};
+
+function groupIntoSections(matches: Match[]): Section[] {
+ const now = new Date();
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+ const tomorrow = new Date(today); tomorrow.setDate(today.getDate() + 1);
+ const endOfWeek = new Date(today); endOfWeek.setDate(today.getDate() + (7 - today.getDay()));
+
+ const sections: Section[] = [
+ { key: 'today', label: 'Heute', matches: [], defaultOpen: true, highlight: true },
+ { key: 'tomorrow', label: 'Morgen', matches: [], defaultOpen: true, highlight: false },
+ { key: 'week', label: 'Diese Woche', matches: [], defaultOpen: false, highlight: false },
+ { key: 'later', label: 'Demnächst', matches: [], defaultOpen: false, highlight: false },
+ { key: 'past', label: 'Vergangene Spiele', matches: [], defaultOpen: false, highlight: false },
+ ];
+
+ for (const match of matches) {
+ const d = new Date(match.utcDate);
+ if (d < today) sections[4].matches.push(match); // past
+ else if (d < tomorrow) sections[0].matches.push(match); // today
+ else if (d < new Date(tomorrow.getTime() + 86400000)) sections[1].matches.push(match); // tomorrow
+ else if (d < endOfWeek) sections[2].matches.push(match); // this week
+ else sections[3].matches.push(match); // later
+ }
+
+ // Sort past matches newest first
+ sections[4].matches.reverse();
+
+ return sections.filter(s => s.matches.length > 0);
+}
+```
+
+- [ ] **Step 2: Replace filter buttons with dropdown + accordion UI**
+
+Replace the stage filter button row with a simple dropdown:
+
+```tsx
+
+```
+
+Render sections as collapsible accordion:
+
+```tsx
+{sections.map(section => (
+
+
+ {openSections.has(section.key) && (
+
+ {section.matches.map(match => (
+
+ ))}
+
+ )}
+
+))}
+```
+
+Initialize `openSections` with sections that have `defaultOpen: true`.
+
+- [ ] **Step 3: Style the sections**
+
+In `MatchesPage.module.css`:
+- `sectionHeader`: clickable, flex row, bold label, count badge, chevron
+- `sectionHighlight`: subtle gold left border for "Heute"
+- `sectionContent`: padding, gap between cards
+- `stageFilter`: styled select replacing the button row
+- Remove old filter button styles
+
+- [ ] **Step 4: Build and verify**
+
+```bash
+cd frontend && npm run build
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add -A && git commit -m "feat: smart sections in Spielplan (Heute/Morgen/Woche/Vergangen)
+
+Matches grouped by time relevance with collapsible accordion.
+'Heute' and 'Morgen' open by default with highlight.
+Stage filter simplified to dropdown."
+```
+
+---
+
+## Task 7: Zustandsbasierte Match-Cards
+
+Redesign MatchCard to look visually different per match state.
+
+**Files:**
+- Modify: `frontend/src/components/MatchCard.tsx`
+- Modify: `frontend/src/components/MatchCard.module.css`
+
+- [ ] **Step 1: Derive visual state from match data**
+
+Add state derivation at the top of `MatchCard`:
+
+```tsx
+type CardState = 'open' | 'tipped' | 'live' | 'finished' | 'missed';
+
+function getCardState(match: Match): CardState {
+ if (match.status === 'IN_PLAY' || match.status === 'PAUSED') return 'live';
+ if (match.status === 'FINISHED') {
+ return match.userTip ? 'finished' : 'missed';
+ }
+ // SCHEDULED or TIMED
+ return match.userTip ? 'tipped' : 'open';
+}
+```
+
+- [ ] **Step 2: Render state-specific UI**
+
+For each state, adjust the card's visual treatment:
+
+- **open**: Standard look. "Tipp abgeben" button. If `minutesUntilKickoff < 60`, show pulsing countdown.
+- **tipped**: Green left border. Show "Dein Tipp: 2:1" prominently. "Ändern" link instead of button.
+- **live**: Red pulsing dot + "LIVE" badge. Show score if available. Lock icon for tip.
+- **finished**: Show result + tip + points badge. Gold shimmer for exact (3pts), green for tendency (1pt), gray for wrong (0pts).
+- **missed**: Reduced opacity (0.5). "Nicht getippt" label. No interaction.
+
+Apply state as CSS class:
+```tsx
+
+```
+
+- [ ] **Step 3: Add countdown timer for urgent matches**
+
+```tsx
+function CountdownTimer({ minutes }: { minutes: number }) {
+ const [remaining, setRemaining] = useState(minutes);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setRemaining(r => Math.max(0, r - 1/60));
+ }, 1000);
+ return () => clearInterval(interval);
+ }, []);
+
+ if (remaining > 60) return null;
+
+ const mins = Math.floor(remaining);
+ const urgent = remaining < 5;
+
+ return (
+
+ Noch {mins} Min!
+
+ );
+}
+```
+
+- [ ] **Step 4: Style each card state**
+
+In `MatchCard.module.css`:
+
+```css
+.card_open { /* default styling */ }
+
+.card_tipped {
+ border-left: 3px solid var(--success);
+}
+
+.card_live {
+ border-left: 3px solid var(--error);
+}
+.card_live .liveDot {
+ width: 8px; height: 8px;
+ background: var(--error);
+ border-radius: 50%;
+ animation: pulse 1.5s infinite;
+}
+
+.card_finished .pointsBadge_exact {
+ background: linear-gradient(135deg, var(--gold), #FFD700);
+ color: #1a1a1a;
+ animation: shimmer 2s ease-in-out;
+}
+.card_finished .pointsBadge_tendency {
+ background: var(--success);
+ color: #1a1a1a;
+}
+.card_finished .pointsBadge_wrong {
+ background: var(--text-muted);
+}
+
+.card_missed {
+ opacity: 0.45;
+ pointer-events: none;
+}
+
+.countdown {
+ color: var(--error);
+ font-weight: 700;
+ font-size: 0.85rem;
+}
+.countdownUrgent {
+ animation: pulse 0.8s infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.4; }
+}
+
+@keyframes shimmer {
+ 0% { box-shadow: 0 0 0 rgba(254, 174, 50, 0); }
+ 50% { box-shadow: 0 0 12px rgba(254, 174, 50, 0.6); }
+ 100% { box-shadow: 0 0 0 rgba(254, 174, 50, 0); }
+}
+```
+
+- [ ] **Step 5: Build and verify**
+
+```bash
+cd frontend && npm run build
+```
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add -A && git commit -m "feat: zustandsbasierte Match-Cards (open/tipped/live/finished/missed)
+
+Each card state has distinct visual treatment:
+- Open: standard with countdown timer when <1h
+- Tipped: green accent with tip display
+- Live: pulsing red dot
+- Finished: points badge (gold/green/gray)
+- Missed: grayed out"
+```
+
+---
+
+## Task 8: Tipp-Bestätigung Animation
+
+Add success animation after submitting a tip.
+
+**Files:**
+- Modify: `frontend/src/components/TipModal.tsx`
+- Modify: `frontend/src/components/TipModal.module.css`
+
+- [ ] **Step 1: Add success state to TipModal**
+
+After successful `api.submitTip()`, instead of immediately closing the modal, show a success state:
+
+```tsx
+const [showSuccess, setShowSuccess] = useState(false);
+
+async function handleSave() {
+ // ... existing save logic ...
+ await api.submitTip(match.id, tipHome, tipAway);
+ setShowSuccess(true);
+ // Haptic feedback on mobile
+ if (navigator.vibrate) navigator.vibrate(50);
+ // Auto-close after animation
+ setTimeout(() => {
+ setShowSuccess(false);
+ onClose(true); // true = tip was saved
+ }, 1200);
+}
+```
+
+- [ ] **Step 2: Render success overlay in modal**
+
+```tsx
+{showSuccess && (
+
+
✓
+
Dein Tipp ist drin! 🎯
+
+)}
+```
+
+- [ ] **Step 3: Style the success animation**
+
+```css
+.successOverlay {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background: color-mix(in srgb, var(--bg-deep) 90%, transparent);
+ border-radius: inherit;
+ animation: fadeIn 0.3s ease;
+ z-index: 10;
+}
+
+.successCheck {
+ width: 64px;
+ height: 64px;
+ border-radius: 50%;
+ background: var(--success);
+ color: white;
+ font-size: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ animation: popIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+}
+
+.successText {
+ margin-top: 12px;
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+@keyframes popIn {
+ 0% { transform: scale(0); opacity: 0; }
+ 100% { transform: scale(1); opacity: 1; }
+}
+
+@keyframes fadeIn {
+ 0% { opacity: 0; }
+ 100% { opacity: 1; }
+}
+```
+
+- [ ] **Step 4: Build and verify**
+
+```bash
+cd frontend && npm run build
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add -A && git commit -m "feat: tip confirmation animation with haptic feedback
+
+Success overlay with animated checkmark and '🎯 Dein Tipp ist drin!'
+message. Haptic vibration on mobile. Auto-closes after 1.2s."
+```
+
+---
+
+## Task 9: Punkte-Reveal with Confetti
+
+Animated reveal when the user sees evaluated results for the first time.
+
+**Files:**
+- Create: `frontend/src/components/ConfettiReveal.tsx`
+- Create: `frontend/src/components/ConfettiReveal.module.css`
+- Create: `frontend/src/hooks/useRevealQueue.ts`
+- Modify: `frontend/src/pages/MatchesPage.tsx` (trigger reveals)
+- Install: `canvas-confetti`
+
+- [ ] **Step 1: Install canvas-confetti**
+
+```bash
+cd frontend && npm install canvas-confetti && npm install -D @types/canvas-confetti
+```
+
+- [ ] **Step 2: Create useRevealQueue hook**
+
+Create `frontend/src/hooks/useRevealQueue.ts`:
+
+```ts
+import { useState, useEffect } from 'react';
+import { Match } from '../api/client';
+
+const SEEN_KEY = 'tippspiel_seen_results';
+
+function getSeenIds(): Set
{
+ try {
+ return new Set(JSON.parse(localStorage.getItem(SEEN_KEY) || '[]'));
+ } catch { return new Set(); }
+}
+
+function markSeen(matchId: number) {
+ const seen = getSeenIds();
+ seen.add(matchId);
+ localStorage.setItem(SEEN_KEY, JSON.stringify([...seen]));
+}
+
+export function useRevealQueue(matches: Match[]) {
+ const [queue, setQueue] = useState([]);
+
+ useEffect(() => {
+ const seen = getSeenIds();
+ const unseen = matches.filter(
+ m => m.status === 'FINISHED' && m.userTip && m.userTip.points !== null && !seen.has(m.id)
+ );
+ setQueue(unseen);
+ }, [matches]);
+
+ function dismissCurrent() {
+ if (queue.length === 0) return;
+ markSeen(queue[0].id);
+ setQueue(q => q.slice(1));
+ }
+
+ return { current: queue[0] || null, remaining: queue.length, dismissCurrent };
+}
+```
+
+- [ ] **Step 3: Create ConfettiReveal component**
+
+Create `frontend/src/components/ConfettiReveal.tsx`:
+
+```tsx
+import { useEffect, useRef } from 'react';
+import confetti from 'canvas-confetti';
+import { Match } from '../api/client';
+import styles from './ConfettiReveal.module.css';
+
+interface Props {
+ match: Match;
+ onDismiss: () => void;
+}
+
+export default function ConfettiReveal({ match, onDismiss }: Props) {
+ const didFire = useRef(false);
+ const tip = match.userTip!;
+ const points = tip.points!;
+
+ useEffect(() => {
+ if (points === 3 && !didFire.current) {
+ didFire.current = true;
+ confetti({ particleCount: 120, spread: 80, origin: { y: 0.6 } });
+ }
+ }, [points]);
+
+ const resultLabel = points === 3 ? 'EXAKT! 🎉' : points === 1 ? 'Richtige Tendenz! 👏' : 'Knapp daneben... 😅';
+ const resultClass = points === 3 ? styles.exact : points === 1 ? styles.tendency : styles.wrong;
+
+ return (
+
+
e.stopPropagation()}>
+
+ {match.homeTeam.shortName} {match.score.home}:{match.score.away} {match.awayTeam.shortName}
+
+
+ Dein Tipp: {tip.home}:{tip.away}
+
+
+ {points} {points === 1 ? 'Punkt' : 'Punkte'}
+
+
{resultLabel}
+
+
+
+ );
+}
+```
+
+- [ ] **Step 4: Style the reveal**
+
+Create `frontend/src/components/ConfettiReveal.module.css` — centered modal overlay with animated entrance, gold/green/gray badge based on points.
+
+- [ ] **Step 5: Integrate into MatchesPage**
+
+In `MatchesPage.tsx`:
+
+```tsx
+import { useRevealQueue } from '../hooks/useRevealQueue';
+import ConfettiReveal from '../components/ConfettiReveal';
+
+// Inside component:
+const { current: revealMatch, dismissCurrent } = useRevealQueue(matches);
+
+// In JSX, before the sections:
+{revealMatch && (
+
+)}
+```
+
+- [ ] **Step 6: Build and verify**
+
+```bash
+cd frontend && npm run build
+```
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add -A && git commit -m "feat: Punkte-Reveal with confetti animation
+
+Shows animated reveal overlay for unseen match results.
+Exact match (3pts) triggers confetti explosion.
+Each reveal shown only once (localStorage tracking)."
+```
+
+---
+
+## Task 10: Rang-Change Toast Notifications
+
+Toast notification when rank changes between visits.
+
+**Files:**
+- Create: `frontend/src/components/Toast.tsx`
+- Create: `frontend/src/components/Toast.module.css`
+- Create: `frontend/src/hooks/useRankChange.ts`
+- Modify: `frontend/src/App.tsx` (render toast)
+
+- [ ] **Step 1: Create useRankChange hook**
+
+Create `frontend/src/hooks/useRankChange.ts`:
+
+```ts
+import { useState, useEffect } from 'react';
+import { api } from '../api/client';
+
+const RANK_KEY = 'tippspiel_last_rank';
+
+export function useRankChange() {
+ const [message, setMessage] = useState(null);
+
+ useEffect(() => {
+ api.getMyStats().then(stats => {
+ if (!stats.rank) return;
+ const lastRank = parseInt(localStorage.getItem(RANK_KEY) || '0');
+ if (lastRank > 0 && lastRank !== stats.rank) {
+ if (stats.rank < lastRank) {
+ setMessage(`⬆️ Du bist auf Platz ${stats.rank} aufgestiegen!`);
+ } else {
+ setMessage(`⬇️ Du bist auf Platz ${stats.rank} gerutscht — hol dir die Punkte zurück!`);
+ }
+ }
+ localStorage.setItem(RANK_KEY, String(stats.rank));
+ }).catch(() => {});
+ }, []);
+
+ function dismiss() { setMessage(null); }
+
+ return { message, dismiss };
+}
+```
+
+- [ ] **Step 2: Create Toast component**
+
+Create `frontend/src/components/Toast.tsx`:
+
+```tsx
+import { useEffect } from 'react';
+import styles from './Toast.module.css';
+
+interface Props {
+ message: string;
+ onDismiss: () => void;
+ duration?: number;
+}
+
+export default function Toast({ message, onDismiss, duration = 5000 }: Props) {
+ useEffect(() => {
+ const timer = setTimeout(onDismiss, duration);
+ return () => clearTimeout(timer);
+ }, [onDismiss, duration]);
+
+ return (
+
+ {message}
+
+ );
+}
+```
+
+Style: fixed top, centered, glassmorphism background, slide-in from top animation, auto-dismiss after 5s.
+
+- [ ] **Step 3: Wire into App.tsx**
+
+```tsx
+import Toast from './components/Toast';
+import { useRankChange } from './hooks/useRankChange';
+
+// Inside App():
+const { message: rankMsg, dismiss: dismissRank } = useRankChange();
+
+// In JSX, after :
+{rankMsg && }
+```
+
+- [ ] **Step 4: Build and commit**
+
+```bash
+cd frontend && npm run build
+git add -A && git commit -m "feat: rank change toast notifications
+
+Shows toast on app open when rank changed since last visit.
+Auto-dismisses after 5 seconds. Tracks last rank in localStorage."
+```
+
+---
+
+## Task 11: Streak Tracker UI
+
+Display streak on dashboard (already done via API) and profile page. Add streak break nudge.
+
+**Files:**
+- Modify: `frontend/src/pages/DashboardPage.tsx` (already has streak in stats)
+- Modify: `frontend/src/pages/ProfilePage.tsx` (add streak display)
+
+- [ ] **Step 1: Add streak milestone icons**
+
+Create a helper used in DashboardPage and ProfilePage:
+
+```tsx
+function streakDisplay(streak: number): string {
+ if (streak >= 20) return `⚡${streak}`;
+ if (streak >= 10) return `🔥🔥${streak}`;
+ if (streak >= 3) return `🔥${streak}`;
+ return String(streak);
+}
+```
+
+- [ ] **Step 2: Show streak on profile**
+
+In `ProfilePage.tsx`, fetch streak from dashboard endpoint and display prominently near the stats ring.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add -A && git commit -m "feat: streak tracker UI with milestone icons
+
+Streak displayed on dashboard stats and profile.
+Milestones: 🔥 at 3, 🔥🔥 at 10, ⚡ at 20."
+```
+
+---
+
+## Task 12: Rich Profile Page
+
+Redesign ProfilePage with stats ring, tip history, and fun stats.
+
+**Files:**
+- Create: `frontend/src/components/StatsRing.tsx`
+- Create: `frontend/src/components/StatsRing.module.css`
+- Modify: `frontend/src/pages/ProfilePage.tsx`
+- Modify: `frontend/src/pages/ProfilePage.module.css`
+
+- [ ] **Step 1: Create StatsRing SVG component**
+
+Create `frontend/src/components/StatsRing.tsx` — a donut chart using SVG `` with `stroke-dasharray`:
+
+```tsx
+interface Props {
+ exact: number;
+ tendency: number;
+ wrong: number;
+ total: number;
+}
+
+export default function StatsRing({ exact, tendency, wrong, total }: Props) {
+ const radius = 60;
+ const circumference = 2 * Math.PI * radius;
+ const all = exact + tendency + wrong || 1;
+
+ const segments = [
+ { value: exact / all, color: 'var(--gold)', label: 'Exakt' },
+ { value: tendency / all, color: 'var(--success)', label: 'Tendenz' },
+ { value: wrong / all, color: 'var(--error)', label: 'Falsch' },
+ ];
+
+ let offset = 0;
+
+ return (
+
+
+
+ {segments.map((seg, i) => (
+
+
+ {seg.label}
+
+ ))}
+
+
+ );
+}
+```
+
+- [ ] **Step 2: Rewrite ProfilePage**
+
+Replace the 4 stat boxes with:
+1. Header card (avatar, name, rank badge, team)
+2. StatsRing component
+3. Tip history list (fetched from `api.getMyTips()`)
+4. Fun stats (calculated client-side from tips data)
+
+```tsx
+// Fun stats calculation:
+const favoriteTip = tips.reduce((acc, t) => {
+ const key = `${t.tip_home}:${t.tip_away}`;
+ acc[key] = (acc[key] || 0) + 1;
+ return acc;
+}, {} as Record);
+const topTip = Object.entries(favoriteTip).sort((a, b) => b[1] - a[1])[0];
+
+const homeWinTips = tips.filter(t => t.tip_home > t.tip_away).length;
+const homeWinPct = tips.length > 0 ? Math.round((homeWinTips / tips.length) * 100) : 0;
+```
+
+- [ ] **Step 3: Style the rich profile**
+
+Update `ProfilePage.module.css` — header card with flex row, stats ring centered, tip history as scrollable list with match icons and point badges, fun stats as subtle text cards.
+
+- [ ] **Step 4: Build and verify**
+
+```bash
+cd frontend && npm run build
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add -A && git commit -m "feat: rich profile page with stats ring, tip history, fun stats
+
+Donut chart showing exact/tendency/wrong distribution.
+Scrollable tip history with point badges.
+Fun stats: favorite tip, home win percentage, longest streak."
+```
+
+---
+
+## Task 13: Final Polish — CSS Tokens & Theme Consistency
+
+Add new animation tokens, ensure light mode works for all new components.
+
+**Files:**
+- Modify: `frontend/src/index.css`
+
+- [ ] **Step 1: Add animation and color tokens**
+
+Add to `index.css` `:root`:
+
+```css
+:root {
+ /* ... existing vars ... */
+ --primary-rgb: 75, 183, 248;
+ --streak-fire: #FF6B35;
+ --streak-lightning: #FFD700;
+ --transition-fast: 0.15s ease;
+ --transition-normal: 0.3s ease;
+}
+```
+
+Ensure all new components look correct in `[data-theme="light"]` — verify that `var(--surface-mid)`, `var(--gold)`, etc. work in both themes.
+
+- [ ] **Step 2: Verify light mode for all new components**
+
+Open app in light mode, check: dashboard, bottom nav, match cards (all states), tip modal success, profile, toast. Fix any contrast or readability issues.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add -A && git commit -m "style: add animation tokens, verify light mode for all new components"
+```
+
+---
+
+## Task 14: Build, Deploy & Smoke Test
+
+Final integration build, push, and verification.
+
+**Files:** None (deployment only)
+
+- [ ] **Step 1: Full build**
+
+```bash
+cd frontend && npm run build && cd ../backend && npx tsc --noEmit
+```
+
+- [ ] **Step 2: Commit and push**
+
+```bash
+git add -A && git push
+```
+
+Wait for Gitea CI pipeline to build and deploy.
+
+- [ ] **Step 3: Smoke test all flows**
+
+Using Playwright or manual browser testing at `http://192.168.1.60:3301?devUser=1`:
+
+1. `/` → Dashboard loads with hero match, stats, nudges
+2. Bottom nav visible on mobile viewport, all 4 tabs work
+3. `/spiele` → Smart sections (Heute expanded, past collapsed)
+4. Click "Tipp abgeben" → Slim modal, submit → success animation with haptic
+5. Match cards show correct states (tipped = green border)
+6. `/rangliste` → Leaderboard with existing podium
+7. `/profil` → Stats ring, tip history, fun stats
+8. Theme toggle → All components look correct in light mode
+9. AgentChat widget is gone
+10. Admin only visible as gear icon for devUser=1 (editor)
+
+- [ ] **Step 4: Final commit if fixes needed**
+
+```bash
+git add -A && git commit -m "fix: smoke test fixes after Phase 1 integration"
+git push
+```
+
+---
+
+## Summary
+
+| Task | Description | New Files | Modified Files |
+|------|-------------|-----------|---------------|
+| 1 | Remove AgentChat | — | 2 (delete 3) |
+| 2 | Simplify TipModal | — | 2 |
+| 3 | Bottom Navigation | 2 | 2 |
+| 4 | Dashboard Backend | 1 | 3 |
+| 5 | Dashboard Page | 2 | 1 |
+| 6 | Smart Sections | — | 2 |
+| 7 | Match Card States | — | 2 |
+| 8 | Tip Confirmation | — | 2 |
+| 9 | Punkte-Reveal | 3 | 1 |
+| 10 | Rank Toast | 3 | 1 |
+| 11 | Streak UI | — | 2 |
+| 12 | Rich Profile | 2 | 2 |
+| 13 | CSS Polish | — | 1 |
+| 14 | Deploy & Test | — | — |
+
+**Total: 14 tasks, ~13 new files, ~20 file modifications, 3 deletions**