diff --git a/.audit/animation-polish.tsv b/.audit/animation-polish.tsv
new file mode 100644
index 000000000..a858ccd63
--- /dev/null
+++ b/.audit/animation-polish.tsv
@@ -0,0 +1,17 @@
+ts phase decision why evidence result
+2026-08-31T00:00:00Z isolation Used the existing clean T3 worktree and t3code/animation-polish branch It is already isolated from main and all other attached worktrees git worktree list --porcelain clean at acaac0d
+2026-08-31T00:00:01Z design Kept the existing DebateLab visual system and set restrained motion intensity The plan asks for polish and accessibility, not a redesign plans/animation-polish.md motion target set to purposeful state changes
+2026-08-31T00:00:02Z runtime Installed Node 22 through NVM before verification package.json and CI require Node 22, while the VPS only had Node 24 available package.json Node 22 installation started
+2026-08-31T15:28:36Z runtime Completed Node 22 installation and clean npm dependency install Matched the repository engine and CI runtime before checking code package.json Node v22.23.2 and npm 10.9.8 active
+2026-08-31T15:31:21Z browser Rejected the first E2E result because Playwright reused another worktree server on port 8080 The browser did not contain this branch's route evidence attributes test-results error contexts rerun isolated on port 8187
+2026-08-31T15:33:01Z browser Passed the focused normal and reduced-motion matrix on desktop and mobile Used a dedicated port and deterministic local mock provider e2e/animation-polish.spec.ts 4 tests passed
+2026-08-31T15:40:34Z implementation Closed remaining route state and reduced-motion audit findings Luna audits found lazy fallback mobile menu timers smooth scrolling focus and nested result motion gaps src/App.tsx, src/index.css, src/components/landing/Hero.tsx 57 focused tests plus lint and typecheck passed
+2026-08-31T15:50:00Z verification Passed the full Node 22 unit coverage and production build gate Fresh checks covered the complete maintained test scope and compiled release output coverage summary and dist build 874 tests passed before final review fixes
+2026-08-31T15:57:00Z verification Passed the complete desktop and mobile browser suite after isolating the server and bounding workers The four-worker run exposed fixture timeouts under contention, while the CI-parity two-worker run was stable e2e suite on port 8191 116 tests passed
+2026-08-31T16:03:30Z review Fixed the mounted-result scroll race and expanded the feature-state browser matrix Independent Luna reviews found one code race and incomplete options/state coverage src/pages/GeneratePage.tsx, e2e/animation-polish.spec.ts focused unit and 6 browser tests passed
+2026-08-31T16:11:13Z verification Passed final coverage build and full browser gates on the reviewed diff Re-ran every required check after review changes and inspected all eight normal and reduced-motion screenshots coverage summary, build output, test-results/animation-polish-* 875 tests and 118 E2E tests passed
+2026-08-31T16:11:13Z visual Manually inspected desktop and mobile empty/options and result screenshots in normal and reduced-motion modes Confirmed readable stable layouts with no toast overlay or missing result sections test-results/animation-polish-* 8 screenshots accepted
+2026-08-31T16:15:00Z integration Detected origin/main advancing from acaac0d to 66640be before commit Final review compared against the live base and found the parallel analytics merge git log HEAD..origin/main rebase required before PR
+2026-08-31T16:17:00Z integration Rebased the feature commit onto origin/main and preserved analytics plus motion behavior The only conflict was adjacent App imports from the analytics and animation commits src/App.tsx rebase completed at 7075d4e
+2026-08-31T16:22:00Z verification Passed the complete gate after rebasing onto analytics main Fresh checks included the new analytics tests and regenerated browser evidence on an isolated port coverage summary, build output, Playwright port 8194 929 tests and 118 E2E tests passed
+2026-08-31T16:32:00Z review Accepted all five current-head CodeRabbit findings Each finding reproduced a bounded reduced-motion state or React lifecycle issue PR 20 review threads fixes implemented for fresh verification
diff --git a/e2e/animation-polish.spec.ts b/e2e/animation-polish.spec.ts
new file mode 100644
index 000000000..82a33830f
--- /dev/null
+++ b/e2e/animation-polish.spec.ts
@@ -0,0 +1,128 @@
+import { expect, test, type Page } from '@playwright/test';
+import { stubSupabaseSession } from './helpers/auth';
+
+const featureConfig = { provider: 'mock', model: 'local', temperature: 0.7 };
+const STATE_TIMEOUT_MS = 15_000;
+
+async function prepareMotionFixture(page: Page, reducedMotion: 'no-preference' | 'reduce') {
+ await page.emulateMedia({ reducedMotion });
+ await stubSupabaseSession(page);
+ await page.addInitScript((config) => {
+ window.localStorage.setItem('debate-trainer-settings', JSON.stringify({
+ state: {
+ language: 'en',
+ outputLanguage: 'en',
+ theme: 'light',
+ featureConfigs: {
+ generate: config,
+ attack: config,
+ brainstorm: config,
+ factoids: config,
+ ask: config,
+ sparring: config,
+ },
+ wikiResearchEnabled: false,
+ },
+ version: 0,
+ }));
+ }, featureConfig);
+}
+
+async function openGenerateOptions(page: Page) {
+ await page.goto('/app/generate', { waitUntil: 'domcontentloaded' });
+ await expect(page.getByText('Ready to Generate')).toBeVisible({ timeout: STATE_TIMEOUT_MS });
+ const options = page.locator('#generate-more-options');
+ expect(await options.evaluate((element) => (element as HTMLElement).inert)).toBe(true);
+ await page.getByRole('button', { name: 'More options' }).click();
+ await expect(options).toHaveAttribute('data-open', 'true');
+ expect(await options.evaluate((element) => (element as HTMLElement).inert)).toBe(false);
+ await page.getByRole('button', { name: 'More options' }).click();
+ await expect(options).toHaveAttribute('data-open', 'false');
+ expect(await options.evaluate((element) => (element as HTMLElement).inert)).toBe(true);
+ await page.getByRole('button', { name: 'More options' }).click();
+ await expect(options).toHaveAttribute('data-open', 'true');
+ return options;
+}
+
+async function generateMockCase(page: Page) {
+ await page.getByLabel('Motion / Topic').fill('This House would make public transport free.');
+ await page.getByRole('button', { name: 'Generate Debate Case' }).click();
+ await expect(page.getByText('Reading the motion...')).toBeVisible({ timeout: STATE_TIMEOUT_MS });
+ await expect(page.getByText(/We support the motion/)).toBeVisible({ timeout: STATE_TIMEOUT_MS });
+}
+
+test('keeps route and feature-state motion short and stable', async ({ page }, testInfo) => {
+ await prepareMotionFixture(page, 'no-preference');
+ const options = await openGenerateOptions(page);
+
+ const route = page.locator('[data-route-transition-ms="160"]');
+ await expect(route).toHaveAttribute('data-reduced-motion', 'false');
+ const transitionMs = await options.evaluate((element) => Number.parseFloat(getComputedStyle(element).transitionDuration) * 1000);
+ expect(transitionMs).toBeGreaterThan(0);
+ await page.screenshot({ path: testInfo.outputPath('normal-empty-options.png'), fullPage: true });
+
+ await generateMockCase(page);
+ await expect(page.locator('#generate-results')).toBeVisible();
+ await expect.poll(() => page.evaluate(() => window.scrollY)).toBeGreaterThan(0);
+ await expect(page.locator('main.page-enter')).toHaveCount(0);
+ await expect(page.getByText('Generate complete', { exact: true })).toBeHidden({ timeout: 10_000 });
+ await page.screenshot({ path: testInfo.outputPath('normal-result.png'), fullPage: true });
+});
+
+test('removes non-essential route and state movement for reduced motion', async ({ page }, testInfo) => {
+ await prepareMotionFixture(page, 'reduce');
+ const options = await openGenerateOptions(page);
+
+ const route = page.locator('[data-route-transition-ms="160"]');
+ await expect(route).toHaveAttribute('data-reduced-motion', 'true');
+ await expect.poll(() => options.evaluate((element) => getComputedStyle(element).transitionDuration)).toBe('0s');
+ await expect.poll(() => page.evaluate(() => document.getAnimations().filter((animation) => animation.playState === 'running').length)).toBe(0);
+ await page.screenshot({ path: testInfo.outputPath('reduced-empty-options.png'), fullPage: true });
+
+ await generateMockCase(page);
+ await expect(page.locator('#generate-results')).toBeVisible();
+ await expect.poll(() => page.evaluate(() => window.scrollY)).toBeGreaterThan(0);
+ await expect.poll(() => page.evaluate(() => document.getAnimations().filter((animation) => animation.playState === 'running').length)).toBe(0);
+ await expect(page.getByText('Generate complete', { exact: true })).toBeHidden({ timeout: 10_000 });
+ await page.screenshot({ path: testInfo.outputPath('reduced-result.png'), fullPage: true });
+});
+
+test('audits applicable empty, loading, result, and option states across feature routes', async ({ page }, testInfo) => {
+ test.setTimeout(90_000);
+ await prepareMotionFixture(page, 'no-preference');
+
+ await page.goto('/app/attack', { waitUntil: 'domcontentloaded' });
+ await expect(page.getByText('No Mercy Mode')).toBeVisible({ timeout: STATE_TIMEOUT_MS });
+ const attackOptions = page.locator('#attack-options');
+ await page.getByRole('button', { name: 'More options' }).click();
+ await expect(attackOptions).toHaveAttribute('data-open', 'true');
+ await page.getByRole('button', { name: 'More options' }).click();
+ await expect(attackOptions).toHaveAttribute('data-open', 'false');
+ expect(await attackOptions.evaluate((element) => (element as HTMLElement).inert)).toBe(true);
+ await page.getByLabel('Your Argument').fill('Universal transit improves access, reduces congestion, and strengthens civic participation.');
+ await page.getByRole('button', { name: 'Find Weaknesses' }).click();
+ await expect(page.getByText('Analyzing the case structure...')).toBeVisible({ timeout: STATE_TIMEOUT_MS });
+ await expect(page.getByRole('heading', { name: 'Overall Assessment' })).toBeVisible({ timeout: STATE_TIMEOUT_MS });
+ await page.screenshot({ path: testInfo.outputPath('attack-result.png'), fullPage: true });
+
+ await page.goto('/app/brainstorm', { waitUntil: 'domcontentloaded' });
+ await expect(page.getByText('Unlock Creative Angles')).toBeVisible({ timeout: STATE_TIMEOUT_MS });
+ await page.getByLabel('Motion / Topic').fill('This House would make public transport free.');
+ await page.getByRole('button', { name: 'Start Brainstorming' }).click();
+ await expect(page.getByText('Exploring angles...')).toBeVisible({ timeout: STATE_TIMEOUT_MS });
+ await expect(page.getByRole('heading', { name: 'Top 5 Argument Angles' })).toBeVisible({ timeout: STATE_TIMEOUT_MS });
+
+ await page.goto('/app/factoids', { waitUntil: 'domcontentloaded' });
+ await expect(page.getByText('Evidence That Wins')).toBeVisible({ timeout: STATE_TIMEOUT_MS });
+ await page.getByLabel('Statement').fill('Free public transport reduces urban congestion.');
+ await page.getByRole('button', { name: 'Find Factoids' }).click();
+ await expect(page.getByText('Searching databases...')).toBeVisible({ timeout: STATE_TIMEOUT_MS });
+ await expect(page.getByRole('heading', { name: 'Factoids' })).toBeVisible({ timeout: STATE_TIMEOUT_MS });
+
+ await page.goto('/app/ask', { waitUntil: 'domcontentloaded' });
+ await expect(page.getByText('Start a conversation')).toBeVisible({ timeout: STATE_TIMEOUT_MS });
+ await page.getByPlaceholder('Ask your debate coach...').fill('How should I weigh access against cost?');
+ await page.getByRole('button', { name: 'Send message' }).click();
+ await expect(page.getByText('Thinking...')).toBeVisible({ timeout: STATE_TIMEOUT_MS });
+ await expect(page.getByText(/Great question about/)).toBeVisible({ timeout: STATE_TIMEOUT_MS });
+});
diff --git a/src/App.tsx b/src/App.tsx
index 05efebe28..d52306eba 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -5,7 +5,7 @@ import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom";
-import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
+import { AnimatePresence, MotionConfig, motion, useReducedMotion } from "framer-motion";
import { AppLayout } from "@/components/layout/AppLayout";
import { AuthProvider } from "@/lib/supabase/AuthProvider";
import { RequireAuth } from "@/components/auth/RequireAuth";
@@ -15,6 +15,7 @@ import { useSessionStorageSync } from "@/hooks/use-session-storage-sync";
import { Loader2 } from "lucide-react";
import { OfflineIndicator } from "@/components/OfflineIndicator";
import { captureException, captureProductEvent } from "@/lib/observability";
+import { getRouteMotion, ROUTE_TRANSITION_DURATION_SECONDS } from "@/lib/motion-policy";
// Lazy load all pages for code splitting
const LandingPage = lazy(() => import("./pages/LandingPage"));
@@ -78,7 +79,7 @@ function getToolForPathname(pathname: string): ToolName | undefined {
// Loading fallback component
const PageLoader = () => (
-
+
@@ -131,19 +132,20 @@ export function ToolRouteAnalytics(): null {
function AnimatedRoutes() {
const location = useLocation();
const shouldReduceMotion = useReducedMotion();
+ const routeMotion = getRouteMotion(Boolean(shouldReduceMotion));
useSessionStorageSync();
return (
-
+ }>
+
{/* Public Routes */}
} />
} />
@@ -178,7 +180,8 @@ function AnimatedRoutes() {
} />
} />
-
+
+
);
@@ -188,18 +191,18 @@ const App = () => (
-
-
-
-
-
-
- }>
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
diff --git a/src/components/features/ask/AskChat.tsx b/src/components/features/ask/AskChat.tsx
index 2f3871225..193ae1c9a 100644
--- a/src/components/features/ask/AskChat.tsx
+++ b/src/components/features/ask/AskChat.tsx
@@ -16,6 +16,7 @@ import { validateFileUpload, MAX_FILE_COUNT, formatFileSize } from '@/lib/valida
import { formatEuropeanDate, formatEuropeanTime } from '@/lib/date-format';
import { ModelSelector } from '@/components/ModelSelector';
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet';
+import { useReducedMotion } from 'framer-motion';
interface Message {
id: string;
@@ -368,14 +369,15 @@ export function AskChat({ messages, isLoading, onSend, onClear, onSaveChat, save
const fileInputRef = useRef
(null);
const messagesEndRef = useRef(null);
const { toast } = useToast();
+ const shouldReduceMotion = useReducedMotion();
const { isListening, transcript, error: voiceError, startListening, stopListening, resetTranscript, hasSupport } = useSpeechToText();
const hasStreamingMessage = messages.some((message) => message.isStreaming);
// Auto-scroll to bottom on new messages or streaming content
useEffect(() => {
- messagesEndRef.current?.scrollIntoView?.({ behavior: 'smooth' });
- }, [messages]);
+ messagesEndRef.current?.scrollIntoView?.({ behavior: shouldReduceMotion ? 'auto' : 'smooth' });
+ }, [messages, shouldReduceMotion]);
// Cycle through loading messages
useEffect(() => {
diff --git a/src/components/features/attack/AttackForm.tsx b/src/components/features/attack/AttackForm.tsx
index 8d170e80c..04ff0a69a 100644
--- a/src/components/features/attack/AttackForm.tsx
+++ b/src/components/features/attack/AttackForm.tsx
@@ -1,5 +1,5 @@
// src/components/features/attack/AttackForm.tsx
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
@@ -32,6 +32,11 @@ export function AttackForm({ onSubmit, isLoading, importedCase }: AttackFormProp
const { t } = useTranslation();
const { attackForm, setAttackForm } = useSessionStore();
const [optionsOpen, setOptionsOpen] = useState(false);
+ const advancedOptionsRef = useRef(null);
+
+ useEffect(() => {
+ if (advancedOptionsRef.current) advancedOptionsRef.current.inert = !optionsOpen;
+ }, [optionsOpen]);
const { register, handleSubmit, setValue, watch, formState: { errors } } = useForm({
resolver: zodResolver(AttackInputSchema),
defaultValues: {
@@ -114,9 +119,11 @@ export function AttackForm({ onSubmit, isLoading, importedCase }: AttackFormProp
{/* Settings Grid */}
diff --git a/src/components/features/generate/GenerateForm.tsx b/src/components/features/generate/GenerateForm.tsx
index 699c4f310..be5ab3ceb 100644
--- a/src/components/features/generate/GenerateForm.tsx
+++ b/src/components/features/generate/GenerateForm.tsx
@@ -1,5 +1,5 @@
// src/components/features/generate/GenerateForm.tsx
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
@@ -31,6 +31,11 @@ export function GenerateForm({ onSubmit, isLoading }: GenerateFormProps) {
const { t } = useTranslation();
const { generateForm, setGenerateForm } = useSessionStore();
const [advancedOpen, setAdvancedOpen] = useState(false);
+ const advancedOptionsRef = useRef(null);
+
+ useEffect(() => {
+ if (advancedOptionsRef.current) advancedOptionsRef.current.inert = !advancedOpen;
+ }, [advancedOpen]);
const { register, handleSubmit, setValue, watch, formState: { errors } } = useForm({
resolver: zodResolver(GenerateInputSchema),
@@ -198,9 +203,11 @@ export function GenerateForm({ onSubmit, isLoading }: GenerateFormProps) {
{/* Length */}
diff --git a/src/components/landing/Features.tsx b/src/components/landing/Features.tsx
index ba8464a82..4b758882f 100644
--- a/src/components/landing/Features.tsx
+++ b/src/components/landing/Features.tsx
@@ -61,7 +61,7 @@ const Features = () => {
className="feature-card group tilt-card"
>
-
+
{feature.title}
{feature.description}
diff --git a/src/components/landing/Hero.tsx b/src/components/landing/Hero.tsx
index cba207e07..78e9dd9a6 100644
--- a/src/components/landing/Hero.tsx
+++ b/src/components/landing/Hero.tsx
@@ -1,6 +1,6 @@
import React from "react";
import { useTranslation } from "react-i18next";
-import { motion } from "framer-motion";
+import { motion, useReducedMotion } from "framer-motion";
import { ArrowRight, CheckCircle2, Sparkles } from "lucide-react";
const Hero = () => {
@@ -128,11 +128,15 @@ const PHASE_TIMINGS: Record
= {
impact: 1800,
};
-const TypewriterText = ({ text, durationMs }: { text: string; durationMs: number }) => {
+const TypewriterText = ({ text, durationMs, shouldReduceMotion }: { text: string; durationMs: number; shouldReduceMotion: boolean }) => {
const [displayed, setDisplayed] = React.useState("");
const intervalRef = React.useRef | null>(null);
React.useEffect(() => {
+ if (shouldReduceMotion) {
+ setDisplayed(text);
+ return;
+ }
let i = 0;
setDisplayed("");
const charInterval = durationMs / text.length;
@@ -142,21 +146,26 @@ const TypewriterText = ({ text, durationMs }: { text: string; durationMs: number
if (i >= text.length && intervalRef.current) clearInterval(intervalRef.current);
}, charInterval);
return () => { if (intervalRef.current) clearInterval(intervalRef.current); };
- }, [text, durationMs]);
+ }, [text, durationMs, shouldReduceMotion]);
return <>{displayed}|>;
};
const HeroPreview = () => {
const { t } = useTranslation();
+ const shouldReduceMotion = Boolean(useReducedMotion());
const outputContent = {
thesis: t('hero.preview.thesis'),
contention: t('hero.preview.contention'),
impact: t('hero.preview.outputImpact'),
};
- const [phase, setPhase] = React.useState("idle");
+ const [phase, setPhase] = React.useState(shouldReduceMotion ? "impact" : "idle");
React.useEffect(() => {
+ if (shouldReduceMotion) {
+ setPhase("impact");
+ return;
+ }
const phases: Phase[] = ["idle", "shimmer", "generating", "thesis", "contention", "impact"];
let idx = 0;
let timeout: ReturnType;
@@ -169,7 +178,7 @@ const HeroPreview = () => {
timeout = setTimeout(advance, PHASE_TIMINGS[phases[0]]);
return () => clearTimeout(timeout);
- }, []);
+ }, [shouldReduceMotion]);
const showThesis = phase === "thesis" || phase === "contention" || phase === "impact";
const showContention = phase === "contention" || phase === "impact";
@@ -242,42 +251,42 @@ const HeroPreview = () => {
{showThesis && (
{t('hero.preview.mainArgument')}
- {phase === "thesis" ? : outputContent.thesis}
+ {phase === "thesis" ? : outputContent.thesis}
)}
{showContention && (
{t('hero.preview.reasoning')}
- {phase === "contention" ? : outputContent.contention}
+ {phase === "contention" ? : outputContent.contention}
)}
{showImpact && (
{t('hero.preview.impact')}
-
+
)}
diff --git a/src/components/landing/HowItWorks.tsx b/src/components/landing/HowItWorks.tsx
index d4f5e655f..c7e4c99a6 100644
--- a/src/components/landing/HowItWorks.tsx
+++ b/src/components/landing/HowItWorks.tsx
@@ -43,7 +43,7 @@ const HowItWorks = () => {
className="relative flex flex-col items-center text-center"
>
-
+
diff --git a/src/components/landing/Navbar.tsx b/src/components/landing/Navbar.tsx
index 34da7d7d6..3bd9b55bf 100644
--- a/src/components/landing/Navbar.tsx
+++ b/src/components/landing/Navbar.tsx
@@ -1,12 +1,13 @@
import { useTranslation } from "react-i18next";
import { useState } from "react";
-import { motion, AnimatePresence } from "framer-motion";
+import { motion, AnimatePresence, useReducedMotion } from "framer-motion";
import { Menu, X, ArrowRight } from "lucide-react";
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
const Navbar = () => {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
+ const shouldReduceMotion = useReducedMotion();
const navLinks = [
{ href: "#features", label: t('nav.features') },
@@ -70,7 +71,7 @@ const Navbar = () => {
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
- transition={{ duration: 0.3 }}
+ transition={{ duration: shouldReduceMotion ? 0 : 0.3 }}
className="border-b border-border bg-background/95 backdrop-blur-xl md:hidden"
>
diff --git a/src/components/landing/Pricing.tsx b/src/components/landing/Pricing.tsx
index 80860bccf..142315122 100644
--- a/src/components/landing/Pricing.tsx
+++ b/src/components/landing/Pricing.tsx
@@ -47,7 +47,7 @@ const Pricing = () => {
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
viewport={{ once: true }}
- className="rounded-2xl border border-border/70 bg-card/80 p-7 shadow-sm backdrop-blur transition-all duration-300 hover:-translate-y-1 hover:shadow-lg"
+ className="rounded-2xl border border-border/70 bg-card/80 p-7 shadow-sm backdrop-blur transition-all duration-300 hover:-translate-y-1 hover:shadow-lg motion-reduce:hover:translate-y-0"
>
{t('pricing.trial')}
@@ -80,7 +80,7 @@ const Pricing = () => {
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
viewport={{ once: true }}
- className="relative rounded-2xl border-2 border-primary/45 bg-card/85 p-7 shadow-xl backdrop-blur ring-1 ring-primary/10 transition-all duration-300 hover:-translate-y-1"
+ className="relative rounded-2xl border-2 border-primary/45 bg-card/85 p-7 shadow-xl backdrop-blur ring-1 ring-primary/10 transition-all duration-300 hover:-translate-y-1 motion-reduce:hover:translate-y-0"
>
diff --git a/src/components/landing/WhatsNew.tsx b/src/components/landing/WhatsNew.tsx
index ee6263128..228fbea74 100644
--- a/src/components/landing/WhatsNew.tsx
+++ b/src/components/landing/WhatsNew.tsx
@@ -42,11 +42,11 @@ const WhatsNew = () => {
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45, delay: index * 0.08 }}
viewport={{ once: true }}
- className="group rounded-2xl border border-border/70 bg-card/75 p-6 shadow-sm backdrop-blur-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-lg"
+ className="group rounded-2xl border border-border/70 bg-card/75 p-6 shadow-sm backdrop-blur-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-lg motion-reduce:hover:translate-y-0"
>
-
+
{update.badge}
diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx
index a1eed638a..f532b1244 100644
--- a/src/components/layout/AppLayout.tsx
+++ b/src/components/layout/AppLayout.tsx
@@ -1,6 +1,6 @@
// src/components/layout/AppLayout.tsx
import { useState } from 'react';
-import { Outlet } from 'react-router-dom';
+import { Link, Outlet } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
Home,
@@ -49,14 +49,14 @@ export function AppLayout() {
{/* Header */}
-
+ setMobileMenuOpen(false)} className="flex items-center gap-3 shrink-0">
DebateLab
-
+
{/* Desktop Nav */}