From 357c9fd102f95a6171d56f9a83f0ff8e1fe671ae Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 9 Jun 2026 11:23:20 +0300 Subject: [PATCH 01/15] feat(onboarding): casino-style polish for persona quiz buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rework the buttons and interactions in the Patchy persona quiz step for a gamified, slot-machine feel: - Primary CTAs gain a breathing neon halo + shine sweep on hover, scale on press - Yes/No answers behave like tactile chips with color-coded glow (avocado / ketchup) and icon tint; preserve the downvote's 180° rotation on hover so the arrow no longer flips to point up - Persona/option/picker/modifier rows get a reach-for-it glow, emoji wink and press feedback; selected modifier ticks pop in - Progress bar fills with an animated gold→grape shimmer All effects use design-system tokens and are disabled under prefers-reduced-motion. Co-Authored-By: Claude Opus 4.8 --- .../steps/FunnelPersonaQuiz.module.css | 163 +++++++++++++++++- .../onboarding/steps/FunnelPersonaQuiz.tsx | 76 ++++++-- 2 files changed, 219 insertions(+), 20 deletions(-) diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css index 1b00cc35f6..94d0759d07 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css @@ -53,12 +53,173 @@ animation: reveal-rise 0.5s ease 0.45s both; } +/* ─── Casino / arcade micro-interactions ────────────────────────────────── + * Patchy is a genie, so the whole step leans into a "make a wish" slot-machine + * feel: the primary CTA breathes a neon halo and flashes a shine sweep on + * hover, the yes/no answers behave like tactile chips with a color-coded glow, + * and the option cards light up as you reach for them. Everything is paired + * with cheap transform/opacity work and fully disabled under reduced motion. */ + +@keyframes cta-pulse { + 0%, + 100% { + box-shadow: 0 6px 20px -10px + color-mix(in srgb, var(--theme-accent-cabbage-default) 80%, transparent); + } + 50% { + box-shadow: 0 8px 26px -6px + color-mix(in srgb, var(--theme-accent-cabbage-default) 95%, transparent), + 0 0 26px -2px + color-mix(in srgb, var(--theme-accent-bacon-default) 75%, transparent); + } +} + +@keyframes shine-sweep { + from { + transform: translateX(-180%) skewX(-16deg); + } + to { + transform: translateX(320%) skewX(-16deg); + } +} + +@keyframes bar-shimmer { + to { + background-position: -200% 0; + } +} + +@keyframes tick-pop { + 0% { + transform: scale(0.4); + opacity: 0; + } + 60% { + transform: scale(1.18); + } + 100% { + transform: scale(1); + opacity: 1; + } +} + +/* Primary "play" button: a living halo plus a shine that sweeps across on + * hover. overflow-hidden clips the shine; the pseudo-element never eats + * pointer events so the click target is unchanged. */ +.cta { + position: relative; + overflow: hidden; + isolation: isolate; + animation: cta-pulse 2.6s ease-in-out infinite; +} + +.cta::after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 36%; + transform: translateX(-180%) skewX(-16deg); + background: linear-gradient( + 90deg, + transparent, + rgb(255 255 255 / 45%), + transparent + ); + pointer-events: none; +} + +.cta:hover::after { + animation: shine-sweep 0.8s ease-out; +} + +/* Yes / No answer chips. The compound selector keeps the colored glow ahead of + * the shared `.btn:hover` lift shadow regardless of stylesheet order. */ +.chip { + transition: transform 0.18s ease, box-shadow 0.18s ease, + border-color 0.18s ease; +} + +.chip svg { + transition: transform 0.18s ease, color 0.18s ease; +} + +.chip.chipYes:hover { + border-color: var(--theme-accent-avocado-default); + box-shadow: 0 0 24px -4px + color-mix(in srgb, var(--theme-accent-avocado-default) 70%, transparent); +} + +.chip.chipYes:hover svg { + color: var(--theme-accent-avocado-default); + transform: scale(1.12); +} + +.chip.chipNo:hover { + border-color: var(--theme-accent-ketchup-default); + box-shadow: 0 0 24px -4px + color-mix(in srgb, var(--theme-accent-ketchup-default) 70%, transparent); +} + +/* DownvoteIcon is UpvoteIcon + `rotate-180`, so the hover transform must + * re-apply that rotation — a bare scale() would clobber it and flip the arrow + * back to pointing up. */ +.chip.chipNo:hover svg { + color: var(--theme-accent-ketchup-default); + transform: rotate(0.5turn) scale(1.12); +} + +/* Persona / option cards: reach-for-it glow + emoji wink. */ +.card { + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; +} + +.card:hover { + box-shadow: 0 14px 34px -16px + color-mix(in srgb, var(--theme-accent-cabbage-default) 90%, transparent); +} + +.cardEmoji { + transition: transform 0.2s ease; +} + +.card:hover .cardEmoji { + transform: scale(1.16) rotate(-6deg); +} + +/* Selected modifier tick. */ +.tick { + animation: tick-pop 0.28s ease both; +} + +/* Progress fill: a slow gold→grape shimmer so the bar feels alive while it + * fills, not a static block. */ +.progressFill { + background-image: linear-gradient( + 90deg, + var(--theme-accent-cabbage-default), + var(--theme-accent-bacon-default), + var(--theme-accent-cheese-default), + var(--theme-accent-cabbage-default) + ); + background-size: 200% 100%; + animation: bar-shimmer 3s linear infinite; +} + @media (prefers-reduced-motion: reduce) { .questionIn, .dot, .revealName, .revealTagline, - .revealActions { + .revealActions, + .cta, + .progressFill, + .tick { animation: none; } + + .cta::after { + display: none; + } } diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx index 5187b25125..e64375b06c 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx @@ -356,7 +356,10 @@ const QuizStage = ({
@@ -385,12 +388,16 @@ const PersonaCard = ({ key={persona.id} type="button" onClick={() => onSelect(persona.id)} - className="flex flex-col items-center gap-2 rounded-16 border-2 border-border-subtlest-tertiary bg-surface-float p-6 text-center transition-all hover:-translate-y-1 hover:border-accent-cabbage-default tablet:p-8" + className={classNames( + styles.card, + 'flex flex-col items-center gap-2 rounded-16 border-2 border-border-subtlest-tertiary bg-surface-float p-6 text-center hover:-translate-y-1 hover:border-accent-cabbage-default active:translate-y-0 active:scale-[0.98] tablet:p-8', + )} > {persona.emoji} @@ -516,7 +523,10 @@ function FunnelPersonaQuizComponent({
); })}
- + @@ -553,8 +561,9 @@ function FunnelPersonaQuizComponent({ return (
+ Who are you, really? @@ -690,7 +699,7 @@ function FunnelPersonaQuizComponent({ /> ))}
- + ); @@ -789,12 +798,13 @@ function FunnelPersonaQuizComponent({ ); })} - - + )} @@ -928,7 +939,7 @@ function FunnelPersonaQuizComponent({ )} >
- - +
- + {isThinking && ( From ef45bd86ac53d3cea2d23adc526a129856899100 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 9 Jun 2026 11:51:30 +0300 Subject: [PATCH 03/15] feat(onboarding): redesign persona quiz as a spotlight stage Reworks the look and feel based on review: - Full-bleed "spotlight stage" backdrop via fixed positioning, so it no longer gets clipped by the content column. Replaces the flat funnel gradient with a soft cabbage spotlight from the top, a warm pool of lamp-light at the base, a vignette that pulls focus to the centre, a slow-drifting aurora and faint twinkling "magic dust". Demo background switched to Blank so the stage owns the look. - Primary CTAs are white again for maximum contrast, lit by a soft luminous halo (white light with a faint cabbage rim) instead of a purple fill. - Speech bubble is now a frosted-glass panel (translucent, blurred, glass top-edge highlight, rounded-24) in place of the flat bordered box. All colors use design-system tokens; backdrop + animations respect prefers-reduced-motion. Co-Authored-By: Claude Opus 4.8 --- .../steps/FunnelPersonaQuiz.module.css | 134 ++++++++++++++---- .../onboarding/steps/FunnelPersonaQuiz.tsx | 17 +-- .../webapp/pages/onboarding-persona-demo.tsx | 2 +- 3 files changed, 110 insertions(+), 43 deletions(-) diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css index 9822edacc3..f2ee058f83 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css @@ -60,17 +60,18 @@ * and the option cards light up as you reach for them. Everything is paired * with cheap transform/opacity work and fully disabled under reduced motion. */ +/* White CTA sitting in a spotlight: a soft luminous halo that breathes, so the + * button reads as the lit object on the stage. Neutral white light with the + * faintest cabbage rim — never tints the white fill. */ @keyframes cta-pulse { 0%, 100% { - box-shadow: 0 6px 20px -10px - color-mix(in srgb, var(--theme-accent-cabbage-default) 80%, transparent); + box-shadow: 0 10px 34px -16px rgb(255 255 255 / 35%); } 50% { - box-shadow: 0 8px 26px -6px - color-mix(in srgb, var(--theme-accent-cabbage-default) 95%, transparent), - 0 0 26px -2px - color-mix(in srgb, var(--theme-accent-bacon-default) 75%, transparent); + box-shadow: 0 12px 42px -14px rgb(255 255 255 / 55%), + 0 0 24px -6px + color-mix(in srgb, var(--theme-accent-cabbage-default) 45%, transparent); } } @@ -103,44 +104,121 @@ } } -/* Casino "stage" backdrop: layered radial glows that sit behind the whole - * step — a purple spotlight up top, warm gold/pink felt in the lower corners — - * so the page reads like a lit game table rather than a flat funnel screen. - * Lives behind the content via a negative z-index inside an isolated parent. */ +@keyframes aurora-drift { + from { + transform: translate(-58%, -3%) scale(1); + } + to { + transform: translate(-42%, 5%) scale(1.08); + } +} + +@keyframes star-twinkle { + 0%, + 100% { + opacity: 0.35; + } + 50% { + opacity: 0.7; + } +} + +/* Spotlight "stage" backdrop. Full-bleed via `fixed` so it never gets clipped + * by the content column, and sits behind the content through a negative + * z-index inside the isolated stage. A single soft cabbage spotlight from the + * top, a faint warm pool of lamp-light at the base, and a vignette that pulls + * focus to the centre — deep and premium rather than a flat funnel gradient. */ .stageBackdrop { - position: absolute; + position: fixed; inset: 0; z-index: -1; pointer-events: none; + overflow: hidden; background: radial-gradient( - 62% 48% at 50% -6%, - color-mix(in srgb, var(--theme-accent-cabbage-default) 36%, transparent), - transparent 70% + 120% 95% at 50% 32%, + transparent 52%, + rgb(0 0 0 / 55%) 100% ), radial-gradient( - 50% 44% at 90% 110%, - color-mix(in srgb, var(--theme-accent-bacon-default) 28%, transparent), + 46% 36% at 50% 104%, + color-mix(in srgb, var(--theme-accent-bun-default) 14%, transparent), transparent 72% ), radial-gradient( - 44% 40% at 6% 106%, - color-mix(in srgb, var(--theme-accent-cheese-default) 20%, transparent), - transparent 72% + 72% 58% at 50% -12%, + color-mix(in srgb, var(--theme-accent-cabbage-default) 20%, transparent), + transparent 66% ); } -/* Faint dotted "felt" texture layered over the glows for tactile depth. */ +/* Slow-drifting aurora blob — gives the spotlight life without motion noise. */ +.stageBackdrop::before { + content: ''; + position: absolute; + left: 50%; + top: -18%; + width: 90vw; + height: 70vh; + transform: translate(-50%, 0); + background: radial-gradient( + closest-side, + color-mix(in srgb, var(--theme-accent-cabbage-default) 24%, transparent), + transparent + ); + filter: blur(48px); + animation: aurora-drift 16s ease-in-out infinite alternate; +} + +/* Sparse "magic dust" starfield — subtle, masked to the stage centre. */ .stageBackdrop::after { content: ''; position: absolute; inset: 0; background-image: radial-gradient( - color-mix(in srgb, var(--theme-text-primary) 10%, transparent) 1px, - transparent 1px - ); - background-size: 24px 24px; - opacity: 0.55; - mask-image: radial-gradient(85% 85% at 50% 25%, #000, transparent 78%); + 1.5px 1.5px at 18% 24%, + color-mix(in srgb, var(--theme-text-primary) 55%, transparent), + transparent + ), + radial-gradient( + 1.5px 1.5px at 72% 16%, + color-mix(in srgb, var(--theme-text-primary) 45%, transparent), + transparent + ), + radial-gradient( + 1px 1px at 84% 60%, + color-mix(in srgb, var(--theme-text-primary) 45%, transparent), + transparent + ), + radial-gradient( + 1px 1px at 30% 72%, + color-mix(in srgb, var(--theme-text-primary) 40%, transparent), + transparent + ), + radial-gradient( + 1.5px 1.5px at 58% 84%, + color-mix(in srgb, var(--theme-text-primary) 40%, transparent), + transparent + ), + radial-gradient( + 1px 1px at 8% 54%, + color-mix(in srgb, var(--theme-text-primary) 40%, transparent), + transparent + ); + animation: star-twinkle 5s ease-in-out infinite; + mask-image: radial-gradient(75% 70% at 50% 35%, #000, transparent 80%); +} + +/* Frosted-glass panel for Patchy's lines: translucent, blurred, with a glass + * top-edge highlight. Replaces the flat bordered speech bubble. */ +.panel { + border-radius: 24px; + background: color-mix(in srgb, var(--theme-surface-float) 50%, transparent); + backdrop-filter: blur(18px) saturate(1.1); + -webkit-backdrop-filter: blur(18px) saturate(1.1); + border: 1px solid + color-mix(in srgb, var(--theme-text-primary) 12%, transparent); + box-shadow: 0 28px 64px -34px rgb(0 0 0 / 65%), + inset 0 1px 0 0 color-mix(in srgb, var(--theme-text-primary) 10%, transparent); } /* Primary "play" button: a living halo plus a shine that sweeps across on @@ -255,7 +333,9 @@ .revealActions, .cta, .progressFill, - .tick { + .tick, + .stageBackdrop::before, + .stageBackdrop::after { animation: none; } diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx index 078ea13744..3b4d9795a5 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx @@ -11,7 +11,6 @@ import type { FunnelStepPersonaQuiz } from '../types/funnel'; import { FunnelStepTransitionType } from '../types/funnel'; import { withIsActiveGuard } from '../shared/withActiveGuard'; import { - ButtonColor, ButtonSize, ButtonV2, ButtonVariant, @@ -298,21 +297,12 @@ const SpeechBubble = ({ }): ReactElement => (
{children} - {/* On mobile Patchy sits above the bubble, so the tail points up. */} - - {/* On laptop Patchy sits to the right, so the tail points right. */} -
); @@ -535,7 +525,6 @@ function FunnelPersonaQuizComponent({ 'w-full transition-transform duration-200 ease-out hover:scale-[1.03] active:scale-[0.97] laptop:w-auto', )} variant={ButtonVariant.Primary} - color={ButtonColor.Cabbage} size={ButtonSize.XLarge} onClick={start} type="button" @@ -804,7 +793,6 @@ function FunnelPersonaQuizComponent({ 'mt-auto w-full transition-transform duration-200 ease-out hover:scale-[1.03] active:scale-[0.97] laptop:mt-0 laptop:w-auto', )} variant={ButtonVariant.Primary} - color={ButtonColor.Cabbage} size={ButtonSize.XLarge} onClick={handleComplete} type="button" @@ -869,7 +857,6 @@ function FunnelPersonaQuizComponent({ 'w-full transition-transform duration-200 ease-out hover:scale-[1.03] active:scale-[0.97] laptop:w-auto', )} variant={ButtonVariant.Primary} - color={ButtonColor.Cabbage} size={ButtonSize.XLarge} onClick={confirmPersona} type="button" diff --git a/packages/webapp/pages/onboarding-persona-demo.tsx b/packages/webapp/pages/onboarding-persona-demo.tsx index 3194a06e95..cafdc0714c 100644 --- a/packages/webapp/pages/onboarding-persona-demo.tsx +++ b/packages/webapp/pages/onboarding-persona-demo.tsx @@ -26,7 +26,7 @@ function PersonaQuizDemo(): ReactElement { type: FunnelStepType.PersonaQuiz, isActive: true, parameters: { - backgroundType: FunnelBackgroundVariant.Default, + backgroundType: FunnelBackgroundVariant.Blank, mascotVideoBaseUrl: '/onboarding/patchy', }, transitions: [], From 4a1f86b0a779070e52fefded24a7ca8f49dabc95 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 9 Jun 2026 12:20:40 +0300 Subject: [PATCH 04/15] feat(onboarding): more magic + redesigned answers for persona quiz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addressing review feedback on the spotlight stage: - Magic background: a second cooler aurora that drifts and breathes out of phase with the cabbage one, so the spotlight slowly shifts purple → blue, plus floating "magic dust" specks that rise from the lamp and fade. - Thought-cloud connector: glassy trailing circles from the speech panel toward Patchy (up on mobile, out to the right on laptop) so the message reads as coming from him. - Yes / No are now prominent glass answer tiles with a tinted icon badge that fills with its verdict colour on hover (avocado / ketchup), replacing the thin secondary outline. - "Not sure" redesigned as a dashed ghost pill — clearly visible yet plainly secondary to the answer tiles. All colors use design-system tokens; new animations respect prefers-reduced-motion (dust is hidden entirely). Co-Authored-By: Claude Opus 4.8 --- .../steps/FunnelPersonaQuiz.module.css | 173 +++++++++++++++--- .../onboarding/steps/FunnelPersonaQuiz.tsx | 119 +++++++++--- 2 files changed, 239 insertions(+), 53 deletions(-) diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css index f2ee058f83..89c7fa750f 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css @@ -208,6 +208,79 @@ mask-image: radial-gradient(75% 70% at 50% 35%, #000, transparent 80%); } +/* Second aurora in a cooler hue. It drifts the opposite way and breathes its + * opacity out of phase with the cabbage one, so the spotlight slowly shifts + * colour — purple → blue → purple — like magic settling over the stage. */ +@keyframes aurora-shift { + 0%, + 100% { + opacity: 0.25; + transform: translate(-38%, 4%) scale(1); + } + 50% { + opacity: 0.6; + transform: translate(-58%, -4%) scale(1.12); + } +} + +.auroraAlt { + position: absolute; + left: 50%; + top: -22%; + width: 80vw; + height: 68vh; + background: radial-gradient( + closest-side, + color-mix(in srgb, var(--theme-accent-blueCheese-default) 26%, transparent), + transparent + ); + filter: blur(54px); + animation: aurora-shift 13s ease-in-out infinite; +} + +/* Floating "magic dust": specks that rise from the lamp and fade, each given a + * random column/size/delay inline. Pure decoration, behind the content. */ +@keyframes particle-rise { + 0% { + transform: translateY(28px) scale(0.5); + opacity: 0; + } + 18% { + opacity: 1; + } + 82% { + opacity: 1; + } + 100% { + transform: translateY(-150px) scale(1); + opacity: 0; + } +} + +.magicParticle { + position: absolute; + bottom: 18%; + width: 4px; + height: 4px; + border-radius: 9999px; + background: color-mix(in srgb, var(--theme-accent-cheese-default) 85%, white); + box-shadow: 0 0 8px 1px + color-mix(in srgb, var(--theme-accent-cheese-default) 60%, transparent); + animation: particle-rise var(--particle-duration, 7s) ease-in-out infinite; + animation-delay: var(--particle-delay, 0s); +} + +/* Thought-cloud connector: glassy circles that trail from the panel toward + * Patchy, so the message reads as coming from the avatar. */ +.bubbleDot { + border-radius: 9999px; + background: color-mix(in srgb, var(--theme-surface-float) 50%, transparent); + backdrop-filter: blur(18px) saturate(1.1); + -webkit-backdrop-filter: blur(18px) saturate(1.1); + border: 1px solid + color-mix(in srgb, var(--theme-text-primary) 12%, transparent); +} + /* Frosted-glass panel for Patchy's lines: translucent, blurred, with a glass * top-edge highlight. Replaces the flat bordered speech bubble. */ .panel { @@ -252,40 +325,86 @@ animation: shine-sweep 0.8s ease-out; } -/* Yes / No answer chips. The compound selector keeps the colored glow ahead of - * the shared `.btn:hover` lift shadow regardless of stylesheet order. */ -.chip { - transition: transform 0.18s ease, box-shadow 0.18s ease, - border-color 0.18s ease; +/* Yes / No answer tiles. Big, glassy and prominent by default; on hover they + * flood with their verdict colour (avocado for yes, ketchup for no), the icon + * badge fills, and the whole tile lifts with a colour-matched glow. */ +.answer { + position: relative; + overflow: hidden; + background: color-mix(in srgb, var(--theme-surface-float) 55%, transparent); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid + color-mix(in srgb, var(--theme-text-primary) 14%, transparent); + box-shadow: inset 0 1px 0 0 + color-mix(in srgb, var(--theme-text-primary) 8%, transparent); + transition: transform 0.18s ease, box-shadow 0.2s ease, border-color 0.2s ease, + background-color 0.2s ease; } -.chip svg { - transition: transform 0.18s ease, color 0.18s ease; +/* Icon badge: a tinted disc that brightens to a solid fill on hover. */ +.answerBadge { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 9999px; + transition: transform 0.18s ease, background-color 0.2s ease, color 0.2s ease; } -.chip.chipYes:hover { - border-color: var(--theme-accent-avocado-default); - box-shadow: 0 0 24px -4px - color-mix(in srgb, var(--theme-accent-avocado-default) 70%, transparent); +.answerYes .answerBadge { + background: color-mix(in srgb, var(--theme-accent-avocado-default) 20%, transparent); + color: var(--theme-accent-avocado-default); } -.chip.chipYes:hover svg { - color: var(--theme-accent-avocado-default); - transform: scale(1.12); +.answerNo .answerBadge { + background: color-mix(in srgb, var(--theme-accent-ketchup-default) 20%, transparent); + color: var(--theme-accent-ketchup-default); +} + +.answerYes:hover { + border-color: var(--theme-accent-avocado-default); + background: color-mix(in srgb, var(--theme-accent-avocado-default) 16%, transparent); + box-shadow: 0 12px 30px -12px + color-mix(in srgb, var(--theme-accent-avocado-default) 80%, transparent); } -.chip.chipNo:hover { +.answerNo:hover { border-color: var(--theme-accent-ketchup-default); - box-shadow: 0 0 24px -4px - color-mix(in srgb, var(--theme-accent-ketchup-default) 70%, transparent); + background: color-mix(in srgb, var(--theme-accent-ketchup-default) 16%, transparent); + box-shadow: 0 12px 30px -12px + color-mix(in srgb, var(--theme-accent-ketchup-default) 80%, transparent); } -/* DownvoteIcon is UpvoteIcon + `rotate-180`, so the hover transform must - * re-apply that rotation — a bare scale() would clobber it and flip the arrow - * back to pointing up. */ -.chip.chipNo:hover svg { - color: var(--theme-accent-ketchup-default); - transform: rotate(0.5turn) scale(1.12); +/* Solid badge on hover. Scaling the badge (not the svg) keeps the downvote's + * `rotate-180` intact. */ +.answerYes:hover .answerBadge { + background: var(--theme-accent-avocado-default); + color: var(--theme-surface-invert); + transform: scale(1.08); +} + +.answerNo:hover .answerBadge { + background: var(--theme-accent-ketchup-default); + color: var(--theme-surface-invert); + transform: scale(1.08); +} + +/* "Not sure": a clearly secondary ghost pill — visible, but lighter than the + * answer tiles. Dashed hairline + muted label that warms on hover. */ +.notSure { + border-radius: 9999px; + border: 1px dashed + color-mix(in srgb, var(--theme-text-primary) 22%, transparent); + background: color-mix(in srgb, var(--theme-surface-float) 30%, transparent); + color: var(--theme-text-secondary); + transition: transform 0.16s ease, color 0.18s ease, border-color 0.18s ease, + background-color 0.18s ease; +} + +.notSure:hover { + color: var(--theme-text-primary); + border-color: color-mix(in srgb, var(--theme-text-primary) 40%, transparent); + background: color-mix(in srgb, var(--theme-surface-float) 55%, transparent); } /* Persona / option cards: reach-for-it glow + emoji wink. */ @@ -335,10 +454,16 @@ .progressFill, .tick, .stageBackdrop::before, - .stageBackdrop::after { + .stageBackdrop::after, + .auroraAlt, + .magicParticle { animation: none; } + .magicParticle { + display: none; + } + .cta::after { display: none; } diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx index 3b4d9795a5..a764d6c832 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx @@ -22,6 +22,7 @@ import { TypographyType, } from '../../../components/typography/Typography'; import { DownvoteIcon, UpvoteIcon } from '../../../components/icons'; +import { IconSize } from '../../../components/Icon'; import { usePersonaQuiz } from './persona/usePersonaQuiz'; import type { AnswerValue } from './persona/engine'; import type { DeveloperPersona } from './persona/data'; @@ -303,6 +304,24 @@ const SpeechBubble = ({ )} > {children} + {/* Thought-cloud trailing toward Patchy: up on mobile (he sits above), + * out to the right on laptop (he sits beside). */} + + + + + + + + + + ); @@ -314,9 +333,44 @@ interface QuizStageProps { children: ReactNode; } -// Decorative casino-table glow behind the stage. Purely presentational. +// Fixed configs (left column / size / timing) so the floating dust is stable +// across SSR and re-renders instead of jumping on every paint. +const MAGIC_PARTICLES = [ + { left: '10%', size: 3, delay: '0s', duration: '7.5s' }, + { left: '22%', size: 5, delay: '2.4s', duration: '9s' }, + { left: '34%', size: 3, delay: '4.1s', duration: '8s' }, + { left: '44%', size: 4, delay: '1.2s', duration: '10s' }, + { left: '52%', size: 6, delay: '5.6s', duration: '11s' }, + { left: '61%', size: 3, delay: '3s', duration: '8.5s' }, + { left: '70%', size: 5, delay: '0.8s', duration: '9.5s' }, + { left: '79%', size: 4, delay: '4.8s', duration: '7s' }, + { left: '88%', size: 3, delay: '2s', duration: '10.5s' }, + { left: '16%', size: 4, delay: '6.2s', duration: '9s' }, + { left: '66%', size: 3, delay: '6.9s', duration: '8s' }, + { left: '93%', size: 5, delay: '1.7s', duration: '11.5s' }, +]; + +// Decorative spotlight-stage layer: a colour-shifting aurora plus floating +// "magic dust" rising from the lamp. Purely presentational, behind content. const StageBackdrop = (): ReactElement => ( -
+
+ + {MAGIC_PARTICLES.map((particle) => ( + + ))} +
); // Shared skeleton for the intro, question and reveal screens: a top progress @@ -926,47 +980,54 @@ function FunnelPersonaQuizComponent({ )} >
- } disabled={isThinking} onClick={() => handleAnswer(1)} - > - Yes - - + + + + + Yes + + +
- handleAnswer(0.5)} + className={classNames( + styles.notSure, + 'mx-auto mt-1 flex items-center gap-2 px-5 py-2 transition-transform duration-150 ease-out hover:scale-105 active:scale-95', + )} > - Not sure - + + Not sure + +
{isThinking && ( From b3909c0a0bf4d4ba4aa64775de2b8ff0561c3a9f Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 9 Jun 2026 12:29:16 +0300 Subject: [PATCH 05/15] feat(onboarding): celebratory persona reveal moment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turns the flat reveal into the payoff of the flow: - The persona emoji becomes a hero "amulet" — a glowing coin in the persona's brand colour, ringed by spinning light spokes, with a shockwave ring and a brand-coloured confetti burst on entrance. Fixes the awkward emoji-at-the-end of the sentence. - Re-sequenced entrance: emblem springs in → shockwave → confetti → kicker → name → tagline → CTAs cascade. - New copy: a "✦ The genie has spoken ✦" kicker above the name. - Name stays readable on any persona colour via a color-mix toward white (was raw persona colour, which went near-invisible for dark personas). All decoration uses design-system tokens and is disabled / hidden under prefers-reduced-motion. Co-Authored-By: Claude Opus 4.8 --- .../steps/FunnelPersonaQuiz.module.css | 161 +++++++++++++++++- .../onboarding/steps/FunnelPersonaQuiz.tsx | 121 ++++++++++--- 2 files changed, 255 insertions(+), 27 deletions(-) diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css index 89c7fa750f..9470ca06b3 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css @@ -41,16 +41,161 @@ animation: dot-blink 1.2s ease-in-out infinite; } +.revealEyebrow { + animation: reveal-rise 0.5s ease 0.5s both; +} + .revealName { - animation: reveal-rise 0.5s ease 0.15s both; + animation: reveal-rise 0.5s ease 0.62s both; } .revealTagline { - animation: reveal-rise 0.5s ease 0.3s both; + animation: reveal-rise 0.5s ease 0.76s both; } .revealActions { - animation: reveal-rise 0.5s ease 0.45s both; + animation: reveal-rise 0.5s ease 0.9s both; +} + +/* ─── Reveal celebration ────────────────────────────────────────────────── + * The genie's verdict lands as an event: a glowing persona "amulet" springs + * in, a shockwave ring snaps out, spokes of light spin behind it, and a burst + * of brand-coloured confetti scatters. `--persona` (the persona's brand colour) + * is set inline on the .emblem and inherited by every layer. */ +.emblem { + position: relative; + display: grid; + place-items: center; + width: 9rem; + height: 9rem; +} + +@keyframes emblem-pop { + 0% { + transform: scale(0) rotate(-28deg); + opacity: 0; + } + 62% { + transform: scale(1.14) rotate(6deg); + opacity: 1; + } + 100% { + transform: scale(1) rotate(0); + opacity: 1; + } +} + +@keyframes emblem-glow { + 0%, + 100% { + box-shadow: 0 0 0 1px + color-mix(in srgb, var(--persona) 55%, transparent), + 0 14px 44px -10px color-mix(in srgb, var(--persona) 55%, transparent); + } + 50% { + box-shadow: 0 0 0 1px color-mix(in srgb, var(--persona) 85%, transparent), + 0 18px 64px -8px color-mix(in srgb, var(--persona) 85%, transparent); + } +} + +.emblemCoin { + position: relative; + z-index: 2; + display: grid; + place-items: center; + width: 7rem; + height: 7rem; + border-radius: 9999px; + background: radial-gradient( + circle at 36% 28%, + color-mix(in srgb, var(--persona) 10%, white) 0%, + transparent 46% + ), + color-mix(in srgb, var(--persona) 32%, var(--theme-surface-float)); + border: 1px solid color-mix(in srgb, var(--persona) 65%, transparent); + animation: emblem-pop 0.6s cubic-bezier(0.18, 0.9, 0.32, 1.4) both, + emblem-glow 2.6s ease-in-out 0.65s infinite; +} + +.emblemEmoji { + font-size: 3.25rem; + line-height: 1; + filter: drop-shadow(0 3px 8px rgb(0 0 0 / 0.4)); +} + +@keyframes emblem-spin { + to { + transform: rotate(1turn); + } +} + +.emblemRays { + position: absolute; + inset: -55%; + z-index: 0; + background: repeating-conic-gradient( + from 0deg, + color-mix(in srgb, var(--persona) 24%, transparent) 0deg 6deg, + transparent 6deg 22deg + ); + mask: radial-gradient(closest-side, transparent 34%, #000 46%, transparent 78%); + -webkit-mask: radial-gradient( + closest-side, + transparent 34%, + #000 46%, + transparent 78% + ); + animation: emblem-spin 18s linear infinite, reveal-rise 0.6s ease both; + opacity: 0.8; +} + +@keyframes shockwave { + 0% { + transform: scale(0.55); + opacity: 0.85; + } + 100% { + transform: scale(2.7); + opacity: 0; + } +} + +.emblemFlash { + position: absolute; + z-index: 1; + width: 7rem; + height: 7rem; + border-radius: 9999px; + border: 2px solid color-mix(in srgb, var(--persona) 70%, transparent); + animation: shockwave 0.85s ease-out 0.1s both; +} + +@keyframes confetti-fly { + 0% { + transform: translate(-50%, -50%) rotate(0); + opacity: 0; + } + 12% { + opacity: 1; + } + 100% { + transform: translate(calc(-50% + var(--cx)), calc(-50% + var(--cy))) + rotate(var(--cr)); + opacity: 0; + } +} + +.confettiPiece { + position: absolute; + left: 50%; + top: 50%; + z-index: 3; + width: var(--cw, 8px); + height: var(--ch, 8px); + border-radius: 1px; + background: var(--cc); + animation: confetti-fly var(--cd, 1.1s) cubic-bezier(0.1, 0.6, 0.3, 1) + var(--cdelay, 0s) both; } /* ─── Casino / arcade micro-interactions ────────────────────────────────── @@ -447,6 +592,7 @@ @media (prefers-reduced-motion: reduce) { .questionIn, .dot, + .revealEyebrow, .revealName, .revealTagline, .revealActions, @@ -456,11 +602,16 @@ .stageBackdrop::before, .stageBackdrop::after, .auroraAlt, - .magicParticle { + .magicParticle, + .emblemCoin, + .emblemRays, + .emblemFlash { animation: none; } - .magicParticle { + .magicParticle, + .emblemFlash, + .confettiPiece { display: none; } diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx index a764d6c832..cc0561a10f 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx @@ -350,6 +350,34 @@ const MAGIC_PARTICLES = [ { left: '93%', size: 5, delay: '1.7s', duration: '11.5s' }, ]; +// Brand-coloured confetti for the reveal burst, generated once (deterministic, +// no randomness) so it's SSR-stable. Each piece flies from the emblem centre +// out to (cx, cy) while spinning. +const CONFETTI_COLORS = [ + 'var(--theme-accent-cabbage-default)', + 'var(--theme-accent-cheese-default)', + 'var(--theme-accent-bacon-default)', + 'var(--theme-accent-avocado-default)', + 'var(--theme-accent-water-default)', + 'var(--theme-accent-bun-default)', +]; + +const REVEAL_CONFETTI = Array.from({ length: 20 }, (_, index) => { + const angle = (index / 20) * Math.PI * 2; + const distance = 130 + (index % 3) * 48; + return { + id: index, + cx: `${Math.round(Math.cos(angle) * distance)}px`, + cy: `${Math.round(Math.sin(angle) * distance)}px`, + cr: `${(index % 2 ? 1 : -1) * (200 + (index % 4) * 80)}deg`, + cw: `${6 + (index % 3) * 2}px`, + ch: `${8 + (index % 2) * 4}px`, + cc: CONFETTI_COLORS[index % CONFETTI_COLORS.length], + cd: `${1.05 + (index % 4) * 0.14}s`, + cdelay: `${(index % 5) * 0.04}s`, + }; +}); + // Decorative spotlight-stage layer: a colour-shifting aurora plus floating // "magic dust" rising from the lamp. Purely presentational, behind content. const StageBackdrop = (): ReactElement => ( @@ -468,6 +496,45 @@ const PersonaCard = ({ ); +// The celebratory persona "amulet" for the reveal: a glowing coin in the +// persona's brand colour, ringed by spinning light spokes and a shockwave, with +// a one-shot confetti burst. `--persona` cascades to every layer. +const PersonaEmblem = ({ + persona, +}: { + persona: DeveloperPersona; +}): ReactElement => ( +
+ + + + {persona.emoji} + + {REVEAL_CONFETTI.map((piece) => ( + + ))} +
+); + function FunnelPersonaQuizComponent({ parameters: { headline, explainer, cta, mascotVideoBaseUrl }, onTransition, @@ -876,33 +943,43 @@ function FunnelPersonaQuizComponent({ /> } > -
+
{revealReady && ( <> - -
- - {personaRevealPhrase(persona.name)} {persona.emoji} - - - {persona.tagline} - -
-
+ + + ✦ The genie has spoken ✦ + + + {personaRevealPhrase(persona.name)} + + + {persona.tagline} +
Date: Tue, 9 Jun 2026 12:42:25 +0300 Subject: [PATCH 06/15] style(onboarding): tune persona quiz answers + purple ambiance - Narrow the Yes/No answer row (max-w-sm) and widen the gap before "Not sure". - Recolor the floating magic dust from gold to purple (cabbage). - Swap the bottom backdrop pool from warm bun/orange to cabbage/purple. Co-Authored-By: Claude Opus 4.8 --- .../features/onboarding/steps/FunnelPersonaQuiz.module.css | 6 +++--- .../src/features/onboarding/steps/FunnelPersonaQuiz.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css index 9470ca06b3..f509b03035 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css @@ -286,7 +286,7 @@ ), radial-gradient( 46% 36% at 50% 104%, - color-mix(in srgb, var(--theme-accent-bun-default) 14%, transparent), + color-mix(in srgb, var(--theme-accent-cabbage-default) 18%, transparent), transparent 72% ), radial-gradient( @@ -408,9 +408,9 @@ width: 4px; height: 4px; border-radius: 9999px; - background: color-mix(in srgb, var(--theme-accent-cheese-default) 85%, white); + background: color-mix(in srgb, var(--theme-accent-cabbage-default) 75%, white); box-shadow: 0 0 8px 1px - color-mix(in srgb, var(--theme-accent-cheese-default) 60%, transparent); + color-mix(in srgb, var(--theme-accent-cabbage-default) 60%, transparent); animation: particle-rise var(--particle-duration, 7s) ease-in-out infinite; animation-delay: var(--particle-delay, 0s); } diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx index cc0561a10f..5d3be29345 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx @@ -1049,10 +1049,10 @@ function FunnelPersonaQuizComponent({ {questionText} -
+
@@ -1098,7 +1098,7 @@ function FunnelPersonaQuizComponent({ onClick={() => handleAnswer(0.5)} className={classNames( styles.notSure, - 'mx-auto mt-1 flex items-center gap-2 px-5 py-2 transition-transform duration-150 ease-out hover:scale-105 active:scale-95', + 'mx-auto flex items-center gap-2 px-5 py-2 transition-transform duration-150 ease-out hover:scale-105 active:scale-95', )} > From ca18f4cb49e4bc1d9dfb41ea7e17c15eb04dc30d Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 9 Jun 2026 12:49:10 +0300 Subject: [PATCH 07/15] feat(onboarding): full-screen party on the persona reveal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the "aha" moment land like a celebration: - A flash "pop" (radial cabbage→white burst from the emblem) fires the instant the persona is exposed. - A full-screen confetti cannon: 48 brand-coloured pieces (squares + streamers) burst from the emblem and rain down across the whole viewport, tumbling as they fall. Rendered in a fixed, click-through overlay so it sits over everything without blocking the CTAs. Confetti config is deterministic (SSR-safe); the whole celebration is hidden under prefers-reduced-motion. Co-Authored-By: Claude Opus 4.8 --- .../steps/FunnelPersonaQuiz.module.css | 61 ++++++++++++++++--- .../onboarding/steps/FunnelPersonaQuiz.tsx | 53 ++++++++++------ 2 files changed, 89 insertions(+), 25 deletions(-) diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css index f509b03035..b4fc281f8d 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css @@ -170,17 +170,62 @@ animation: shockwave 0.85s ease-out 0.1s both; } -@keyframes confetti-fly { +/* Full-screen party overlay: a flash "pop" the instant the genie exposes you, + * then a confetti cannon that bursts from the emblem and rains down across the + * whole viewport. Fixed + pointer-events:none so it sits over everything + * without blocking the CTAs. */ +.celebration { + position: fixed; + inset: 0; + z-index: 4; + overflow: hidden; + pointer-events: none; +} + +@keyframes reveal-flash { + 0% { + opacity: 0; + transform: scale(0.3); + } + 16% { + opacity: 0.7; + } + 100% { + opacity: 0; + transform: scale(1.7); + } +} + +.revealFlash { + position: absolute; + inset: 0; + transform-origin: 50% 42%; + background: radial-gradient( + circle at 50% 42%, + color-mix(in srgb, var(--theme-accent-cabbage-default) 60%, white) 0%, + color-mix(in srgb, var(--theme-accent-cabbage-default) 22%, transparent) 26%, + transparent 52% + ); + animation: reveal-flash 0.75s ease-out both; +} + +/* Burst out to (--bx,--by), then arc down to (--fx,--fy) while spinning. */ +@keyframes confetti-burst { 0% { transform: translate(-50%, -50%) rotate(0); opacity: 0; } - 12% { + 6% { + opacity: 1; + } + 34% { + transform: translate(calc(-50% + var(--bx)), calc(-50% + var(--by))) + rotate(var(--r1)); opacity: 1; } 100% { - transform: translate(calc(-50% + var(--cx)), calc(-50% + var(--cy))) - rotate(var(--cr)); + transform: translate(calc(-50% + var(--fx)), calc(-50% + var(--fy))) + rotate(var(--r2)); opacity: 0; } } @@ -188,13 +233,12 @@ .confettiPiece { position: absolute; left: 50%; - top: 50%; - z-index: 3; + top: 42%; width: var(--cw, 8px); height: var(--ch, 8px); border-radius: 1px; background: var(--cc); - animation: confetti-fly var(--cd, 1.1s) cubic-bezier(0.1, 0.6, 0.3, 1) + animation: confetti-burst var(--cd, 2.2s) cubic-bezier(0.2, 0.7, 0.35, 1) var(--cdelay, 0s) both; } @@ -611,7 +655,8 @@ .magicParticle, .emblemFlash, - .confettiPiece { + .confettiPiece, + .revealFlash { display: none; } diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx index 5d3be29345..175c061f21 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx @@ -350,9 +350,9 @@ const MAGIC_PARTICLES = [ { left: '93%', size: 5, delay: '1.7s', duration: '11.5s' }, ]; -// Brand-coloured confetti for the reveal burst, generated once (deterministic, -// no randomness) so it's SSR-stable. Each piece flies from the emblem centre -// out to (cx, cy) while spinning. +// Brand-coloured confetti for the reveal party, generated once (deterministic, +// no randomness) so it's SSR-stable. Each piece bursts from the emblem out to +// (bx, by), then arcs down and off-screen to (fx, fy) while tumbling. const CONFETTI_COLORS = [ 'var(--theme-accent-cabbage-default)', 'var(--theme-accent-cheese-default)', @@ -362,19 +362,26 @@ const CONFETTI_COLORS = [ 'var(--theme-accent-bun-default)', ]; -const REVEAL_CONFETTI = Array.from({ length: 20 }, (_, index) => { - const angle = (index / 20) * Math.PI * 2; - const distance = 130 + (index % 3) * 48; +const REVEAL_CONFETTI = Array.from({ length: 48 }, (_, index) => { + const spread = (index / 47) * 2 - 1; // -1 → 1 fanned across the width + const burstX = Math.round(spread * (300 + (index % 5) * 70)); + const burstY = -(140 + (index % 6) * 55); // up and out first + const isStreamer = index % 3 === 0; return { id: index, - cx: `${Math.round(Math.cos(angle) * distance)}px`, - cy: `${Math.round(Math.sin(angle) * distance)}px`, - cr: `${(index % 2 ? 1 : -1) * (200 + (index % 4) * 80)}deg`, - cw: `${6 + (index % 3) * 2}px`, - ch: `${8 + (index % 2) * 4}px`, + bx: `${burstX}px`, + by: `${burstY}px`, + fx: `${Math.round( + burstX * 1.18 + (index % 2 ? 1 : -1) * (index % 7) * 9, + )}px`, + fy: `${460 + (index % 8) * 70}px`, // then rain down past the fold + r1: `${(index % 2 ? 1 : -1) * (160 + (index % 5) * 70)}deg`, + r2: `${(index % 2 ? 1 : -1) * (420 + (index % 6) * 110)}deg`, + cw: `${isStreamer ? 4 : 7 + (index % 3) * 2}px`, + ch: `${isStreamer ? 14 + (index % 3) * 4 : 7 + (index % 2) * 3}px`, cc: CONFETTI_COLORS[index % CONFETTI_COLORS.length], - cd: `${1.05 + (index % 4) * 0.14}s`, - cdelay: `${(index % 5) * 0.04}s`, + cd: `${1.9 + (index % 5) * 0.18}s`, + cdelay: `${(index % 6) * 0.05}s`, }; }); @@ -513,16 +520,27 @@ const PersonaEmblem = ({ {persona.emoji} +
+); + +// Full-screen "aha" party: a flash pop the instant the persona is exposed, +// then a confetti cannon raining across the whole viewport. Decorative and +// click-through (pointer-events: none). +const RevealCelebration = (): ReactElement => ( +
+ {REVEAL_CONFETTI.map((piece) => ( {revealReady && ( <> + Date: Tue, 9 Jun 2026 12:56:21 +0300 Subject: [PATCH 08/15] style(onboarding): clean top, bottom-up dust, drop emblem rays - Remove the spinning "sun ray" spokes around the reveal emblem. - Fix the floating dust: lift off the bottom edge, drift steadily up (linear), and fade out by ~25% of the screen height instead of parking mid-screen. - Remove the top spotlight gradient; move the ambient/colour-shift glow to the base so the top of the stage stays clean and dark. Co-Authored-By: Claude Opus 4.8 --- .../steps/FunnelPersonaQuiz.module.css | 71 ++++++------------- .../onboarding/steps/FunnelPersonaQuiz.tsx | 25 ++++--- 2 files changed, 32 insertions(+), 64 deletions(-) diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css index b4fc281f8d..0a2c75e236 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css @@ -123,32 +123,6 @@ filter: drop-shadow(0 3px 8px rgb(0 0 0 / 0.4)); } -@keyframes emblem-spin { - to { - transform: rotate(1turn); - } -} - -.emblemRays { - position: absolute; - inset: -55%; - z-index: 0; - background: repeating-conic-gradient( - from 0deg, - color-mix(in srgb, var(--persona) 24%, transparent) 0deg 6deg, - transparent 6deg 22deg - ); - mask: radial-gradient(closest-side, transparent 34%, #000 46%, transparent 78%); - -webkit-mask: radial-gradient( - closest-side, - transparent 34%, - #000 46%, - transparent 78% - ); - animation: emblem-spin 18s linear infinite, reveal-rise 0.6s ease both; - opacity: 0.8; -} - @keyframes shockwave { 0% { transform: scale(0.55); @@ -312,11 +286,10 @@ } } -/* Spotlight "stage" backdrop. Full-bleed via `fixed` so it never gets clipped - * by the content column, and sits behind the content through a negative - * z-index inside the isolated stage. A single soft cabbage spotlight from the - * top, a faint warm pool of lamp-light at the base, and a vignette that pulls - * focus to the centre — deep and premium rather than a flat funnel gradient. */ +/* "Stage" backdrop. Full-bleed via `fixed` so it never gets clipped by the + * content column, and sits behind the content through a negative z-index inside + * the isolated stage. A faint pool of lamp-light glows up from the base and a + * vignette pulls focus to the centre — the top stays clean and dark. */ .stageBackdrop { position: fixed; inset: 0; @@ -332,22 +305,18 @@ 46% 36% at 50% 104%, color-mix(in srgb, var(--theme-accent-cabbage-default) 18%, transparent), transparent 72% - ), - radial-gradient( - 72% 58% at 50% -12%, - color-mix(in srgb, var(--theme-accent-cabbage-default) 20%, transparent), - transparent 66% ); } -/* Slow-drifting aurora blob — gives the spotlight life without motion noise. */ +/* Slow-drifting aurora blob rising from the base — gives the lamp-light life + * and a colour shift without lighting up the top of the screen. */ .stageBackdrop::before { content: ''; position: absolute; left: 50%; - top: -18%; + bottom: -28%; width: 90vw; - height: 70vh; + height: 60vh; transform: translate(-50%, 0); background: radial-gradient( closest-side, @@ -415,9 +384,9 @@ .auroraAlt { position: absolute; left: 50%; - top: -22%; + bottom: -30%; width: 80vw; - height: 68vh; + height: 58vh; background: radial-gradient( closest-side, color-mix(in srgb, var(--theme-accent-blueCheese-default) 26%, transparent), @@ -427,35 +396,36 @@ animation: aurora-shift 13s ease-in-out infinite; } -/* Floating "magic dust": specks that rise from the lamp and fade, each given a - * random column/size/delay inline. Pure decoration, behind the content. */ +/* Floating "magic dust": specks that lift off the bottom edge, drift steadily + * up, and fade out by the time they reach ~25% of the screen height. Linear + * timing keeps them continuously moving (never parked mid-screen). */ @keyframes particle-rise { 0% { - transform: translateY(28px) scale(0.5); + transform: translateY(0) scale(0.5); opacity: 0; } - 18% { + 12% { opacity: 1; } - 82% { - opacity: 1; + 70% { + opacity: 0.9; } 100% { - transform: translateY(-150px) scale(1); + transform: translateY(-26vh) scale(1); opacity: 0; } } .magicParticle { position: absolute; - bottom: 18%; + bottom: 0; width: 4px; height: 4px; border-radius: 9999px; background: color-mix(in srgb, var(--theme-accent-cabbage-default) 75%, white); box-shadow: 0 0 8px 1px color-mix(in srgb, var(--theme-accent-cabbage-default) 60%, transparent); - animation: particle-rise var(--particle-duration, 7s) ease-in-out infinite; + animation: particle-rise var(--particle-duration, 6s) linear infinite; animation-delay: var(--particle-delay, 0s); } @@ -648,7 +618,6 @@ .auroraAlt, .magicParticle, .emblemCoin, - .emblemRays, .emblemFlash { animation: none; } diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx index 175c061f21..780a89d985 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx @@ -336,18 +336,18 @@ interface QuizStageProps { // Fixed configs (left column / size / timing) so the floating dust is stable // across SSR and re-renders instead of jumping on every paint. const MAGIC_PARTICLES = [ - { left: '10%', size: 3, delay: '0s', duration: '7.5s' }, - { left: '22%', size: 5, delay: '2.4s', duration: '9s' }, - { left: '34%', size: 3, delay: '4.1s', duration: '8s' }, - { left: '44%', size: 4, delay: '1.2s', duration: '10s' }, - { left: '52%', size: 6, delay: '5.6s', duration: '11s' }, - { left: '61%', size: 3, delay: '3s', duration: '8.5s' }, - { left: '70%', size: 5, delay: '0.8s', duration: '9.5s' }, - { left: '79%', size: 4, delay: '4.8s', duration: '7s' }, - { left: '88%', size: 3, delay: '2s', duration: '10.5s' }, - { left: '16%', size: 4, delay: '6.2s', duration: '9s' }, - { left: '66%', size: 3, delay: '6.9s', duration: '8s' }, - { left: '93%', size: 5, delay: '1.7s', duration: '11.5s' }, + { left: '10%', size: 3, delay: '0s', duration: '5s' }, + { left: '22%', size: 5, delay: '1.6s', duration: '6.2s' }, + { left: '34%', size: 3, delay: '3.2s', duration: '4.6s' }, + { left: '44%', size: 4, delay: '0.8s', duration: '5.6s' }, + { left: '52%', size: 6, delay: '2.4s', duration: '6.8s' }, + { left: '61%', size: 3, delay: '4s', duration: '5.2s' }, + { left: '70%', size: 5, delay: '0.4s', duration: '6s' }, + { left: '79%', size: 4, delay: '3s', duration: '4.4s' }, + { left: '88%', size: 3, delay: '1.2s', duration: '6.4s' }, + { left: '16%', size: 4, delay: '3.8s', duration: '5.4s' }, + { left: '66%', size: 3, delay: '4.6s', duration: '4.8s' }, + { left: '93%', size: 5, delay: '2s', duration: '7s' }, ]; // Brand-coloured confetti for the reveal party, generated once (deterministic, @@ -515,7 +515,6 @@ const PersonaEmblem = ({ className={styles.emblem} style={{ '--persona': persona.color } as React.CSSProperties} > - {persona.emoji} From e895576553875cdc935d83f8caf4e81b36a275ac Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 9 Jun 2026 13:24:14 +0300 Subject: [PATCH 09/15] feat(onboarding): replace reveal confetti with a firework from the icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The square confetti pop felt off. Swap it for a firework of glowing dots — the same family as the floating dust — that explodes radially out of the emblem, decelerates, sags a touch with gravity, and fades. Originates from the icon itself (rendered inside the emblem), not a full-screen overlay. Removes the confetti cannon + screen flash overlay. Firework is deterministic (SSR-safe) and hidden under prefers-reduced-motion. Co-Authored-By: Claude Opus 4.8 --- .../steps/FunnelPersonaQuiz.module.css | 78 +++++------------- .../onboarding/steps/FunnelPersonaQuiz.tsx | 81 +++++++------------ 2 files changed, 50 insertions(+), 109 deletions(-) diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css index 0a2c75e236..e1b25d40c7 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css @@ -144,76 +144,39 @@ animation: shockwave 0.85s ease-out 0.1s both; } -/* Full-screen party overlay: a flash "pop" the instant the genie exposes you, - * then a confetti cannon that bursts from the emblem and rains down across the - * whole viewport. Fixed + pointer-events:none so it sits over everything - * without blocking the CTAs. */ -.celebration { - position: fixed; - inset: 0; - z-index: 4; - overflow: hidden; - pointer-events: none; -} - -@keyframes reveal-flash { +/* Firework burst: glowing dots (same family as the floating dust) explode + * outward from the emblem centre, decelerate, sag a touch with gravity, then + * fade — like a firework going off where the icon is. */ +@keyframes firework { 0% { + transform: translate(-50%, -50%) scale(0.3); opacity: 0; - transform: scale(0.3); } - 16% { - opacity: 0.7; - } - 100% { - opacity: 0; - transform: scale(1.7); - } -} - -.revealFlash { - position: absolute; - inset: 0; - transform-origin: 50% 42%; - background: radial-gradient( - circle at 50% 42%, - color-mix(in srgb, var(--theme-accent-cabbage-default) 60%, white) 0%, - color-mix(in srgb, var(--theme-accent-cabbage-default) 22%, transparent) 26%, - transparent 52% - ); - animation: reveal-flash 0.75s ease-out both; -} - -/* Burst out to (--bx,--by), then arc down to (--fx,--fy) while spinning. */ -@keyframes confetti-burst { - 0% { - transform: translate(-50%, -50%) rotate(0); - opacity: 0; - } - 6% { + 12% { opacity: 1; } - 34% { - transform: translate(calc(-50% + var(--bx)), calc(-50% + var(--by))) - rotate(var(--r1)); + 70% { opacity: 1; } 100% { - transform: translate(calc(-50% + var(--fx)), calc(-50% + var(--fy))) - rotate(var(--r2)); + transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) + scale(0.6); opacity: 0; } } -.confettiPiece { +.fireworkSpark { position: absolute; left: 50%; - top: 42%; - width: var(--cw, 8px); - height: var(--ch, 8px); - border-radius: 1px; - background: var(--cc); - animation: confetti-burst var(--cd, 2.2s) cubic-bezier(0.2, 0.7, 0.35, 1) - var(--cdelay, 0s) both; + top: 50%; + z-index: 0; + width: var(--sw, 5px); + height: var(--sw, 5px); + border-radius: 9999px; + background: var(--sc); + box-shadow: 0 0 10px 1px color-mix(in srgb, var(--sc) 65%, transparent); + animation: firework var(--sd, 1.3s) cubic-bezier(0.12, 0.75, 0.25, 1) + var(--sdelay, 0s) both; } /* ─── Casino / arcade micro-interactions ────────────────────────────────── @@ -624,8 +587,7 @@ .magicParticle, .emblemFlash, - .confettiPiece, - .revealFlash { + .fireworkSpark { display: none; } diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx index 780a89d985..d99bb8bded 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx @@ -350,10 +350,10 @@ const MAGIC_PARTICLES = [ { left: '93%', size: 5, delay: '2s', duration: '7s' }, ]; -// Brand-coloured confetti for the reveal party, generated once (deterministic, -// no randomness) so it's SSR-stable. Each piece bursts from the emblem out to -// (bx, by), then arcs down and off-screen to (fx, fy) while tumbling. -const CONFETTI_COLORS = [ +// Reveal firework: glowing dots (same family as the floating dust) explode +// radially from the emblem centre. Generated once (deterministic, no +// randomness) so it's SSR-stable. +const FIREWORK_COLORS = [ 'var(--theme-accent-cabbage-default)', 'var(--theme-accent-cheese-default)', 'var(--theme-accent-bacon-default)', @@ -362,26 +362,19 @@ const CONFETTI_COLORS = [ 'var(--theme-accent-bun-default)', ]; -const REVEAL_CONFETTI = Array.from({ length: 48 }, (_, index) => { - const spread = (index / 47) * 2 - 1; // -1 → 1 fanned across the width - const burstX = Math.round(spread * (300 + (index % 5) * 70)); - const burstY = -(140 + (index % 6) * 55); // up and out first - const isStreamer = index % 3 === 0; +const REVEAL_FIREWORK = Array.from({ length: 30 }, (_, index) => { + // Even radial spread + slight per-spoke jitter so it reads organic. + const angle = (index / 30) * Math.PI * 2 + (index % 2 ? 0.12 : -0.12); + const distance = 95 + (index % 4) * 48; // 95 → 239px rings + const gravity = 24 + (index % 3) * 16; // gentle downward sag return { id: index, - bx: `${burstX}px`, - by: `${burstY}px`, - fx: `${Math.round( - burstX * 1.18 + (index % 2 ? 1 : -1) * (index % 7) * 9, - )}px`, - fy: `${460 + (index % 8) * 70}px`, // then rain down past the fold - r1: `${(index % 2 ? 1 : -1) * (160 + (index % 5) * 70)}deg`, - r2: `${(index % 2 ? 1 : -1) * (420 + (index % 6) * 110)}deg`, - cw: `${isStreamer ? 4 : 7 + (index % 3) * 2}px`, - ch: `${isStreamer ? 14 + (index % 3) * 4 : 7 + (index % 2) * 3}px`, - cc: CONFETTI_COLORS[index % CONFETTI_COLORS.length], - cd: `${1.9 + (index % 5) * 0.18}s`, - cdelay: `${(index % 6) * 0.05}s`, + dx: `${Math.round(Math.cos(angle) * distance)}px`, + dy: `${Math.round(Math.sin(angle) * distance + gravity)}px`, + sw: `${4 + (index % 3) * 2}px`, + sc: FIREWORK_COLORS[index % FIREWORK_COLORS.length], + sd: `${1.1 + (index % 4) * 0.2}s`, + sdelay: `${(index % 3) * 0.05}s`, }; }); @@ -504,8 +497,8 @@ const PersonaCard = ({ ); // The celebratory persona "amulet" for the reveal: a glowing coin in the -// persona's brand colour, ringed by spinning light spokes and a shockwave, with -// a one-shot confetti burst. `--persona` cascades to every layer. +// persona's brand colour with a shockwave ring, and a firework of glowing dots +// that explodes outward from the icon. `--persona` cascades to every layer. const PersonaEmblem = ({ persona, }: { @@ -516,39 +509,26 @@ const PersonaEmblem = ({ style={{ '--persona': persona.color } as React.CSSProperties} > - - {persona.emoji} - -
-); - -// Full-screen "aha" party: a flash pop the instant the persona is exposed, -// then a confetti cannon raining across the whole viewport. Decorative and -// click-through (pointer-events: none). -const RevealCelebration = (): ReactElement => ( -
- - {REVEAL_CONFETTI.map((piece) => ( + {REVEAL_FIREWORK.map((spark) => ( ))} + + {persona.emoji} +
); @@ -963,7 +943,6 @@ function FunnelPersonaQuizComponent({
{revealReady && ( <> - Date: Tue, 9 Jun 2026 13:25:28 +0300 Subject: [PATCH 10/15] style(onboarding): color the reveal kicker cabbage purple Brand-tint "The genie has spoken" with accent-cabbage instead of tertiary grey. Co-Authored-By: Claude Opus 4.8 --- .../shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx index d99bb8bded..6df3bcbe4b 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx @@ -947,10 +947,9 @@ function FunnelPersonaQuizComponent({ ✦ The genie has spoken ✦ From df6c344af4043969fe5e7a7b905538fedb207df0 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 9 Jun 2026 13:40:10 +0300 Subject: [PATCH 11/15] =?UTF-8?q?fix(onboarding):=20smooth=20the=20questio?= =?UTF-8?q?n=E2=86=92reveal=20transition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reveal held back ALL its content behind a `revealReady` timer (video half-point or a 2.5s fallback), so right after the last answer the screen went near-empty — just Patchy and a tiny emblem — before the content popped in. That empty in-between frame read as a blink / layout shift / glitch. Drop the gating: the reveal content now renders the moment the phase opens and cascades in with its existing staggered entrance (emblem pop → kicker → name → tagline → CTAs), in sync with Patchy's reveal clip. No empty gap, no jump. Removes the revealReady state, its effect, and the mascot onHalfway wiring. Co-Authored-By: Claude Opus 4.8 --- .../onboarding/steps/FunnelPersonaQuiz.tsx | 137 ++++++++---------- 1 file changed, 57 insertions(+), 80 deletions(-) diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx index 6df3bcbe4b..33ba6a8b62 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx @@ -561,24 +561,6 @@ function FunnelPersonaQuizComponent({ // Which clip plays during the thinking beat, chosen per answer. const [thinkingClip, setThinkingClip] = useState('thinking'); - // On the reveal we hold the answer back until Patchy finishes his animation. - const [revealReady, setRevealReady] = useState(false); - - useEffect(() => { - if (phase !== 'reveal') { - return undefined; - } - setRevealReady(false); - // Patchy now plays the reveal on every breakpoint, so wait for his - // animation whenever there is a video to wait for. - if (!mascotVideoBaseUrl) { - setRevealReady(true); - return undefined; - } - // Fallback in case the video never reports progress (e.g. blocked autoplay). - const timeout = setTimeout(() => setRevealReady(true), 2500); - return () => clearTimeout(timeout); - }, [phase, mascotVideoBaseUrl]); const handleAnswer = (value: AnswerValue) => { if (isThinking) { @@ -935,73 +917,68 @@ function FunnelPersonaQuizComponent({ clips={['reveal']} idleClips={IDLE_CLIPS} size="lg" - onHalfway={() => setRevealReady(true)} className={MASCOT_STAGE_CLASS} /> } >
- {revealReady && ( - <> - - - ✦ The genie has spoken ✦ - - - {personaRevealPhrase(persona.name)} - - - {persona.tagline} - -
- - {cta || "Yes, that's me!"} - - - Nah, I'll pick myself - -
- - )} + + + ✦ The genie has spoken ✦ + + + {personaRevealPhrase(persona.name)} + + + {persona.tagline} + +
+ + {cta || "Yes, that's me!"} + + + Nah, I'll pick myself + +
); From f4198ed2ed7c2ecccfd369b8e13432a3aa3504af Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 9 Jun 2026 14:32:37 +0300 Subject: [PATCH 12/15] style(onboarding): flatten yes/no answers to circle + side label Drop the glassy gray tile (background, border, blur) from the yes/no buttons so they read flat. Keep the colour-filled icon circle, make it bigger (h-14), and lay the two circles close together in the middle with the "Yes"/"No" labels on the outer sides (Yes: label+circle, No: circle+label). On hover the circle fills solid, glows and lifts. Co-Authored-By: Claude Opus 4.8 --- .../steps/FunnelPersonaQuiz.module.css | 51 +++++++------------ .../onboarding/steps/FunnelPersonaQuiz.tsx | 16 +++--- 2 files changed, 25 insertions(+), 42 deletions(-) diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css index e1b25d40c7..97f4d1286c 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.module.css @@ -447,68 +447,51 @@ animation: shine-sweep 0.8s ease-out; } -/* Yes / No answer tiles. Big, glassy and prominent by default; on hover they - * flood with their verdict colour (avocado for yes, ketchup for no), the icon - * badge fills, and the whole tile lifts with a colour-matched glow. */ +/* Yes / No answers. Flat — no tile background or border. Just a colour-filled + * circle (the icon) beside a plain label; the two circles sit close together in + * the middle with the labels flanking the outer edges. On hover the circle + * fills solid, glows and lifts. */ .answer { - position: relative; - overflow: hidden; - background: color-mix(in srgb, var(--theme-surface-float) 55%, transparent); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid - color-mix(in srgb, var(--theme-text-primary) 14%, transparent); - box-shadow: inset 0 1px 0 0 - color-mix(in srgb, var(--theme-text-primary) 8%, transparent); - transition: transform 0.18s ease, box-shadow 0.2s ease, border-color 0.2s ease, - background-color 0.2s ease; + background: transparent; + border: none; + transition: transform 0.18s ease; } -/* Icon badge: a tinted disc that brightens to a solid fill on hover. */ +/* Icon circle: a colour-tinted disc that brightens to a solid fill on hover. */ .answerBadge { display: inline-flex; align-items: center; justify-content: center; border-radius: 9999px; - transition: transform 0.18s ease, background-color 0.2s ease, color 0.2s ease; + transition: transform 0.2s ease, background-color 0.2s ease, color 0.2s ease, + box-shadow 0.2s ease; } .answerYes .answerBadge { - background: color-mix(in srgb, var(--theme-accent-avocado-default) 20%, transparent); + background: color-mix(in srgb, var(--theme-accent-avocado-default) 22%, transparent); color: var(--theme-accent-avocado-default); } .answerNo .answerBadge { - background: color-mix(in srgb, var(--theme-accent-ketchup-default) 20%, transparent); + background: color-mix(in srgb, var(--theme-accent-ketchup-default) 22%, transparent); color: var(--theme-accent-ketchup-default); } -.answerYes:hover { - border-color: var(--theme-accent-avocado-default); - background: color-mix(in srgb, var(--theme-accent-avocado-default) 16%, transparent); - box-shadow: 0 12px 30px -12px - color-mix(in srgb, var(--theme-accent-avocado-default) 80%, transparent); -} - -.answerNo:hover { - border-color: var(--theme-accent-ketchup-default); - background: color-mix(in srgb, var(--theme-accent-ketchup-default) 16%, transparent); - box-shadow: 0 12px 30px -12px - color-mix(in srgb, var(--theme-accent-ketchup-default) 80%, transparent); -} - -/* Solid badge on hover. Scaling the badge (not the svg) keeps the downvote's - * `rotate-180` intact. */ +/* Scaling the badge (not the svg) keeps the downvote's `rotate-180` intact. */ .answerYes:hover .answerBadge { background: var(--theme-accent-avocado-default); color: var(--theme-surface-invert); transform: scale(1.08); + box-shadow: 0 0 28px -4px + color-mix(in srgb, var(--theme-accent-avocado-default) 75%, transparent); } .answerNo:hover .answerBadge { background: var(--theme-accent-ketchup-default); color: var(--theme-surface-invert); transform: scale(1.08); + box-shadow: 0 0 28px -4px + color-mix(in srgb, var(--theme-accent-ketchup-default) 75%, transparent); } /* "Not sure": a clearly secondary ghost pill — visible, but lighter than the diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx index 33ba6a8b62..a642b466ba 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx @@ -1029,7 +1029,7 @@ function FunnelPersonaQuizComponent({ isThinking && 'pointer-events-none opacity-0', )} > -
+
From d7614b9a25088b25e4d09fafa513e73dae9fb6c5 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 9 Jun 2026 14:47:26 +0300 Subject: [PATCH 15/15] style(onboarding): all-purple reveal firework MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recolor the reveal firework dots to purple tones only — the cabbage and onion palettes (default / bolder / subtler each) instead of the mixed brand colours. Co-Authored-By: Claude Opus 4.8 --- .../features/onboarding/steps/FunnelPersonaQuiz.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx index 520ae0fe55..61bf9eb229 100644 --- a/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx +++ b/packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz.tsx @@ -355,11 +355,11 @@ const MAGIC_PARTICLES = [ // randomness) so it's SSR-stable. const FIREWORK_COLORS = [ 'var(--theme-accent-cabbage-default)', - 'var(--theme-accent-cheese-default)', - 'var(--theme-accent-bacon-default)', - 'var(--theme-accent-avocado-default)', - 'var(--theme-accent-water-default)', - 'var(--theme-accent-bun-default)', + 'var(--theme-accent-cabbage-bolder)', + 'var(--theme-accent-cabbage-subtler)', + 'var(--theme-accent-onion-default)', + 'var(--theme-accent-onion-bolder)', + 'var(--theme-accent-onion-subtler)', ]; const REVEAL_FIREWORK = Array.from({ length: 30 }, (_, index) => {