diff --git a/src/features/auth/components/FloatingShape.tsx b/src/features/auth/components/FloatingShape.tsx
new file mode 100644
index 0000000..5682dd6
--- /dev/null
+++ b/src/features/auth/components/FloatingShape.tsx
@@ -0,0 +1,46 @@
+import { motion } from 'framer-motion'
+
+/**
+ * FloatingShape — the shared decorative background orb for every auth shell
+ * (dashboard login + register + MFA + the standalone auth pages).
+ *
+ * ONE-SHOT fade/scale-in only (no `repeat: Infinity`) with a soft radial fill
+ * instead of an animated `backdrop-filter: blur`. The previous orb ran an
+ * INFINITE framer-motion loop AND re-blurred its backdrop every frame, which —
+ * layered over the (now removed) animated shell gradient — was a continuous
+ * main-thread compositing cost (worst with hardware-acceleration off) that
+ * contributed to the auth-page jank. After the entrance transition this orb is
+ * fully static, so it adds zero per-frame work while staying as brand
+ * decoration. See loginBackground.ts for the matching static-gradient fix.
+ *
+ * Callers gate the orbs behind `!usePrefersReducedMotion()` so users who ask
+ * for reduced motion get no decorative chrome at all.
+ */
+const FloatingShape = ({ delay, size, left, top }: {
+ delay: number
+ size: number
+ left: string
+ top: string
+}) => (
+
+)
+
+export default FloatingShape
diff --git a/src/features/auth/components/LoginPage.tsx b/src/features/auth/components/LoginPage.tsx
index 7cba457..9562d43 100644
--- a/src/features/auth/components/LoginPage.tsx
+++ b/src/features/auth/components/LoginPage.tsx
@@ -55,6 +55,7 @@ import { hasPasswordLayer1, selectPuzzleConfig, type LoginConfig } from '@domain
import StepProgress from '../../../verify-app/StepProgress'
import { usePrefersReducedMotion } from '@hooks/usePrefersReducedMotion'
import { loginShellBackgroundSx, glassCardBackdropFilter } from './loginBackground'
+import FloatingShape from './FloatingShape'
/**
* Login form validation schema
@@ -108,40 +109,6 @@ const logoVariants: Variants = {
},
}
-// Decorative background orb. ONE-SHOT fade/scale-in only (no `repeat: Infinity`)
-// and a soft radial fill instead of an animated `backdrop-filter: blur` — the
-// continuous orb animation + per-frame backdrop re-blur was a main-thread compositing
-// cost (worst with hardware-acceleration off) that contributed to the login jank.
-// After the entrance it is fully static, so it adds zero per-frame work — the orbs
-// stay as brand decoration without the cost. See loginBackground.ts for the matching
-// static-gradient fix.
-const FloatingShape = ({ delay, size, left, top }: {
- delay: number
- size: number
- left: string
- top: string
-}) => (
-
-)
-
/**
* Login Page Component
* Beautiful animated login with glassmorphism design
diff --git a/src/features/auth/components/RegisterPage.tsx b/src/features/auth/components/RegisterPage.tsx
index c5a8b44..9d7d9c1 100644
--- a/src/features/auth/components/RegisterPage.tsx
+++ b/src/features/auth/components/RegisterPage.tsx
@@ -39,6 +39,9 @@ import { getService } from '@core/di/container'
import { TYPES } from '@core/di/types'
import type { IHttpClient } from '@domain/interfaces/IHttpClient'
import { isLikelyValidEmail } from '@domain/validators/emailValidator'
+import { usePrefersReducedMotion } from '@hooks/usePrefersReducedMotion'
+import { loginShellBackgroundSx } from './loginBackground'
+import FloatingShape from './FloatingShape'
/**
* Register form validation schema
@@ -117,39 +120,6 @@ const logoVariants: Variants = {
},
}
-// Floating shapes for background
-const FloatingShape = ({ delay, size, left, top }: {
- delay: number
- size: number
- left: string
- top: string
-}) => (
-
-)
-
/**
* Register Page Component
* Beautiful animated registration with glassmorphism design
@@ -157,6 +127,7 @@ const FloatingShape = ({ delay, size, left, top }: {
export default function RegisterPage() {
const { t } = useTranslation()
const navigate = useNavigate()
+ const prefersReducedMotion = usePrefersReducedMotion()
const [showPassword, setShowPassword] = useState(false)
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
const [loading, setLoading] = useState(false)
@@ -226,23 +197,20 @@ export default function RegisterPage() {
justifyContent: 'center',
position: 'relative',
overflow: 'hidden',
- background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f64f59 100%)',
- backgroundSize: '400% 400%',
- animation: 'gradientShift 15s ease infinite',
- '@keyframes gradientShift': {
- '0%': { backgroundPosition: '0% 50%' },
- '50%': { backgroundPosition: '100% 50%' },
- '100%': { backgroundPosition: '0% 50%' },
- },
py: 4,
+ ...loginShellBackgroundSx(),
}}
>
- {/* Animated background shapes */}
-
-
-
-
-
+ {/* Decorative background shapes — skipped for reduced-motion users. */}
+ {!prefersReducedMotion && (
+ <>
+
+
+
+
+
+ >
+ )}
(
-
-)
-
export default function AcceptInvitePage() {
const navigate = useNavigate()
const { t } = useTranslation()
+ const prefersReducedMotion = usePrefersReducedMotion()
const [searchParams] = useSearchParams()
const token = searchParams.get('token') || ''
const [loading, setLoading] = useState(false)
@@ -203,21 +184,19 @@ export default function AcceptInvitePage() {
justifyContent: 'center',
position: 'relative',
overflow: 'hidden',
- background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f64f59 100%)',
- backgroundSize: '400% 400%',
- animation: 'gradientShift 15s ease infinite',
- '@keyframes gradientShift': {
- '0%': { backgroundPosition: '0% 50%' },
- '50%': { backgroundPosition: '100% 50%' },
- '100%': { backgroundPosition: '0% 50%' },
- },
+ ...loginShellBackgroundSx(),
}}
>
-
-
-
-
-
+ {/* Decorative background shapes — skipped for reduced-motion users. */}
+ {!prefersReducedMotion && (
+ <>
+
+
+
+
+
+ >
+ )}
(
-
-)
-
export default function ForgotPasswordPage() {
const navigate = useNavigate()
const { t } = useTranslation()
+ const prefersReducedMotion = usePrefersReducedMotion()
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
@@ -148,21 +120,19 @@ export default function ForgotPasswordPage() {
justifyContent: 'center',
position: 'relative',
overflow: 'hidden',
- background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f64f59 100%)',
- backgroundSize: '400% 400%',
- animation: 'gradientShift 15s ease infinite',
- '@keyframes gradientShift': {
- '0%': { backgroundPosition: '0% 50%' },
- '50%': { backgroundPosition: '100% 50%' },
- '100%': { backgroundPosition: '0% 50%' },
- },
+ ...loginShellBackgroundSx(),
}}
>
-
-
-
-
-
+ {/* Decorative background shapes — skipped for reduced-motion users. */}
+ {!prefersReducedMotion && (
+ <>
+
+
+
+
+
+ >
+ )}
(
-
-)
-
export default function OnboardingPage() {
const navigate = useNavigate()
const { t } = useTranslation()
+ const prefersReducedMotion = usePrefersReducedMotion()
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [submittedEmail, setSubmittedEmail] = useState(null)
@@ -201,21 +182,19 @@ export default function OnboardingPage() {
position: 'relative',
overflow: 'hidden',
py: 4,
- background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f64f59 100%)',
- backgroundSize: '400% 400%',
- animation: 'gradientShift 15s ease infinite',
- '@keyframes gradientShift': {
- '0%': { backgroundPosition: '0% 50%' },
- '50%': { backgroundPosition: '100% 50%' },
- '100%': { backgroundPosition: '0% 50%' },
- },
+ ...loginShellBackgroundSx(),
}}
>
-
-
-
-
-
+ {/* Decorative background shapes — skipped for reduced-motion users. */}
+ {!prefersReducedMotion && (
+ <>
+
+
+
+
+
+ >
+ )}
(
-
-)
-
export default function ResetPasswordPage() {
const navigate = useNavigate()
const { t } = useTranslation()
+ const prefersReducedMotion = usePrefersReducedMotion()
const [searchParams] = useSearchParams()
const email = searchParams.get('email') || ''
const [loading, setLoading] = useState(false)
@@ -210,21 +182,19 @@ export default function ResetPasswordPage() {
justifyContent: 'center',
position: 'relative',
overflow: 'hidden',
- background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f64f59 100%)',
- backgroundSize: '400% 400%',
- animation: 'gradientShift 15s ease infinite',
- '@keyframes gradientShift': {
- '0%': { backgroundPosition: '0% 50%' },
- '50%': { backgroundPosition: '100% 50%' },
- '100%': { backgroundPosition: '0% 50%' },
- },
+ ...loginShellBackgroundSx(),
}}
>
-
-
-
-
-
+ {/* Decorative background shapes — skipped for reduced-motion users. */}
+ {!prefersReducedMotion && (
+ <>
+
+
+
+
+
+ >
+ )}
(
-
-)
-
type VerifyState = 'verifying' | 'success' | 'error' | 'noToken'
export default function VerifyEmailPage() {
const navigate = useNavigate()
const { t } = useTranslation()
+ const prefersReducedMotion = usePrefersReducedMotion()
const [searchParams] = useSearchParams()
const token = searchParams.get('token') || ''
const [state, setState] = useState(token ? 'verifying' : 'noToken')
@@ -138,20 +119,18 @@ export default function VerifyEmailPage() {
justifyContent: 'center',
position: 'relative',
overflow: 'hidden',
- background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f64f59 100%)',
- backgroundSize: '400% 400%',
- animation: 'gradientShift 15s ease infinite',
- '@keyframes gradientShift': {
- '0%': { backgroundPosition: '0% 50%' },
- '50%': { backgroundPosition: '100% 50%' },
- '100%': { backgroundPosition: '0% 50%' },
- },
+ ...loginShellBackgroundSx(),
}}
>
-
-
-
-
+ {/* Decorative background shapes — skipped for reduced-motion users. */}
+ {!prefersReducedMotion && (
+ <>
+
+
+
+
+ >
+ )}