-
-
-
- Why Familiarise?
-
-
- Transform your career with{" "}
- expert guidance
-
-
- Join thousands of professionals who have accelerated their careers
- through personalized mentorship and expert guidance.
-
+ {/* Session imagery */}
+
+
+ {renderLCPImage(images, 0, "/placeholder.svg", 600, 400)}
+
+
+
-
- {BENEFITS.map((benefit, index) => (
-
-
-
-
-
-
+ {/* Benefits ledger */}
+
+ {BENEFITS.map((benefit, index) => (
+
+
+
+ {String(index + 1).padStart(2, "0")}
+
+
+
+
{benefit.title}
-
+
{benefit.description}
-
- ))}
-
-
-
-
-
- {renderLCPImage(images, 0, "/placeholder.svg", 600, 400)}
-
- {/* Floating card decoration */}
-
-
-
-
-
-
- Session Complete
-
-
- Great progress today!
-
-
-
-
-
+
+ ))}
+
+
+ {/* Soft ambient wash */}
+
);
}
diff --git a/components/home/CTAFinaleSection.tsx b/components/home/CTAFinaleSection.tsx
new file mode 100644
index 000000000..1149fe2d1
--- /dev/null
+++ b/components/home/CTAFinaleSection.tsx
@@ -0,0 +1,126 @@
+"use client";
+
+import { motion } from "framer-motion";
+import { ArrowRight, Building2, Mic } from "lucide-react";
+import Link from "next/link";
+
+import { Reveal } from "@/components/motion";
+
+const PATHS = [
+ {
+ icon: Mic,
+ eyebrow: "For experts",
+ title: "Teach what you know",
+ description:
+ "Set your rates, choose your schedule, and build your brand on a platform that handles payments, scheduling and delivery for you.",
+ cta: "Apply as an expert",
+ href: "/become-an-expert",
+ links: [
+ { label: "Expert networks", href: "/explore/enterprise/organisations" },
+ ],
+ },
+ {
+ icon: Building2,
+ eyebrow: "For teams",
+ title: "Upskill your organisation",
+ description:
+ "One plan covering every seat, sponsored sessions with budgets, GST-compliant invoicing — or run your own expert network.",
+ cta: "Explore for enterprise",
+ href: "/enterprise",
+ links: [{ label: "Browse organisations", href: "/explore/enterprise/organisations" }],
+ },
+];
+
+/**
+ * Closing CTA — a split stage where the hovered panel brightens while its
+ * sibling dims. Replaces the separate BecomeExpert/Enterprise sections so the
+ * page ends with one decisive "which side are you on?" choice.
+ */
+export function CTAFinaleSection() {
+ return (
+
+
+
+ Join Familiarise
+
+ {PATHS.map((path, i) => (
+
+ {/* Hover glow */}
+
+
+
+
+ {path.eyebrow}
+
+
+ {path.title}
+
+
+ {path.description}
+
+
+
+
+
+ {path.cta}
+
+
+ {path.links.map((link) => (
+
+ {link.label}
+
+ ))}
+
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/components/home/CategoriesSection.tsx b/components/home/CategoriesSection.tsx
index 540c34e92..206d2d872 100644
--- a/components/home/CategoriesSection.tsx
+++ b/components/home/CategoriesSection.tsx
@@ -1,108 +1,124 @@
"use client";
+import { useRef } from "react";
import { motion } from "framer-motion";
-import { ChevronRight, LucideIcon } from "lucide-react";
+import { LucideIcon } from "lucide-react";
import Link from "next/link";
import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Card, CardContent } from "@/components/ui/card";
import { CATEGORIES } from "./data";
-function CategoryCard({
+function CategoryTile({
category,
index,
}: {
- category: { icon: LucideIcon; name: string; count: string; color: string };
+ category: { icon: LucideIcon; name: string; count: string };
index: number;
}) {
const Icon = category.icon;
return (
-
-
-
-
-
-
-
-
-
- {category.name}
-
-
- {category.count}
-
-
-
-
-
-
-
+ {/* Invert on hover */}
+
+
+
+
+ {String(index + 1).padStart(2, "0")}
+
+
+
+
+ {category.name}
+
+
+ {category.count}
+
+
+
);
}
export function CategoriesSection() {
- return (
-
-
+ const railRef = useRef(null);
-
+ return (
+
+
-
- Categories
-
-
- Browse by expertise
-
-
- Find experts in your field and start learning from the best
-
+
+
+ Categories
+
+
+ Browse by expertise
+
+
+ {/* Scroll affordance for pointer devices; touch users swipe natively */}
+
+ {[-1, 1].map((dir) => (
+
+ ))}
+
-
- {CATEGORIES.map((category, index) => (
-
- ))}
-
-
-
-
-
+
+ {CATEGORIES.map((category, index) => (
+
+ ))}
+
diff --git a/components/home/EnterpriseSection.tsx b/components/home/EnterpriseSection.tsx
deleted file mode 100644
index 009039289..000000000
--- a/components/home/EnterpriseSection.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-"use client";
-
-import { motion } from "framer-motion";
-import { ArrowRight, Building2 } from "lucide-react";
-import Link from "next/link";
-
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-
-import { ENTERPRISE_FEATURES } from "./data";
-
-/**
- * The B2C page had no path for the buyer who arrives on behalf of an
- * organisation — the enterprise subsystem (sponsored bookings, procurement
- * invoicing, hosted expert networks) was invisible from the landing page.
- */
-export function EnterpriseSection() {
- return (
-
-
-
-
-
-
-
-
- For teams & organisations
-
-
- Bring Familiarise to your{" "}
- whole team
-
-
- Sponsor sessions for your people, run structured mentorship
- programs, or host your own experts on the platform — with the
- billing and compliance your finance team expects.
-
-
-
-
- {ENTERPRISE_FEATURES.map((feature, index) => {
- const Icon = feature.icon;
- return (
-
-
-
-
-
-
- {feature.title}
-
-
- {feature.description}
-
-
-
- );
- })}
-
-
-
-
-
-
-
-
- );
-}
diff --git a/components/home/FAQSection.tsx b/components/home/FAQSection.tsx
index f107e9e16..a9b02f14b 100644
--- a/components/home/FAQSection.tsx
+++ b/components/home/FAQSection.tsx
@@ -1,67 +1,72 @@
"use client";
import { motion } from "framer-motion";
+import Link from "next/link";
+import { Reveal } from "@/components/motion";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
-import { Badge } from "@/components/ui/badge";
import { FAQ_ITEMS } from "./data";
export function FAQSection() {
return (
-
+
-
-
- FAQ
-
-
- Common questions
-
-
- Everything you need to know about getting started
-
-
+
+ {/* Editorial column */}
+
+
+ Common questions
+
+
+ Everything you need to know about getting started. Still unsure?{" "}
+
+ Talk to us
+
+ .
+
+
-
-
- {FAQ_ITEMS.map((item, index) => (
-
-
-
- {item.question}
-
-
-
- {item.answer}
-
-
- ))}
-
-
+
+
+ {FAQ_ITEMS.map((item, index) => (
+
+
+
+
+ {String(index + 1).padStart(2, "0")}
+
+
+ {item.question}
+
+
+
+
+ {item.answer}
+
+
+ ))}
+
+
+
);
diff --git a/components/home/FeaturedExpertsSection.tsx b/components/home/FeaturedExpertsSection.tsx
index 3c77c3054..675c807de 100644
--- a/components/home/FeaturedExpertsSection.tsx
+++ b/components/home/FeaturedExpertsSection.tsx
@@ -4,74 +4,73 @@ import { motion } from "framer-motion";
import { ChevronRight, Star } from "lucide-react";
import Link from "next/link";
+import { MarqueeRow, TiltCard } from "@/components/motion";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
-import { Card, CardContent } from "@/components/ui/card";
import type { IConsultantCardData } from "@/types/consultant";
function ExpertCard({ expert }: { expert: IConsultantCardData }) {
return (
-
-
-
-
-
+
+
+
+
+
-
+
{expert.user.name?.charAt(0) ?? "E"}
-
-
+
+
{expert.user.name}
-
+
{expert.headline || expert.domain?.name}
-
-
-
-
+
+
+
+
{expert.rating.toFixed(1)}
+ ·
+ {expert.experience}
-
•
-
- {expert.experience}
-
-
-
- {expert.tags?.slice(0, 3).map((tag) => (
-
- {tag.name}
-
- ))}
+
+ {expert.tags?.slice(0, 3).map((tag) => (
+
+ {tag.name}
+
+ ))}
+
-
-
-
+
+
+
);
}
function ExpertLoadingSkeleton() {
return (
-
-
+
);
}
@@ -86,63 +85,47 @@ export function FeaturedExpertsSection({
isLoading,
}: FeaturedExpertsSectionProps) {
return (
-
-
-
-
+
+
-
+
Featured Experts
-
+
Learn from{" "}
industry leaders
-
- Handpicked professionals ready to guide your journey
+
+ Handpicked professionals ready to guide your journey.
-
- {/* Marquee */}
-
-
-
-
-
- {isLoading ? (
- Array.from({ length: 6 }).map((_, i) => (
-
+ {/* Pause on hover so cards stay readable and clickable; reduced-motion
+ users get native scrolling via the globals.css override. */}
+
+ {isLoading
+ ? Array.from({ length: 6 }).map((_, i) => (
+
))
- ) : (
- <>
- {[...experts, ...experts].map((expert, i) => (
-
- ))}
- >
- )}
-
-
+ : experts.map((expert) => (
+
+ ))}
+
);
}
diff --git a/components/home/FeaturesSection.tsx b/components/home/FeaturesSection.tsx
index 34ff893ad..854cded79 100644
--- a/components/home/FeaturesSection.tsx
+++ b/components/home/FeaturesSection.tsx
@@ -2,12 +2,16 @@
import { motion } from "framer-motion";
import { LucideIcon } from "lucide-react";
+import Link from "next/link";
+import { RevealGroup, RevealItem } from "@/components/motion";
import { Badge } from "@/components/ui/badge";
-import { Card, CardContent } from "@/components/ui/card";
import { FEATURES } from "./data";
-function FeatureCard({
+/** Bento layout: generous half-width cells in a 2×2 arrangement. */
+const SPANS = ["", "", "", ""];
+
+function OfferingCell({
feature,
index,
}: {
@@ -22,67 +26,97 @@ function FeatureCard({
const Icon = feature.icon;
return (
-
-
-
-
-
-
-
+
+
+ {/* Hover glow */}
+
+
+
+
+
+
+
+
+ {String(index + 1).padStart(2, "0")}
+
-
+
{feature.title}
-
{feature.description}
-
-
-
+
+ {feature.description}
+
+
+
+
+ Explore
+
+
+
+
);
}
export function FeaturesSection() {
return (
-
-
-
- {/* Accent gradient */}
-
-
+
-
- Our Offerings
-
-
- Multiple ways to learn & grow
-
-
- Choose the format that works best for your learning style and
- schedule
+
+
+ Our Offerings
+
+
+ Four ways to learn & grow
+
+
+
+ Pick the format that fits your learning style and schedule.
-
+
{FEATURES.map((feature, index) => (
-
+
))}
-
+
);
diff --git a/components/home/HeroSection.tsx b/components/home/HeroSection.tsx
index af8ca52fd..0a02e37da 100644
--- a/components/home/HeroSection.tsx
+++ b/components/home/HeroSection.tsx
@@ -1,169 +1,181 @@
"use client";
-import { useCallback } from "react";
-import { motion, useInView } from "framer-motion";
+import { motion } from "framer-motion";
import { ArrowRight, Sparkles } from "lucide-react";
import Link from "next/link";
-import { useRef, useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
-import { STATS } from "./data";
-
-function AnimatedNumber({
- value,
- suffix = "",
-}: {
- value: number;
- suffix: string;
-}) {
- const ref = useRef(null);
- const isInView = useInView(ref, { once: true });
- const [displayValue, setDisplayValue] = useState(0);
-
- const animate = useCallback(() => {
- const duration = 2000;
- const steps = 60;
- const increment = value / steps;
- let current = 0;
- const timer = setInterval(() => {
- current += increment;
- if (current >= value) {
- setDisplayValue(value);
- clearInterval(timer);
- } else {
- setDisplayValue(Math.floor(current));
- }
- }, duration / steps);
- return () => clearInterval(timer);
- }, [value]);
-
- useEffect(() => {
- if (isInView) {
- return animate();
- }
- }, [isInView, animate]);
-
- return (
-
- {value % 1 !== 0
- ? displayValue.toFixed(1)
- : displayValue.toLocaleString()}
- {suffix}
-
- );
-}
+import {
+ AnimatedNumber,
+ MarqueeRow,
+ SpotlightGrid,
+ wordRevealChild,
+ wordRevealParent,
+} from "@/components/motion";
+import { COMPANY_LOGOS, STATS } from "./data";
+/**
+ * Hero — full-viewport black stage with a cursor-following spotlight grid.
+ * The headline rises word-by-word from a clipped line (SSR-safe: words are
+ * split at module scope, not in an effect), stats count up on load, and the
+ * trusted-by logo cloud rides a marquee along the bottom edge.
+ */
export function HeroSection() {
- return (
-
- {/* Animated gradient orbs */}
-
-
- {/* Grid pattern overlay */}
-
+ const headlineTop = ["Learn", "from", "the"];
+ const headlineAccent = ["best", "minds"];
+ const headlineBottom = ["in", "your", "industry"];
- {/* Spotlight effect */}
-
-
-
-
- {/* Badge */}
-
-
- Trusted by 10,000+ professionals worldwide
-
+ return (
+
+ {/* Interactive backdrop */}
+
+
+
- {/* Main headline */}
-
- Learn from the{" "}
-
- best minds
-
-
- in your industry
-
+
+
+
+ {/* Availability pill */}
+
+
+
+
+
+ Mentors available this week
+
- {/* Subheadline */}
-
- Connect with world-class experts for personalized 1-on-1 sessions,
- interactive classes, and live webinars. Your career transformation
- starts here.
-
+ {/* Headline — word-by-word clip reveal */}
+
+ {[headlineTop, headlineAccent, headlineBottom].map(
+ (words, lineIdx) => (
+
+ {lineIdx === 2 && (
+ in your industry
+ )}
+ {words.map((word, wordIdx) => (
+
+
+ {word}
+ {wordIdx < words.length - 1 ? "\u00A0" : ""}
+
+
+ ))}
+
+ ),
+ )}
+
- {/* CTAs */}
-
-
-
- Find Your Expert
-
-
-
- {/* The supply-side CTA lives here now that the navbar carries no
- CTAs. (It replaces a "Watch Demo" button that had no href and
- no handler — it did nothing when clicked.) */}
-
+
+ {/* CTAs */}
+
-
- Become an Expert
-
-
-
-
+
+
+ Find Your Expert
+
+
+
+
+ Become an Expert
+
+
- {/* Stats with animated counters */}
-
- {STATS.map((stat, i) => (
-
- ))}
-
+ {/* Stats strip */}
+
+ {STATS.map((stat) => (
+
+ ))}
+
+
- {/* Bottom gradient fade */}
-
+ {/* Trusted-by marquee pinned to the hero's base */}
+
+
+
+ Mentors from
+
+
+ {COMPANY_LOGOS.map((logo) => (
+
+ {logo}
+
+ ))}
+
+
+
+ {/* Bottom fade into the offerings band */}
+
);
}
diff --git a/components/home/HowItWorksSection.tsx b/components/home/HowItWorksSection.tsx
index 10dd56d05..de9c2c74d 100644
--- a/components/home/HowItWorksSection.tsx
+++ b/components/home/HowItWorksSection.tsx
@@ -1,108 +1,68 @@
"use client";
import { motion } from "framer-motion";
-import { ArrowRight } from "lucide-react";
-import Link from "next/link";
+import { RevealGroup, RevealItem, SpotlightGrid } from "@/components/motion";
import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
import { HOW_IT_WORKS } from "./data";
-function HowItWorksStep({
- step,
- index,
- isLast,
-}: {
- step: (typeof HOW_IT_WORKS)[0];
- index: number;
- isLast: boolean;
-}) {
- return (
-
-
-
-
- {step.step}
-
- {!isLast && (
-
- )}
-
-
-
- {step.title}
-
-
- {step.description}
-
-
-
-
- );
-}
-
+/**
+ * How it works — a numbered progress line across a dark stage. The connector
+ * draws itself as the steps scroll into view; each step lifts its number chip
+ * on hover.
+ */
export function HowItWorksSection() {
return (
-
-
-
- {/* Decorative circles */}
-
-
-
+
+
-
-
+
-
- Getting Started
-
-
- How it works
-
-
- Get started in minutes. Our streamlined process makes it easy to
- connect with the right expert for your needs.
-
-
-
- Start Your Journey
-
-
-
-
+ How it works
+
+
+ From browsing to breakthrough in{" "}
+ four steps
+
+
-
- {HOW_IT_WORKS.map((step, index) => (
-
- ))}
-
-
+
+ {/* Connector line (desktop) */}
+
+ {HOW_IT_WORKS.map((step) => (
+
+
+
+
+ {step.step}
+
+
+
+
+ {step.title}
+
+
+ {step.description}
+
+
+
+ ))}
+
);
diff --git a/components/home/LandingHeroSkeleton.tsx b/components/home/LandingHeroSkeleton.tsx
index dbded3e40..d88f8cdc6 100644
--- a/components/home/LandingHeroSkeleton.tsx
+++ b/components/home/LandingHeroSkeleton.tsx
@@ -12,8 +12,8 @@ export function LandingHeroSkeleton() {
-
-
+
+
{[1, 2, 3, 4].map((i) => (
diff --git a/components/home/PlatformFeaturesSection.tsx b/components/home/PlatformFeaturesSection.tsx
deleted file mode 100644
index 63c69e9af..000000000
--- a/components/home/PlatformFeaturesSection.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-"use client";
-
-import { motion } from "framer-motion";
-import { LucideIcon } from "lucide-react";
-
-import { Badge } from "@/components/ui/badge";
-import { PLATFORM_FEATURES } from "./data";
-
-function PlatformFeatureCard({
- feature,
- index,
-}: {
- feature: { icon: LucideIcon; title: string; description: string };
- index: number;
-}) {
- const Icon = feature.icon;
-
- return (
-
-
-
-
- {feature.title}
- {feature.description}
-
- );
-}
-
-export function PlatformFeaturesSection() {
- return (
-
-
-
-
-
-
- Platform Features
-
-
- Everything you need to{" "}
- succeed
-
-
- A seamless experience built for learning and growth
-
-
-
-
- {PLATFORM_FEATURES.map((feature, index) => (
-
- ))}
-
-
-
- );
-}
diff --git a/components/home/SuccessStoriesSection.tsx b/components/home/SuccessStoriesSection.tsx
deleted file mode 100644
index 36418a8ff..000000000
--- a/components/home/SuccessStoriesSection.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-"use client";
-
-import { motion } from "framer-motion";
-import { Rocket } from "lucide-react";
-
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
-import { Badge } from "@/components/ui/badge";
-import { Card, CardContent } from "@/components/ui/card";
-import { SUCCESS_STORIES } from "./data";
-
-function SuccessStoryCard({
- story,
- index,
-}: {
- story: (typeof SUCCESS_STORIES)[0];
- index: number;
-}) {
- return (
-
-
-
-
-
-
-
- {story.name.charAt(0)}
-
-
-
-
{story.name}
-
{story.role}
-
Now at {story.company}
-
-
-
- “{story.story}”
-
-
-
- {story.metric}
-
-
-
-
- );
-}
-
-export function SuccessStoriesSection() {
- return (
-
-
-
-
-
-
-
-
-
- Success Stories
-
-
- Real transformations,{" "}
- real results
-
-
- See how our mentees have achieved their career goals
-
-
-
-
- {SUCCESS_STORIES.map((story, index) => (
-
- ))}
-
-
-
- );
-}
diff --git a/components/home/TestimonialsSection.tsx b/components/home/TestimonialsSection.tsx
index 9717f2435..06bf34a0a 100644
--- a/components/home/TestimonialsSection.tsx
+++ b/components/home/TestimonialsSection.tsx
@@ -1,59 +1,54 @@
"use client";
import { useMemo } from "react";
-import { motion } from "framer-motion";
import { Star } from "lucide-react";
+import { MarqueeRow, Reveal } from "@/components/motion";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
-import { Card, CardContent } from "@/components/ui/card";
import type { ReviewWithProfiles } from "@/types/review";
function TestimonialCard({ review }: { review: ReviewWithProfiles }) {
return (
-
-
-
- {Array.from({ length: 5 }).map((_, i) => (
-
- ))}
-
-
- “
- {review.reviewDescription ||
- "Great experience working with this expert!"}
- ”
-
-
-
-
-
- {review.consulteeProfile?.user?.name?.charAt(0) ?? "U"}
-
-
-
-
- {review.consulteeProfile?.user?.name || "Anonymous"}
-
-
- Session with {review.consultantProfile?.user?.name}
-
-
+
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+
+ “
+ {review.reviewDescription ||
+ "Great experience working with this expert!"}
+ ”
+
+
+
+
+
+ {review.consulteeProfile?.user?.name?.charAt(0) ?? "U"}
+
+
+
+
+ {review.consulteeProfile?.user?.name || "Anonymous"}
+
+
+ Session with {review.consultantProfile?.user?.name}
+
-
-
+
+
);
}
function TestimonialLoadingSkeleton() {
- return (
-
-
-
- );
+ return
;
}
interface TestimonialsSectionProps {
@@ -72,85 +67,53 @@ export function TestimonialsSection({
[reviews],
);
- return (
-
-
+ const skeletons = (
+ <>
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+ >
+ );
- {/* Glow accents */}
-
-
+ return (
+
+
-
-
+
+
Testimonials
-
- Loved by professionals
+
+ Trusted by professionals{" "}
+ worldwide
-
- See what our community has to say about their experience
+
+ Real stories from real sessions.
-
-
-
- {/* First marquee row - left to right */}
-
-
-
-
-
- {isLoading ? (
- Array.from({ length: 4 }).map((_, i) => (
-
- ))
- ) : (
- <>
- {[...displayReviews, ...displayReviews, ...displayReviews].map(
- (review, i) => (
-
- ),
- )}
- >
- )}
-
+
- {/* Second marquee row - right to left */}
-
-
-
-
-
- {isLoading ? (
- Array.from({ length: 4 }).map((_, i) => (
-
- ))
- ) : (
- <>
- {[...displayReviews, ...displayReviews, ...displayReviews]
- .reverse()
- .map((review, i) => (
-
- ))}
- >
- )}
-
+ {/* Two counter-drifting rows; both pause on hover for readability */}
+
+
+ {isLoading ? skeletons : displayReviews.map((r) => (
+
+ ))}
+
+
+ {isLoading
+ ? skeletons
+ : [...displayReviews].reverse().map((r) => (
+
+ ))}
+
);
diff --git a/components/home/TrustBadgesSection.tsx b/components/home/TrustBadgesSection.tsx
deleted file mode 100644
index 4f69cee29..000000000
--- a/components/home/TrustBadgesSection.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-"use client";
-
-import { motion } from "framer-motion";
-import { TRUST_BADGES } from "./data";
-
-export function TrustBadgesSection() {
- return (
-
-
-
- {TRUST_BADGES.map((badge, index) => (
-
-
-
-
- {badge.label}
- {badge.description}
-
- ))}
-
-
-
- );
-}
diff --git a/components/home/TrustedBySection.tsx b/components/home/TrustedBySection.tsx
deleted file mode 100644
index 34467e788..000000000
--- a/components/home/TrustedBySection.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-"use client";
-
-import { motion } from "framer-motion";
-import { COMPANY_LOGOS } from "./data";
-
-export function TrustedBySection() {
- return (
-
-
-
- Our experts have worked at leading companies
-
-
- {COMPANY_LOGOS.map((company, i) => (
-
- {company}
-
- ))}
-
-
-
- );
-}
diff --git a/components/home/UpcomingEventsSection.tsx b/components/home/UpcomingEventsSection.tsx
index a9f920676..69116f620 100644
--- a/components/home/UpcomingEventsSection.tsx
+++ b/components/home/UpcomingEventsSection.tsx
@@ -1,185 +1,89 @@
"use client";
-import { motion } from "framer-motion";
-import { Calendar, ChevronRight, Clock, Star } from "lucide-react";
+import { ArrowRight, Calendar, Clock, Users } from "lucide-react";
import Link from "next/link";
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { RevealGroup, RevealItem, Reveal } from "@/components/motion";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
-import { Card, CardContent } from "@/components/ui/card";
-import type { ReviewWithProfiles } from "@/types/review";
import { UPCOMING_EVENTS } from "./data";
-function EventCard({
- event,
- index,
-}: {
- event: (typeof UPCOMING_EVENTS)[0];
- index: number;
-}) {
+/**
+ * Upcoming events — static curated marketing data rendered on a dark strip
+ * beneath the testimonials marquee. Self-contained: it no longer duplicates
+ * the review cards that TestimonialsSection already owns.
+ */
+export function UpcomingEventsSection() {
return (
-
-
-
-
+
+
+
+ {/* Editorial column */}
+
- {event.type}
+ Live sessions
-
- {event.attendees} attending
-
-
-
- {event.title}
-
-
Hosted by {event.host}
-
-
-
- {event.date}
-
-
-
- {event.time}
-
-
-
-
-
- );
-}
-
-interface UpcomingEventsSectionProps {
- reviews: ReviewWithProfiles[];
-}
-
-export function UpcomingEventsSection({ reviews }: UpcomingEventsSectionProps) {
- const slicedReviews = reviews.slice(0, 3);
-
- return (
-
-
-
- {/* Glow accents */}
-
-
-
-
-
- {/* Testimonials Column */}
-
-
-
+ Something is always{" "}
+ happening
+
+
+ Workshops, webinars and cohort classes run every week — join one
+ or bring your whole team.
+
+
+
- Reviews
-
-
- What our users say
-
-
+ Browse programs
+
+
+
+
-
- {slicedReviews.map((review, i) => (
-
+ {UPCOMING_EVENTS.map((event) => (
+
+
-
-
-
- {Array.from({ length: 5 }).map((_, j) => (
-
- ))}
-
-
- “{review.reviewDescription || "Great experience!"}
- ”
-
-
-
-
-
- {review.consulteeProfile?.user?.name?.charAt(0) ??
- "U"}
-
-
-
- — {review.consulteeProfile?.user?.name || "Anonymous"}
-
-
-
-
-
- ))}
-
-
+
+
+ {event.type}
+
+
+ {event.title}
+
+
+ Hosted by {event.host}
+
+
- {/* Upcoming Events Column */}
-
-
-
- Coming Soon
-
-
- Upcoming events
-
-
-
-
- {UPCOMING_EVENTS.map((event, index) => (
-
- ))}
-
-
-
-
-
- View All Events
-
-
-
-
-
+
+
+
+ {event.date}
+
+
+
+ {event.time}
+
+
+
+ {event.attendees}
+
+
+
+
+
+ ))}
+
diff --git a/components/motion/AnimatedNumber.tsx b/components/motion/AnimatedNumber.tsx
new file mode 100644
index 000000000..bef3f7b83
--- /dev/null
+++ b/components/motion/AnimatedNumber.tsx
@@ -0,0 +1,52 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { animate, useInView } from "framer-motion";
+
+interface AnimatedNumberProps {
+ value: number;
+ suffix?: string;
+ prefix?: string;
+ /** Decimal places to render (0.5 ratings etc). */
+ decimals?: number;
+ className?: string;
+}
+
+/**
+ * Counts from zero to `value` with a single framer-motion tween when the
+ * element scrolls into view. Replaces the setInterval-based counter that used
+ * to live inside HeroSection — same behaviour, one animation primitive, and
+ * it honours MotionConfig reducedMotion="user" for free.
+ */
+export function AnimatedNumber({
+ value,
+ suffix = "",
+ prefix = "",
+ decimals = 0,
+ className,
+}: AnimatedNumberProps) {
+ const ref = useRef
(null);
+ const isInView = useInView(ref, { once: true, margin: "-40px" });
+ const [display, setDisplay] = useState(0);
+
+ useEffect(() => {
+ if (!isInView) return;
+ const controls = animate(0, value, {
+ duration: 1.6,
+ ease: [0.16, 1, 0.3, 1],
+ onUpdate: setDisplay,
+ });
+ return () => controls.stop();
+ }, [isInView, value]);
+
+ return (
+
+ {prefix}
+ {display.toLocaleString(undefined, {
+ minimumFractionDigits: decimals,
+ maximumFractionDigits: decimals,
+ })}
+ {suffix}
+
+ );
+}
diff --git a/components/motion/MarqueeRow.tsx b/components/motion/MarqueeRow.tsx
new file mode 100644
index 000000000..9eb2b06a3
--- /dev/null
+++ b/components/motion/MarqueeRow.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+interface MarqueeRowProps {
+ children: React.ReactNode;
+ /** Seconds for one full loop across the track. */
+ duration?: number;
+ reverse?: boolean;
+ className?: string;
+ /** Fade the left/right edges of the viewport container. */
+ fadeEdges?: boolean;
+}
+
+/**
+ * Infinite horizontal marquee built on the .animate-marquee keyframes in
+ * globals.css. The track carries the children twice; translateX(-50%) of the
+ * doubled track equals exactly one copy width, so the loop is seamless.
+ *
+ * Hovering pauses travel so items stay readable/clickable. Under
+ * prefers-reduced-motion globals.css stops the animation and hands scrolling
+ * back to the user.
+ */
+export function MarqueeRow({
+ children,
+ duration = 40,
+ reverse = false,
+ className = "",
+ fadeEdges = true,
+}: MarqueeRowProps) {
+ return (
+
+
+ {children}
+ {children}
+
+
+ );
+}
diff --git a/components/motion/MotionRoot.tsx b/components/motion/MotionRoot.tsx
new file mode 100644
index 000000000..724fc78f2
--- /dev/null
+++ b/components/motion/MotionRoot.tsx
@@ -0,0 +1,16 @@
+"use client";
+
+import { MotionConfig } from "framer-motion";
+
+/**
+ * Shared motion environment for the marketing surfaces. Wraps children in
+ * MotionConfig reducedMotion="user" so every framer-motion animation in the
+ * tree honours the OS reduce-motion setting.
+ *
+ * Server components must never import framer-motion directly — their client
+ * bindings can resolve undefined during hydration ("Element type is
+ * invalid"). Anything animated gets wrapped by this provider instead.
+ */
+export function MotionRoot({ children }: { children: React.ReactNode }) {
+ return {children};
+}
diff --git a/components/motion/Reveal.tsx b/components/motion/Reveal.tsx
new file mode 100644
index 000000000..03dde4ce6
--- /dev/null
+++ b/components/motion/Reveal.tsx
@@ -0,0 +1,106 @@
+"use client";
+
+import { motion, type Variants } from "framer-motion";
+import { fadeUp, staggerContainer } from "./presets";
+
+type RevealVariant = "fadeUp" | "fadeIn" | "blurIn" | "scaleIn";
+
+const VARIANTS: Record = {
+ fadeUp,
+ fadeIn: {
+ hidden: { opacity: 0 },
+ visible: { opacity: 1, transition: { duration: 0.6, ease: "easeOut" } },
+ },
+ blurIn: {
+ hidden: { opacity: 0, filter: "blur(8px)", y: 12 },
+ visible: {
+ opacity: 1,
+ filter: "blur(0px)",
+ y: 0,
+ transition: { duration: 0.7, ease: [0.16, 1, 0.3, 1] },
+ },
+ },
+ scaleIn: {
+ hidden: { opacity: 0, scale: 0.94 },
+ visible: {
+ opacity: 1,
+ scale: 1,
+ transition: { duration: 0.6, ease: [0.16, 1, 0.3, 1] },
+ },
+ },
+};
+
+interface RevealProps {
+ children: React.ReactNode;
+ variant?: RevealVariant;
+ /** Seconds of delay before the reveal starts. */
+ delay?: number;
+ className?: string;
+ /** Animate every time the element enters the viewport (default: once). */
+ repeat?: boolean;
+}
+
+/**
+ * Scroll-triggered entrance for any block. Renders a plain div until it is in
+ * view, so content is never trapped invisible if JS fails — the initial state
+ * only applies once the element mounts as a client component.
+ */
+export function Reveal({
+ children,
+ variant = "fadeUp",
+ delay = 0,
+ className,
+ repeat = false,
+}: RevealProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+/** Parent that staggers its immediate children into view. */
+export function RevealGroup({
+ children,
+ stagger = 0.08,
+ delayChildren = 0.05,
+ className,
+}: {
+ children: React.ReactNode;
+ stagger?: number;
+ delayChildren?: number;
+ className?: string;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function RevealItem({
+ children,
+ className,
+}: {
+ children: React.ReactNode;
+ className?: string;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/components/motion/SmoothScroll.tsx b/components/motion/SmoothScroll.tsx
new file mode 100644
index 000000000..a129689c3
--- /dev/null
+++ b/components/motion/SmoothScroll.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import { ReactLenis } from "lenis/react";
+
+import { MotionRoot } from "./MotionRoot";
+
+/**
+ * Inertial smooth scrolling + shared motion config for the marketing surfaces
+ * (landing, explore). Scoped per page tree rather than the root layout so
+ * dashboard/app routes keep native scrolling. Lenis drives window scroll, so
+ * sticky positioning, anchor links and scroll restoration all keep working.
+ */
+export function SmoothScroll({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/components/motion/SpotlightGrid.tsx b/components/motion/SpotlightGrid.tsx
new file mode 100644
index 000000000..9c92b4388
--- /dev/null
+++ b/components/motion/SpotlightGrid.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import { useRef } from "react";
+
+/**
+ * Cursor-following spotlight over a hairline grid. Writes --sx/--sy (px) on
+ * the container; the .spotlight-grid utility in globals.css paints both the
+ * masked grid and the soft radial from those variables. Purely decorative —
+ * pointer-events: none, and static fallback position when untouched.
+ */
+export function SpotlightGrid({ className = "" }: { className?: string }) {
+ const ref = useRef(null);
+
+ function onPointerMove(e: React.PointerEvent) {
+ if (e.pointerType !== "mouse" || !ref.current) return;
+ const rect = ref.current.getBoundingClientRect();
+ ref.current.style.setProperty("--sx", `${e.clientX - rect.left}px`);
+ ref.current.style.setProperty("--sy", `${e.clientY - rect.top}px`);
+ }
+
+ return (
+
+ );
+}
diff --git a/components/motion/TiltCard.tsx b/components/motion/TiltCard.tsx
new file mode 100644
index 000000000..2e43f8118
--- /dev/null
+++ b/components/motion/TiltCard.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import { useRef, useState } from "react";
+import {
+ motion,
+ useMotionTemplate,
+ useMotionValue,
+ useSpring,
+ useTransform,
+} from "framer-motion";
+
+interface TiltCardProps {
+ children: React.ReactNode;
+ className?: string;
+ /** Max rotation in degrees at the card edge. */
+ maxTilt?: number;
+ /** Render a specular glare that tracks the pointer. */
+ glare?: boolean;
+}
+
+/**
+ * Pointer-tracked 3D tilt with optional moving glare. Springs keep the motion
+ * physical; on touch devices there is no hover pointer, so the handlers simply
+ * never fire and the card renders static. Reduced-motion users get a static
+ * card too via MotionConfig reducedMotion="user".
+ */
+export function TiltCard({
+ children,
+ className = "",
+ maxTilt = 8,
+ glare = true,
+}: TiltCardProps) {
+ const ref = useRef(null);
+ const [hovering, setHovering] = useState(false);
+
+ const px = useMotionValue(0.5);
+ const py = useMotionValue(0.5);
+
+ const rotateX = useSpring(useTransform(py, [0, 1], [maxTilt, -maxTilt]), {
+ stiffness: 180,
+ damping: 20,
+ });
+ const rotateY = useSpring(useTransform(px, [0, 1], [-maxTilt, maxTilt]), {
+ stiffness: 180,
+ damping: 20,
+ });
+
+ const glareX = useTransform(px, (v) => `${v * 100}%`);
+ const glareY = useTransform(py, (v) => `${v * 100}%`);
+ const glareBg = useMotionTemplate`radial-gradient(circle at ${glareX} ${glareY}, rgba(255,255,255,0.18), transparent 60%)`;
+
+ function onPointerMove(e: React.PointerEvent) {
+ if (e.pointerType !== "mouse" || !ref.current) return;
+ const rect = ref.current.getBoundingClientRect();
+ px.set((e.clientX - rect.left) / rect.width);
+ py.set((e.clientY - rect.top) / rect.height);
+ }
+
+ return (
+ {
+ if (e.pointerType === "mouse") setHovering(true);
+ }}
+ onPointerLeave={() => {
+ setHovering(false);
+ px.set(0.5);
+ py.set(0.5);
+ }}
+ >
+ {children}
+ {glare && hovering && (
+
+ )}
+
+ );
+}
diff --git a/components/motion/index.ts b/components/motion/index.ts
new file mode 100644
index 000000000..0635d33f8
--- /dev/null
+++ b/components/motion/index.ts
@@ -0,0 +1,19 @@
+export { AnimatedNumber } from "./AnimatedNumber";
+export { MarqueeRow } from "./MarqueeRow";
+export { MotionRoot } from "./MotionRoot";
+export { Reveal, RevealGroup, RevealItem } from "./Reveal";
+export { SmoothScroll } from "./SmoothScroll";
+export { SpotlightGrid } from "./SpotlightGrid";
+export { TiltCard } from "./TiltCard";
+export {
+ EASE_OUT_EXPO,
+ blurIn,
+ fadeUp,
+ fadeIn,
+ scaleIn,
+ springSnappy,
+ springSoft,
+ staggerContainer,
+ wordRevealChild,
+ wordRevealParent,
+} from "./presets";
diff --git a/components/motion/presets.ts b/components/motion/presets.ts
new file mode 100644
index 000000000..18e04450f
--- /dev/null
+++ b/components/motion/presets.ts
@@ -0,0 +1,73 @@
+import type { Transition, Variants } from "framer-motion";
+
+/** Signature easing of the overhaul — fast start, long glide. Mirrors --ease-out-expo in globals.css. */
+export const EASE_OUT_EXPO = [0.16, 1, 0.3, 1] as const;
+
+export const springSoft: Transition = {
+ type: "spring",
+ stiffness: 120,
+ damping: 20,
+ mass: 0.9,
+};
+
+export const springSnappy: Transition = {
+ type: "spring",
+ stiffness: 400,
+ damping: 32,
+};
+
+export const fadeUp: Variants = {
+ hidden: { opacity: 0, y: 28 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.7, ease: EASE_OUT_EXPO },
+ },
+};
+
+export const fadeIn: Variants = {
+ hidden: { opacity: 0 },
+ visible: { opacity: 1, transition: { duration: 0.6, ease: "easeOut" } },
+};
+
+export const blurIn: Variants = {
+ hidden: { opacity: 0, filter: "blur(8px)", y: 12 },
+ visible: {
+ opacity: 1,
+ filter: "blur(0px)",
+ y: 0,
+ transition: { duration: 0.7, ease: EASE_OUT_EXPO },
+ },
+};
+
+export const scaleIn: Variants = {
+ hidden: { opacity: 0, scale: 0.94 },
+ visible: {
+ opacity: 1,
+ scale: 1,
+ transition: { duration: 0.6, ease: EASE_OUT_EXPO },
+ },
+};
+
+export const staggerContainer = (
+ stagger = 0.08,
+ delayChildren = 0.05,
+): Variants => ({
+ hidden: {},
+ visible: { transition: { staggerChildren: stagger, delayChildren } },
+});
+
+/** Word-level headline reveal: parent staggers, children rise from a clipped line. */
+export const wordRevealParent: Variants = {
+ hidden: {},
+ visible: { transition: { staggerChildren: 0.045 } },
+};
+
+export const wordRevealChild: Variants = {
+ hidden: { y: "110%", rotate: 4 },
+ visible: {
+ y: "0%",
+ rotate: 0,
+ transition: { duration: 0.85, ease: EASE_OUT_EXPO },
+ },
+};
diff --git a/package-lock.json b/package-lock.json
index 43d0a0945..013be2a74 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -57,6 +57,7 @@
"date-fns-tz": "^3.2.0",
"framer-motion": "^11.15.0",
"iso-639-1": "^3.1.5",
+ "lenis": "^1.3.26",
"libsodium-wrappers": "^0.8.2",
"lucide-react": "^0.563.0",
"next": "^15.5.0",
@@ -14518,6 +14519,37 @@
"url": "https://ko-fi.com/killymxi"
}
},
+ "node_modules/lenis": {
+ "version": "1.3.26",
+ "resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.26.tgz",
+ "integrity": "sha512-s/xTCZCxTFvHbAN1OzuhNaN5YPJH2ail0XAkctKW1b+RUAG4nUL5UHLXwNko1h8aEeT2jspBXegMgPJd8zcuag==",
+ "license": "MIT",
+ "workspaces": [
+ "packages/*",
+ "playground",
+ "playground/*"
+ ],
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/darkroomengineering"
+ },
+ "peerDependencies": {
+ "@nuxt/kit": ">=3.0.0",
+ "react": ">=17.0.0",
+ "vue": ">=3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@nuxt/kit": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "vue": {
+ "optional": true
+ }
+ }
+ },
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
diff --git a/package.json b/package.json
index 80e460a23..1ceeea3c1 100644
--- a/package.json
+++ b/package.json
@@ -104,6 +104,7 @@
"date-fns-tz": "^3.2.0",
"framer-motion": "^11.15.0",
"iso-639-1": "^3.1.5",
+ "lenis": "^1.3.26",
"libsodium-wrappers": "^0.8.2",
"lucide-react": "^0.563.0",
"next": "^15.5.0",
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 611c3dbd8..9fad6fbb7 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -154,6 +154,16 @@ const config: Config = {
"0 2px 4px -2px hsl(var(--shadow-color) / 0.1), 0 4px 8px -2px hsl(var(--shadow-color) / 0.06)",
"elevation-3":
"0 8px 16px -4px hsl(var(--shadow-color) / 0.12), 0 16px 32px -8px hsl(var(--shadow-color) / 0.08)",
+ /* UI overhaul: layered card shadows that read as physical elevation
+ rather than a flat outline glow (shadow-card / shadow-lift / float). */
+ card:
+ "0 1px 1px hsl(var(--shadow-color) / 0.03), 0 2px 6px -1px hsl(var(--shadow-color) / 0.05), 0 8px 24px -6px hsl(var(--shadow-color) / 0.06)",
+ lift:
+ "0 2px 4px -1px hsl(var(--shadow-color) / 0.05), 0 12px 28px -6px hsl(var(--shadow-color) / 0.1), 0 32px 64px -16px hsl(var(--shadow-color) / 0.1)",
+ },
+ transitionTimingFunction: {
+ // Signature easing of the overhaul — fast start, long glide.
+ "out-expo": "cubic-bezier(0.16, 1, 0.3, 1)",
},
keyframes: {
"accordion-down": {