diff --git a/frontend/src/app/components/OnboardingV3/OnboardingV3Root.tsx b/frontend/src/app/components/OnboardingV3/OnboardingV3Root.tsx
index c0cbc153..18f9d03c 100644
--- a/frontend/src/app/components/OnboardingV3/OnboardingV3Root.tsx
+++ b/frontend/src/app/components/OnboardingV3/OnboardingV3Root.tsx
@@ -73,10 +73,12 @@ function useOnboardingV3Gate(): boolean {
const IntroBeat: React.FC<{ c: ClaudeTokens; line: string; sub?: string; onNext: () => void }> = ({ c, line, sub, onNext }) => (
+ {/* Opacity and transform only. An animated blur re-rasterises the whole headline every frame,
+ and this is the first thing anyone ever sees of the product. */}
{line}
@@ -85,7 +87,7 @@ const IntroBeat: React.FC<{ c: ClaudeTokens; line: string; sub?: string; onNext:
{sub}
@@ -95,7 +97,7 @@ const IntroBeat: React.FC<{ c: ClaudeTokens; line: string; sub?: string; onNext:
onClick={onNext}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
- transition={{ duration: 0.5, delay: 1.05 }}
+ transition={{ duration: 0.4, delay: 0.34 }}
whileHover={{ scale: 1.06 }}
style={{
position: 'relative', marginTop: 40, width: 54, height: 40, borderRadius: 12, border: 'none',
@@ -176,8 +178,8 @@ const OnboardingV3Root: React.FC = () => {
}}
>
window.clearTimeout(t);
}, []);
+ // Every beat's content rides this, so an animated blur here is paid once per element per frame,
+ // on every screen of the flow. Opacity and transform get the same read for free.
const enter = (delay: number) => ({
- initial: { opacity: 0, y: 14, filter: 'blur(6px)' },
- animate: { opacity: 1, y: 0, filter: 'blur(0px)' },
+ initial: { opacity: 0, y: 14 },
+ animate: { opacity: 1, y: 0 },
transition: { ...SPRING, delay },
});