-
+
diff --git a/src/components/sections/home/SolutionSection.tsx b/src/components/sections/home/SolutionSection.tsx
index bafdb75..379a5fc 100644
--- a/src/components/sections/home/SolutionSection.tsx
+++ b/src/components/sections/home/SolutionSection.tsx
@@ -1,3 +1,6 @@
+'use client';
+
+import { useState } from 'react';
import { FadeIn } from '@/components/ui/FadeIn';
import { CheckCircleIcon } from '@/components/ui/icons';
@@ -26,11 +29,32 @@ const CHECKLIST = [
'Real-time financial dashboard',
];
+// Per-card: stack position + whether to show grayscale overlay in idle state
+const STACK = [
+ {
+ pos: '',
+ hover: 'hover:-translate-y-10',
+ idle: "before:absolute before:inset-0 before:z-10 before:rounded-xl before:content-[''] before:bg-bg/50 before:transition-opacity before:duration-500 grayscale",
+ },
+ {
+ pos: 'translate-x-20 translate-y-12',
+ hover: 'hover:-translate-y-2',
+ idle: "before:absolute before:inset-0 before:z-10 before:rounded-xl before:content-[''] before:bg-bg/50 before:transition-opacity before:duration-500 grayscale",
+ },
+ {
+ pos: 'translate-x-40 translate-y-24',
+ hover: 'hover:translate-y-6',
+ idle: '',
+ },
+];
+
export function SolutionSection() {
+ const [active, setActive] = useState
(null);
+
return (
-
+
{/* Left — text */}
@@ -56,22 +80,47 @@ export function SolutionSection() {
- {/* Right — numbered features */}
-
-
- {FEATURES.map((f) => (
-
-
- {f.number}
-
-
-
- {f.title}
-
-
{f.body}
+ {/* Right — stacked feature cards */}
+
+
+ {FEATURES.map((f, i) => {
+ const isActive = active === i;
+ const isDimmed = active !== null && !isActive;
+ const { pos, hover, idle } = STACK[i];
+
+ return (
+
setActive(isActive ? null : i)}
+ className={[
+ '[grid-area:stack]',
+ pos,
+ 'relative flex h-56 w-[30rem] cursor-pointer flex-col justify-between rounded-xl border px-8 py-6 transition-all duration-500 select-none',
+ isActive
+ ? 'border-emerald/30 z-50 skew-y-0 bg-white shadow-2xl'
+ : [
+ '-skew-y-[8deg] border-black/7 bg-white/90',
+ hover,
+ "after:from-bg after:absolute after:top-[-5%] after:-right-1 after:h-[110%] after:w-80 after:bg-gradient-to-l after:to-transparent after:content-['']",
+ idle,
+ ].join(' '),
+ isDimmed ? 'opacity-40' : '',
+ ]
+ .join(' ')
+ .trim()}
+ >
+
+
+ {f.number}
+
+
+ {f.title}
+
+
+
{f.body}
-
- ))}
+ );
+ })}
diff --git a/src/components/ui/BeamButton.tsx b/src/components/ui/BeamButton.tsx
new file mode 100644
index 0000000..c89e825
--- /dev/null
+++ b/src/components/ui/BeamButton.tsx
@@ -0,0 +1,144 @@
+'use client';
+
+import { useEffect, useId, useRef, useState } from 'react';
+import { motion } from 'framer-motion';
+import { ArrowRightIcon } from './icons';
+import { Spinner } from './Spinner';
+
+type Size = 'sm' | 'md' | 'lg';
+type Variant = 'dark' | 'light';
+
+interface BeamButtonProps extends React.ButtonHTMLAttributes
{
+ size?: Size;
+ variant?: Variant;
+ loading?: boolean;
+ trailingArrow?: boolean;
+ leadingIcon?: React.ReactNode;
+}
+
+const HEIGHTS: Record = { sm: 36, md: 44, lg: 56 };
+const PADDINGS: Record = {
+ sm: 'px-4 text-sm',
+ md: 'px-6 text-base',
+ lg: 'px-8 text-lg',
+};
+const GAPS: Record = { sm: 'gap-1.5', md: 'gap-2', lg: 'gap-2.5' };
+
+const VARIANTS: Record = {
+ dark: {
+ bg: 'bg-charcoal hover:bg-charcoal/85',
+ text: 'text-white',
+ border: 'rgba(255,255,255,0.1)',
+ },
+ light: {
+ bg: 'bg-white hover:bg-emerald-mist',
+ text: 'text-charcoal',
+ border: 'rgba(22,37,32,0.1)',
+ },
+};
+
+export function BeamButton({
+ children,
+ size = 'md',
+ variant = 'dark',
+ loading = false,
+ trailingArrow = false,
+ leadingIcon,
+ className = '',
+ disabled,
+ ...props
+}: BeamButtonProps) {
+ const uid = useId().replace(/:/g, '');
+ const ref = useRef(null);
+ const [w, setW] = useState(180);
+
+ useEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+ const ro = new ResizeObserver(([e]) => setW(Math.round(e.contentRect.width)));
+ ro.observe(el);
+ return () => ro.disconnect();
+ }, []);
+
+ const h = HEIGHTS[size];
+ // Perimeter of pill: two straight edges + one full circle (r = h/2)
+ const perimeter = 2 * (w - h) + Math.PI * h;
+ const beamLen = Math.min(perimeter * 0.28, 90);
+ const { bg, text, border } = VARIANTS[variant];
+ const arrowSize = size === 'sm' ? 14 : size === 'lg' ? 20 : 16;
+
+ return (
+
+ );
+}
diff --git a/src/components/ui/TextHoverEffect.tsx b/src/components/ui/TextHoverEffect.tsx
index d65126f..98d7734 100644
--- a/src/components/ui/TextHoverEffect.tsx
+++ b/src/components/ui/TextHoverEffect.tsx
@@ -20,46 +20,36 @@ export const TextHoverEffect = ({
const [maskPosition, setMaskPosition] = useState({ cx: '50%', cy: '50%' });
useEffect(() => {
- if (svgRef.current && cursor.x !== null && cursor.y !== null) {
+ if (svgRef.current) {
const svgRect = svgRef.current.getBoundingClientRect();
const cxPercentage = ((cursor.x - svgRect.left) / svgRect.width) * 100;
const cyPercentage = ((cursor.y - svgRect.top) / svgRect.height) * 100;
- setMaskPosition({
- cx: `${cxPercentage}%`,
- cy: `${cyPercentage}%`,
- });
+ setMaskPosition({ cx: `${cxPercentage}%`, cy: `${cyPercentage}%` });
}
}, [cursor]);
+ // First word → blue (#1B9AD6), remaining → emerald (#3CC68A) — matches logo
+ const [firstWord, ...rest] = text.split(' ');
+ const secondWord = rest.join(' ');
+
return (
);
@@ -124,7 +143,7 @@ export const FooterBackgroundGradient = () => (
className="absolute inset-0 z-0"
style={{
background:
- 'radial-gradient(125% 125% at 50% 10%, rgba(22,37,32,0.6) 50%, rgba(27,154,214,0.15) 100%)',
+ 'radial-gradient(110% 110% at 50% 0%, rgba(22,37,32,0.8) 40%, rgba(27,154,214,0.1) 100%)',
}}
/>
);