update why nearle page

This commit is contained in:
2026-07-19 23:07:18 +05:30
parent 4073dc8386
commit 9abd59ec85
51 changed files with 3748 additions and 62 deletions

View File

@@ -1,6 +1,7 @@
"use client";
import React from "react";
import Image from "next/image";
import { motion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, scaleIn, staggerContainer, staggerSlow, viewport } from "@/lib/animations";
@@ -415,12 +416,29 @@ const CARDS = [
export function BusinessFeaturesGrid() {
return (
<section
className="bg-[#FAF7FD] border-b border-[#6330D6]/10 overflow-hidden"
className="relative bg-[#FAF7FD] border-b border-[#6330D6]/10 overflow-hidden"
style={{ paddingTop: "clamp(44px, 4.5vw, 64px)", paddingBottom: "clamp(44px, 4.5vw, 64px)" }}
>
{/* Decorative mascot watermark — left side only, never crosses into the feature cards */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-y-0 left-0 z-0 flex items-center select-none"
style={{ width: "clamp(360px, 34vw, 600px)" }}
>
<div className="relative h-[88%] w-full -translate-x-[2%]">
<Image
src="/superhero.png"
alt=""
fill
sizes="600px"
className="object-contain object-left opacity-[0.12] blur-[1px]"
/>
</div>
</div>
{/* Centered page container */}
<div
className="mx-auto w-[calc(100%-48px)] md:w-[calc(100%-64px)] xl:w-[calc(100%-96px)]"
className="relative z-10 mx-auto w-[calc(100%-48px)] md:w-[calc(100%-64px)] xl:w-[calc(100%-96px)]"
style={{ maxWidth: 1800 }}
>
{/* Main two-column grid */}
@@ -439,7 +457,7 @@ export function BusinessFeaturesGrid() {
{/* Badge + Heading */}
<motion.div variants={fadeUp}>
<span className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]">
Our features
MerchantOS
</span>
<h2
className="font-display font-black text-[#16001D] mt-4"

View File

@@ -8,8 +8,8 @@ import { fadeUp, fadeIn, scaleIn, staggerContainer, viewport } from "@/lib/anima
const FEATURES = [
{
title: "Digital Catalogue",
desc: "Showcase your products online and keep your catalogue updated.",
title: "AI Catalogue",
desc: "Turn your shelf into a live online catalogue with ProductLLM — no manual data entry.",
icon: "menu_book",
visual: "/images/nearle-merchant/catalogue-ui.webp",
alt: "Clean grocery catalogue interface preview",

View File

@@ -82,19 +82,16 @@ export function StoreQRSection() {
target="_blank"
rel="noopener noreferrer"
aria-label="Download Nearle on the App Store"
className="h-[60px] border border-white/20 rounded-2xl bg-white/10 text-white/60 px-8 py-2 flex items-center gap-3.5 shrink-0 cursor-pointer select-none hover:bg-white/[0.16] transition-all duration-300 active:scale-[0.98]"
className="h-[60px] flex items-center shrink-0 cursor-pointer select-none transition-all duration-300 hover:scale-[1.03] active:scale-[0.98]"
>
<svg viewBox="0 0 24 24" className="w-7 h-7 shrink-0 fill-current opacity-60" fill="currentColor">
<path d="M17.537 12.625c-.023-2.949 2.398-4.37 2.506-4.44-1.364-2.008-3.48-2.282-4.227-2.307-1.79-.186-3.504 1.064-4.413 1.064-.918 0-2.323-1.04-3.827-1.01-1.963.028-3.788 1.146-4.796 2.91-2.06 3.587-.526 8.9 1.466 11.808.98 1.428 2.147 3.035 3.685 2.975 1.47-.059 2.03-.964 3.807-.964 1.767 0 2.266.964 3.818.932 1.58-.03 2.58-1.453 3.536-2.89 1.116-1.628 1.57-3.21 1.596-3.29-.035-.016-3.057-1.18-3.087-4.678l.036-.002zM14.87 4.058c.821-.99 1.375-2.361 1.223-3.733-1.184.05-2.618.784-3.47 1.77-.762.875-1.43 2.28-1.25 3.623 1.327.102 2.673-.668 3.497-1.66z" />
</svg>
<div className="text-left leading-tight">
<div className="text-[10px] font-bold tracking-wider uppercase opacity-60">
App Store
</div>
<div className="text-sm font-black">
Available on
</div>
</div>
<Image
src="/apple-download.svg"
alt="Download Nearle on the App Store"
width={144}
height={60}
className="h-[60px] w-auto object-contain"
priority
/>
</a>
<a
@@ -102,19 +99,15 @@ export function StoreQRSection() {
target="_blank"
rel="noopener noreferrer"
aria-label="Download Nearle on Google Play"
className="h-[60px] border border-white/20 rounded-2xl bg-white/10 text-white/60 px-8 py-2 flex items-center gap-3.5 shrink-0 cursor-pointer select-none hover:bg-white/[0.16] transition-all duration-300 active:scale-[0.98]"
className="h-[60px] flex items-center shrink-0 cursor-pointer select-none transition-all duration-300 hover:scale-[1.03] active:scale-[0.98]"
>
<svg viewBox="0 0 24 24" className="w-7 h-7 shrink-0 fill-current opacity-60" fill="currentColor">
<path d="M3.609 1.814L13.414 12 3.609 22.186c-.244-.178-.395-.466-.395-.786V2.6c0-.32.151-.608.395-.786zM14.244 12.83l2.705 2.688-4.073 2.276-4.073 2.276L14.244 12.83zm1-1.66l2.838-2.8 4.153 2.336c.464.26.464.688 0 .948l-4.153 2.336-2.838-2.82zM8.803 4.93l4.073 2.276 4.073 2.276-2.705 2.688L8.803 4.93z" />
</svg>
<div className="text-left leading-tight">
<div className="text-[10px] font-bold tracking-wider uppercase opacity-60">
Google Play
</div>
<div className="text-sm font-black">
Available on
</div>
</div>
<Image
src="/play-download.svg"
alt="Download Nearle on Google Play"
width={155}
height={60}
className="h-[60px] w-auto object-contain"
/>
</a>
</div>
</motion.div>
@@ -264,19 +257,15 @@ export function StoreQRSection() {
target="_blank"
rel="noopener noreferrer"
aria-label="Download Nearle on the App Store"
className="h-14 w-60 sm:w-auto border border-white/20 rounded-2xl bg-white/10 text-white/60 px-6 py-2 flex items-center justify-center gap-3 cursor-pointer select-none hover:bg-white/[0.16] transition-all duration-300 active:scale-[0.98]"
className="h-14 flex items-center shrink-0 cursor-pointer select-none transition-all duration-300 hover:scale-[1.03] active:scale-[0.98]"
>
<svg viewBox="0 0 24 24" className="w-6 h-6 shrink-0 fill-current opacity-60" fill="currentColor">
<path d="M17.537 12.625c-.023-2.949 2.398-4.37 2.506-4.44-1.364-2.008-3.48-2.282-4.227-2.307-1.79-.186-3.504 1.064-4.413 1.064-.918 0-2.323-1.04-3.827-1.01-1.963.028-3.788 1.146-4.796 2.91-2.06 3.587-.526 8.9 1.466 11.808.98 1.428 2.147 3.035 3.685 2.975 1.47-.059 2.03-.964 3.807-.964 1.767 0 2.266.964 3.818.932 1.58-.03 2.58-1.453 3.536-2.89 1.116-1.628 1.57-3.21 1.596-3.29-.035-.016-3.057-1.18-3.087-4.678l.036-.002zM14.87 4.058c.821-.99 1.375-2.361 1.223-3.733-1.184.05-2.618.784-3.47 1.77-.762.875-1.43 2.28-1.25 3.623 1.327.102 2.673-.668 3.497-1.66z" />
</svg>
<div className="text-left leading-tight">
<div className="text-[10px] font-bold tracking-wider uppercase opacity-60">
App Store
</div>
<div className="text-sm font-black">
Available on
</div>
</div>
<Image
src="/apple-download.svg"
alt="Download Nearle on the App Store"
width={134}
height={56}
className="h-14 w-auto object-contain"
/>
</a>
<a
@@ -284,19 +273,15 @@ export function StoreQRSection() {
target="_blank"
rel="noopener noreferrer"
aria-label="Download Nearle on Google Play"
className="h-14 w-60 sm:w-auto border border-white/20 rounded-2xl bg-white/10 text-white/60 px-6 py-2 flex items-center justify-center gap-3 cursor-pointer select-none hover:bg-white/[0.16] transition-all duration-300 active:scale-[0.98]"
className="h-14 flex items-center shrink-0 cursor-pointer select-none transition-all duration-300 hover:scale-[1.03] active:scale-[0.98]"
>
<svg viewBox="0 0 24 24" className="w-6 h-6 shrink-0 fill-current opacity-60" fill="currentColor">
<path d="M3.609 1.814L13.414 12 3.609 22.186c-.244-.178-.395-.466-.395-.786V2.6c0-.32.151-.608.395-.786zM14.244 12.83l2.705 2.688-4.073 2.276-4.073 2.276L14.244 12.83zm1-1.66l2.838-2.8 4.153 2.336c.464.26.464.688 0 .948l-4.153 2.336-2.838-2.82zM8.803 4.93l4.073 2.276 4.073 2.276-2.705 2.688L8.803 4.93z" />
</svg>
<div className="text-left leading-tight">
<div className="text-[10px] font-bold tracking-wider uppercase opacity-60">
Google Play
</div>
<div className="text-sm font-black">
Available on
</div>
</div>
<Image
src="/play-download.svg"
alt="Download Nearle on Google Play"
width={145}
height={56}
className="h-14 w-auto object-contain"
/>
</a>
</div>
</div>

View File

@@ -0,0 +1,102 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, fadeIn, staggerContainer, viewport } from "@/lib/animations";
const CARDS = [
{
title: "Neighbourhood Trust",
desc: "Every order is fulfilled by a real local shop your street already knows — not an anonymous warehouse.",
icon: "handshake",
color: "bg-primary/5 text-primary border-primary/10",
},
{
title: "AI-Powered Catalog",
desc: "ProductLLM turns a shelf into a live, shoppable catalog in minutes — so any store can go online without the busywork.",
icon: "auto_awesome",
color: "bg-secondary/5 text-secondary border-secondary/10",
},
{
title: "Offline-First Commerce",
desc: "Billing and inventory keep working even when the network drops. Built for how neighbourhood stores really run.",
icon: "wifi_off",
color: "bg-[#de96f9]/10 text-primary border-[#de96f9]/20",
},
];
export function WhyAICommerceWins() {
return (
<section className="section bg-white w-full overflow-hidden">
<div className="page-container relative">
<motion.div
className="relative text-center mb-8 md:mb-10"
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={viewport}
>
<span className="text-xs text-primary uppercase tracking-widest font-black bg-primary/5 px-3.5 py-1.5 rounded-full">
AI Commerce
</span>
<h2 className="text-4xl md:text-5xl lg:text-[54px] font-display font-black text-brand-dark mt-3 leading-tight tracking-tight">
Why AI Commerce <span className="text-primary font-normal">Wins</span>
</h2>
<p className="text-body-lg text-on-surface-variant mt-2.5 max-w-xl mx-auto leading-relaxed">
Nearle isn&rsquo;t another delivery app. It&rsquo;s the AI layer that brings
neighbourhood stores online without replacing them.
</p>
</motion.div>
<motion.div
className="grid grid-cols-1 md:grid-cols-3 gap-5 lg:gap-7"
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
>
{CARDS.map((card) => (
<motion.div
key={card.title}
variants={fadeIn}
className="premium-card bg-white rounded-3xl p-6 md:p-7 border border-outline-variant/15 shadow-lg flex flex-col h-full"
whileHover={{ y: -4, scale: 1.02, transition: { duration: 0.22, ease: [0.22, 1, 0.36, 1] } }}
whileTap={{ scale: 0.98, transition: { duration: 0.1 } }}
>
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center border mb-5 shrink-0 ${card.color}`}>
<MaterialIcon icon={card.icon} size={27} />
</div>
<p className="font-display text-xl font-black text-brand-dark mb-3">
{card.title}
</p>
<p className="text-sm md:text-base text-on-surface-variant leading-relaxed">
{card.desc}
</p>
</motion.div>
))}
</motion.div>
<motion.div
className="mt-9 flex justify-center"
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={viewport}
>
<Link
href="/why-nearle"
className="group inline-flex items-center gap-2 text-base font-black text-primary transition-colors hover:text-[#4C1D95]"
>
Learn Why Nearle
<MaterialIcon
icon="arrow_forward"
size={20}
className="transition-transform duration-200 group-hover:translate-x-1"
/>
</Link>
</motion.div>
</div>
</section>
);
}

View File

@@ -2,7 +2,7 @@
import Link from "next/link";
import Image from "next/image";
import { useState } from "react";
import { useState, useRef } from "react";
import { usePathname } from "next/navigation";
import {
motion,
@@ -14,11 +14,20 @@ import { MaterialIcon } from "@/components/ui/MaterialIcon";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const NAV_LINKS = [
type NavChild = { label: string; href: string };
type NavLink = { label: string; href: string; children?: NavChild[] };
const SOLUTIONS_CHILDREN: NavChild[] = [
{ label: "For Business", href: "/businesses" },
{ label: "For People", href: "/people" },
{ label: "For D2C Brands", href: "/brands" },
];
const NAV_LINKS: NavLink[] = [
{ label: "Home", href: "/" },
{ label: "Nearle for Business", href: "/businesses" },
{ label: "Nearle for People", href: "/people" },
{ label: "Nearle for D2C Brands", href: "/brands" },
{ label: "Why Nearle", href: "/why-nearle" },
{ label: "Platform", href: "/platform" },
{ label: "Solutions", href: "/businesses", children: SOLUTIONS_CHILDREN },
{ label: "About", href: "/about" },
{ label: "Contact", href: "/contact" },
];
@@ -47,11 +56,44 @@ const mobileItemVariants = {
visible: { opacity: 1, x: 0, transition: { duration: 0.22, ease: EASE } },
};
const dropdownVariants = {
hidden: { opacity: 0, y: 8, scale: 0.98 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { duration: 0.22, ease: EASE },
},
exit: {
opacity: 0,
y: 6,
scale: 0.98,
transition: { duration: 0.15, ease: "easeIn" as const },
},
};
const mobileSubmenuVariants = {
hidden: { opacity: 0, height: 0 },
visible: {
opacity: 1,
height: "auto",
transition: { duration: 0.26, ease: EASE },
},
exit: {
opacity: 0,
height: 0,
transition: { duration: 0.18, ease: "easeIn" as const },
},
};
export function PremiumNavbar() {
const [mobileOpen, setMobileOpen] = useState(false);
const [mobileSolutionsOpen, setMobileSolutionsOpen] = useState(false);
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
const [scrolled, setScrolled] = useState(false);
const pathname = usePathname();
const { scrollY } = useScroll();
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
useMotionValueEvent(scrollY, "change", (y) => {
setScrolled(y > 20);
@@ -62,6 +104,28 @@ export function PremiumNavbar() {
return pathname.startsWith(href);
};
const isGroupActive = (link: NavLink) => {
if (link.children) {
return link.children.some((c) => isActiveLink(c.href));
}
return isActiveLink(link.href);
};
const openMenu = (label: string) => {
if (closeTimer.current) clearTimeout(closeTimer.current);
setOpenDropdown(label);
};
const scheduleClose = () => {
if (closeTimer.current) clearTimeout(closeTimer.current);
closeTimer.current = setTimeout(() => setOpenDropdown(null), 120);
};
const closeMobile = () => {
setMobileOpen(false);
setMobileSolutionsOpen(false);
};
return (
<motion.header
className={`sticky top-0 z-50 w-full text-white transition-[background-color,box-shadow,backdrop-filter] duration-300 ${
@@ -91,9 +155,82 @@ export function PremiumNavbar() {
</Link>
{/* Desktop Navigation */}
<nav className="hidden flex-1 items-center justify-center gap-4 lg:flex min-[1440px]:gap-10">
<nav className="hidden flex-1 items-center justify-center gap-4 lg:flex min-[1440px]:gap-9">
{NAV_LINKS.map((link) => {
const isActive = isActiveLink(link.href);
const isActive = isGroupActive(link);
if (link.children) {
const isOpen = openDropdown === link.label;
return (
<div
key={link.label}
className="relative py-2"
onMouseEnter={() => openMenu(link.label)}
onMouseLeave={scheduleClose}
>
<button
type="button"
className={`flex items-center gap-1 whitespace-nowrap text-sm font-bold transition-colors duration-200 ${
isActive ? "text-white" : "text-white/70 hover:text-white"
}`}
aria-haspopup="true"
aria-expanded={isOpen}
onClick={() => setOpenDropdown(isOpen ? null : link.label)}
onFocus={() => openMenu(link.label)}
>
{link.label}
<motion.span
className="flex"
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.2, ease: EASE }}
>
<MaterialIcon icon="expand_more" size={16} />
</motion.span>
</button>
{isActive && (
<motion.span
layoutId="activeNavUnderline"
className="absolute bottom-0 left-0 right-0 h-0.5 rounded-full bg-white"
transition={{ duration: 0.3, ease: EASE }}
/>
)}
<AnimatePresence>
{isOpen && (
<motion.div
variants={dropdownVariants}
initial="hidden"
animate="visible"
exit="exit"
className="absolute left-1/2 top-full z-50 mt-2 w-56 -translate-x-1/2 overflow-hidden rounded-2xl border border-white/12 bg-[#30226F]/98 p-1.5 shadow-[0_18px_45px_rgba(20,0,30,0.45)] backdrop-blur-xl"
role="menu"
>
{link.children.map((child) => {
const childActive = isActiveLink(child.href);
return (
<Link
key={child.href}
href={child.href}
role="menuitem"
onClick={() => setOpenDropdown(null)}
className={`block rounded-xl px-4 py-2.5 text-sm font-bold transition-all duration-200 ${
childActive
? "bg-white/15 text-white"
: "text-white/75 hover:bg-white/10 hover:text-white"
}`}
>
{child.label}
</Link>
);
})}
</motion.div>
)}
</AnimatePresence>
</div>
);
}
return (
<motion.div
key={link.label}
@@ -164,12 +301,72 @@ export function PremiumNavbar() {
animate="visible"
>
{NAV_LINKS.map((link) => {
if (link.children) {
const groupActive = isGroupActive(link);
return (
<motion.div key={link.label} variants={mobileItemVariants}>
<button
type="button"
onClick={() => setMobileSolutionsOpen((v) => !v)}
aria-expanded={mobileSolutionsOpen}
className={`flex w-full items-center justify-between rounded-xl px-4 py-3 text-sm font-bold transition-all duration-200 ${
groupActive
? "bg-white/15 text-white"
: "text-white/70 hover:bg-white/10 hover:text-white"
}`}
>
{link.label}
<motion.span
className="flex"
animate={{ rotate: mobileSolutionsOpen ? 180 : 0 }}
transition={{ duration: 0.2, ease: EASE }}
>
<MaterialIcon icon="expand_more" size={20} />
</motion.span>
</button>
<AnimatePresence initial={false}>
{mobileSolutionsOpen && (
<motion.div
variants={mobileSubmenuVariants}
initial="hidden"
animate="visible"
exit="exit"
className="overflow-hidden"
>
<div className="mt-1 flex flex-col gap-1 pl-3">
{link.children.map((child) => {
const childActive = isActiveLink(child.href);
return (
<Link
key={child.href}
href={child.href}
onClick={closeMobile}
className={`flex items-center gap-2 rounded-xl px-4 py-2.5 text-sm font-bold transition-all duration-200 ${
childActive
? "bg-white/12 text-white"
: "text-white/60 hover:bg-white/10 hover:text-white"
}`}
>
<span className="h-1 w-1 rounded-full bg-white/40" />
{child.label}
</Link>
);
})}
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
}
const isActive = isActiveLink(link.href);
return (
<motion.div key={link.label} variants={mobileItemVariants}>
<Link
href={link.href}
onClick={() => setMobileOpen(false)}
onClick={closeMobile}
className={`flex items-center justify-between rounded-xl px-4 py-3 text-sm font-bold transition-all duration-200 ${
isActive
? "bg-white/15 text-white"

View File

@@ -0,0 +1,78 @@
"use client";
import { useEffect, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { PhoneMockup } from "@/components/platform/DeviceShowcase";
/**
* ConsumerOS showcase — a premium, sequential product demo.
*
* Only ONE illustration is ever visible at a time:
* 1. The ConsumerOS phone mockup, held completely static for PHONE_DURATION.
* 2. A premium fade + horizontal-slide transition (TRANSITION_DURATION).
* 3. The Food.svg artwork, held static so its OWN internal SVG animation can
* play through uninterrupted. The wrapper never floats or scales.
* 4. The same transition back to the phone. Repeats infinitely.
*
* The PhoneMockup artwork and the Food.svg artwork are used untouched.
*/
// Per-illustration display durations (configurable).
const PHONE_DURATION = 6000; // ConsumerOS held static
// Food.svg's internal SMIL animation is a continuous loop of dur="13.467s"
// (repeatCount="indefinite"). We hold it for exactly one full loop so the art
// returns to its opening frame before we transition — never cut mid-animation.
const FOOD_DURATION = 13467;
const TRANSITION_DURATION = 700; // premium fade + slide, 600800ms
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
function FoodArt() {
return (
// Re-mounts on each cycle (keyed AnimatePresence) so the SVG's internal
// animation restarts and plays from the beginning every time it appears.
// eslint-disable-next-line @next/next/no-img-element
<img
src="/Food.svg"
alt=""
aria-hidden="true"
className="pointer-events-none h-auto w-full max-w-[240px] select-none"
/>
);
}
function PhoneArt() {
return (
<div className="w-full max-w-[220px]">
<PhoneMockup />
</div>
);
}
export function ConsumerShowcase() {
const reduce = useReducedMotion();
const [index, setIndex] = useState(0); // 0 = ConsumerOS phone, 1 = Food.svg
useEffect(() => {
if (reduce) return; // Reduced motion: hold the phone, never cycle.
const delay = index === 0 ? PHONE_DURATION : FOOD_DURATION;
const id = setTimeout(() => setIndex((i) => (i === 0 ? 1 : 0)), delay);
return () => clearTimeout(id);
}, [index, reduce]);
return (
<div className="relative flex h-[300px] w-full items-center justify-center overflow-hidden sm:h-[320px] md:h-[360px] lg:h-[400px]">
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={index}
initial={{ opacity: 0, x: 28 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -28 }}
transition={{ duration: TRANSITION_DURATION / 1000, ease: EASE }}
className="flex w-full items-center justify-center"
>
{index === 0 ? <PhoneArt /> : <FoodArt />}
</motion.div>
</AnimatePresence>
</div>
);
}

View File

@@ -0,0 +1,297 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
/**
* Large animated device illustrations used to replace the flat feature grids.
* - DashboardMockup → MerchantOS (a live store dashboard)
* - PhoneMockup → ConsumerOS (the neighbourhood shopping app)
*
* Purely decorative. GPU-friendly (opacity / transform / pathLength only)
* and fully reduced-motion aware.
*/
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const rise = {
hidden: { opacity: 0, y: 16 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: EASE } },
};
const pop = {
hidden: { opacity: 0, scale: 0.9 },
visible: { opacity: 1, scale: 1, transition: { duration: 0.5, ease: EASE } },
};
const draw = {
hidden: { pathLength: 0, opacity: 0 },
visible: { pathLength: 1, opacity: 1, transition: { duration: 1.5, ease: EASE } },
};
const growBar = {
hidden: { scaleY: 0 },
visible: { scaleY: 1, transition: { duration: 0.7, ease: EASE } },
};
const svgContainer = {
hidden: {},
visible: { transition: { staggerChildren: 0.08, delayChildren: 0.05 } },
};
// ─── MerchantOS — store dashboard ────────────────────────────────────────────
export function DashboardMockup() {
const reduce = useReducedMotion();
const floatA = reduce ? {} : { y: [0, -7, 0] };
const floatB = reduce ? {} : { y: [0, 6, 0] };
return (
<motion.svg
viewBox="0 0 440 336"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="w-full h-auto"
role="img"
aria-label="MerchantOS store dashboard preview"
variants={svgContainer}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.35 }}
>
{/* Soft ground shadow */}
<ellipse cx="220" cy="312" rx="150" ry="14" fill="#6330D6" opacity="0.08" />
{/* Main dashboard window */}
<motion.g variants={rise}>
<rect x="40" y="30" width="360" height="256" rx="22" fill="#FFFFFF" stroke="#EDE9FE" strokeWidth="1.5" />
{/* Top chrome */}
<circle cx="66" cy="54" r="4" fill="#E6D9F7" />
<circle cx="80" cy="54" r="4" fill="#E6D9F7" />
<circle cx="94" cy="54" r="4" fill="#E6D9F7" />
<rect x="150" y="48" width="140" height="13" rx="6.5" fill="#F3E8F6" />
<line x1="40" y1="72" x2="400" y2="72" stroke="#F0E9F8" strokeWidth="1.5" />
</motion.g>
{/* Sidebar */}
<motion.g variants={rise}>
<rect x="52" y="82" width="58" height="192" rx="14" fill="#FAF7FD" />
<rect x="64" y="98" width="34" height="8" rx="4" fill="#6330D6" />
<rect x="64" y="120" width="30" height="7" rx="3.5" fill="#D9CBEF" />
<rect x="64" y="138" width="34" height="7" rx="3.5" fill="#D9CBEF" />
<rect x="64" y="156" width="26" height="7" rx="3.5" fill="#D9CBEF" />
<rect x="64" y="174" width="32" height="7" rx="3.5" fill="#D9CBEF" />
</motion.g>
{/* KPI chips */}
<motion.g variants={pop}>
<rect x="124" y="84" width="80" height="46" rx="12" fill="#F5F1FB" />
<rect x="136" y="96" width="30" height="7" rx="3.5" fill="#B9A6E0" />
<rect x="136" y="110" width="46" height="10" rx="5" fill="#6330D6" />
</motion.g>
<motion.g variants={pop}>
<rect x="212" y="84" width="80" height="46" rx="12" fill="#ECFBF1" />
<rect x="224" y="96" width="30" height="7" rx="3.5" fill="#86E0A6" />
<rect x="224" y="110" width="40" height="10" rx="5" fill="#16A34A" />
</motion.g>
<motion.g variants={pop}>
<rect x="300" y="84" width="80" height="46" rx="12" fill="#FFF7E8" />
<rect x="312" y="96" width="30" height="7" rx="3.5" fill="#F1C876" />
<rect x="312" y="110" width="34" height="10" rx="5" fill="#D97706" />
</motion.g>
{/* Chart panel */}
<motion.g variants={rise}>
<rect x="124" y="142" width="168" height="120" rx="14" fill="#FCFAFF" stroke="#F0E9F8" strokeWidth="1.2" />
<rect x="138" y="156" width="44" height="8" rx="4" fill="#C9B8EC" />
{/* gridlines */}
<line x1="138" y1="196" x2="278" y2="196" stroke="#EFE7FB" strokeWidth="1" />
<line x1="138" y1="222" x2="278" y2="222" stroke="#EFE7FB" strokeWidth="1" />
{/* Area + animated line */}
<motion.path
d="M140 236 L168 218 L196 224 L224 196 L252 204 L278 176 L278 248 L140 248 Z"
fill="url(#dashArea)"
variants={{ hidden: { opacity: 0 }, visible: { opacity: 1, transition: { duration: 1, delay: 0.5 } } }}
/>
<motion.path
d="M140 236 L168 218 L196 224 L224 196 L252 204 L278 176"
fill="none"
stroke="#6330D6"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
variants={draw}
/>
<motion.circle cx="278" cy="176" r="4" fill="#6330D6" stroke="#fff" strokeWidth="1.5" variants={pop} />
</motion.g>
{/* Mini bar chart */}
<motion.g variants={rise}>
<rect x="300" y="142" width="80" height="120" rx="14" fill="#FCFAFF" stroke="#F0E9F8" strokeWidth="1.2" />
{[
{ x: 314, h: 34, c: "#DDD0F4" },
{ x: 332, h: 52, c: "#B99BEC" },
{ x: 350, h: 44, c: "#9B78E4" },
{ x: 368, h: 66, c: "#6330D6" },
].map((b) => (
<motion.rect
key={b.x}
x={b.x}
y={250 - b.h}
width="11"
height={b.h}
rx="4"
fill={b.c}
variants={growBar}
style={{ transformBox: "fill-box", transformOrigin: "bottom" }}
/>
))}
</motion.g>
{/* Floating "new order" card */}
<motion.g animate={floatA} transition={{ repeat: Infinity, duration: 3.6, ease: "easeInOut" }}>
<motion.g variants={pop}>
<rect x="316" y="24" width="104" height="42" rx="13" fill="#16001D" />
<circle cx="336" cy="45" r="10" fill="#7C3AED" />
<path d="M332 45 l3 3 l6 -6.5" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none" />
<rect x="352" y="38" width="52" height="6" rx="3" fill="#ffffff" opacity="0.9" />
<rect x="352" y="49" width="34" height="5" rx="2.5" fill="#C9A2FF" />
</motion.g>
</motion.g>
{/* Floating live-stock pill */}
<motion.g animate={floatB} transition={{ repeat: Infinity, duration: 4.2, ease: "easeInOut" }}>
<motion.g variants={pop}>
<rect x="22" y="196" width="96" height="38" rx="12" fill="#FFFFFF" stroke="#EDE9FE" strokeWidth="1.5" />
<motion.circle
cx="42" cy="215" r="6" fill="#22C55E"
animate={reduce ? {} : { opacity: [1, 0.4, 1], scale: [1, 1.15, 1] }}
transition={{ repeat: Infinity, duration: 1.8, ease: "easeInOut" }}
/>
<rect x="56" y="208" width="48" height="6" rx="3" fill="#16001D" opacity="0.75" />
<rect x="56" y="219" width="30" height="5" rx="2.5" fill="#B9A6E0" />
</motion.g>
</motion.g>
<defs>
<linearGradient id="dashArea" x1="0" y1="176" x2="0" y2="248" gradientUnits="userSpaceOnUse">
<stop stopColor="#6330D6" stopOpacity="0.22" />
<stop offset="1" stopColor="#6330D6" stopOpacity="0" />
</linearGradient>
</defs>
</motion.svg>
);
}
// ─── ConsumerOS — neighbourhood shopping app ─────────────────────────────────
export function PhoneMockup() {
const reduce = useReducedMotion();
return (
<motion.svg
viewBox="0 0 360 336"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="w-full h-auto"
role="img"
aria-label="ConsumerOS neighbourhood shopping app preview"
variants={svgContainer}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.35 }}
>
<ellipse cx="180" cy="316" rx="120" ry="13" fill="#6330D6" opacity="0.08" />
{/* Phone body */}
<motion.g variants={rise}>
<rect x="118" y="14" width="150" height="306" rx="34" fill="#16001D" />
<rect x="126" y="22" width="134" height="290" rx="28" fill="#FBF9FE" />
{/* notch */}
<rect x="168" y="30" width="50" height="10" rx="5" fill="#16001D" />
</motion.g>
{/* Search bar */}
<motion.g variants={pop}>
<rect x="140" y="52" width="106" height="26" rx="13" fill="#FFFFFF" stroke="#EDE9FE" strokeWidth="1.4" />
<circle cx="156" cy="65" r="5" fill="none" stroke="#6330D6" strokeWidth="1.8" />
<line x1="160" y1="69" x2="164" y2="73" stroke="#6330D6" strokeWidth="1.8" strokeLinecap="round" />
<rect x="170" y="62" width="60" height="6" rx="3" fill="#D9CBEF" />
</motion.g>
{/* Map region with roads */}
<motion.g variants={rise}>
<rect x="140" y="86" width="106" height="88" rx="14" fill="#EEF4FF" />
<path d="M140 128 H246" stroke="#FFFFFF" strokeWidth="5" />
<path d="M193 86 V174" stroke="#FFFFFF" strokeWidth="5" />
<path d="M140 104 Q168 104 168 128" stroke="#FFFFFF" strokeWidth="3.5" fill="none" />
<path d="M218 128 Q218 152 246 156" stroke="#FFFFFF" strokeWidth="3.5" fill="none" />
{/* animated route */}
<motion.path
d="M162 158 L162 128 L193 128 L193 108 L224 108"
fill="none"
stroke="#6330D6"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
strokeDasharray="4 5"
variants={draw}
/>
</motion.g>
{/* Floating map pins */}
<motion.g
animate={reduce ? {} : { y: [0, -6, 0] }}
transition={{ repeat: Infinity, duration: 3.4, ease: "easeInOut" }}
>
<motion.g variants={pop}>
<path d="M162 150 C157 150 153 146 153 141 C153 135 162 126 162 126 C162 126 171 135 171 141 C171 146 167 150 162 150 Z" fill="#6330D6" />
<circle cx="162" cy="140" r="3.4" fill="#fff" />
</motion.g>
</motion.g>
<motion.g
animate={reduce ? {} : { y: [0, -6, 0] }}
transition={{ repeat: Infinity, duration: 3.9, ease: "easeInOut", delay: 0.4 }}
>
<motion.g variants={pop}>
<path d="M224 122 C219 122 215 118 215 113 C215 107 224 98 224 98 C224 98 233 107 233 113 C233 118 229 122 224 122 Z" fill="#22C55E" />
<circle cx="224" cy="112" r="3.4" fill="#fff" />
</motion.g>
</motion.g>
{/* Product cards */}
{[0, 1].map((i) => (
<motion.g key={i} variants={pop}>
<rect x="140" y={184 + i * 40} width="106" height="34" rx="11" fill="#FFFFFF" stroke="#EDE9FE" strokeWidth="1.3" />
<rect x="148" y={190 + i * 40} width="22" height="22" rx="7" fill={i === 0 ? "#EDE9FE" : "#DCFCE7"} />
<rect x="178" y={192 + i * 40} width="42" height="6" rx="3" fill="#16001D" opacity="0.72" />
<rect x="178" y={202 + i * 40} width="28" height="5" rx="2.5" fill="#C1B2E4" />
<circle cx="234" cy={201 + i * 40} r="8" fill={i === 0 ? "#6330D6" : "#22C55E"} />
<path d={`M230 ${201 + i * 40} h8 M234 ${197 + i * 40} v8`} stroke="#fff" strokeWidth="1.6" strokeLinecap="round" />
</motion.g>
))}
{/* Bottom nav */}
<motion.g variants={pop}>
<rect x="140" y="272" width="106" height="30" rx="12" fill="#FFFFFF" stroke="#F0E9F8" strokeWidth="1.2" />
<circle cx="160" cy="287" r="4.5" fill="#6330D6" />
<circle cx="184" cy="287" r="4" fill="#D9CBEF" />
<circle cx="208" cy="287" r="4" fill="#D9CBEF" />
<circle cx="230" cy="287" r="4" fill="#D9CBEF" />
</motion.g>
{/* Floating rewards chip */}
<motion.g
animate={reduce ? {} : { y: [0, 7, 0] }}
transition={{ repeat: Infinity, duration: 4, ease: "easeInOut" }}
>
<motion.g variants={pop}>
<rect x="250" y="196" width="96" height="40" rx="13" fill="#16001D" />
<circle cx="270" cy="216" r="10" fill="#F59E0B" />
<path d="M270 211 l1.6 3.3 l3.6 0.5 l-2.6 2.5 l0.6 3.6 l-3.2 -1.7 l-3.2 1.7 l0.6 -3.6 l-2.6 -2.5 l3.6 -0.5 Z" fill="#fff" />
<rect x="286" y="209" width="50" height="6" rx="3" fill="#ffffff" opacity="0.9" />
<rect x="286" y="220" width="32" height="5" rx="2.5" fill="#F5C97B" />
</motion.g>
</motion.g>
</motion.svg>
);
}

View File

@@ -0,0 +1,156 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, scaleInSpring, staggerContainer, staggerSlow, viewport } from "@/lib/animations";
const LAYERS = [
{
name: "MerchantOS",
tag: "For the store",
icon: "storefront",
desc: "Billing, inventory, catalog, and orders — the operating system a neighborhood shop runs on.",
accent: "from-[#6330D6] to-[#7C3AED]",
},
{
name: "ProductLLM",
tag: "The intelligence",
icon: "auto_awesome",
desc: "Nearle's product AI. It reads a shelf and turns it into an accurate, live digital catalog.",
accent: "from-[#8B5CF6] to-[#A855F7]",
highlight: true,
},
{
name: "ConsumerOS",
tag: "For the shopper",
icon: "smartphone",
desc: "Discovery, search, ordering, and rewards — how the neighborhood finds and shops nearby stores.",
accent: "from-[#6330D6] to-[#4C1D95]",
},
];
/** A gradient link with a pulse of light traveling between two layers. */
function FlowConnector() {
const reduce = useReducedMotion();
return (
<motion.div
variants={fadeUp}
aria-hidden="true"
className="flex shrink-0 items-center justify-center py-1 lg:px-2"
>
{/* mobile — vertical */}
<div className="relative h-9 w-0.5 lg:hidden">
<div className="absolute inset-0 rounded-full bg-gradient-to-b from-[#6330D6]/45 to-[#A855F7]/45" />
{!reduce && (
<motion.span
className="absolute left-1/2 top-0 h-2 w-2 -translate-x-1/2 rounded-full bg-[#7C3AED] shadow-[0_0_10px_2px_rgba(124,58,237,0.6)]"
animate={{ y: [0, 30], opacity: [0, 1, 0] }}
transition={{ repeat: Infinity, duration: 1.7, ease: "easeInOut" }}
/>
)}
</div>
{/* desktop — horizontal */}
<div className="relative hidden h-0.5 w-12 lg:block">
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-[#6330D6]/45 to-[#A855F7]/45" />
{!reduce && (
<motion.span
className="absolute left-0 top-1/2 h-2 w-2 -translate-y-1/2 rounded-full bg-[#7C3AED] shadow-[0_0_10px_2px_rgba(124,58,237,0.6)]"
animate={{ x: [0, 42], opacity: [0, 1, 0] }}
transition={{ repeat: Infinity, duration: 1.7, ease: "easeInOut" }}
/>
)}
</div>
</motion.div>
);
}
export function PlatformArchitecture() {
const reduce = useReducedMotion();
return (
<section className="section bg-white w-full border-b border-[#6330D6]/10 overflow-hidden">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[720px] mx-auto"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
>
Platform Architecture
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
One connected stack
</motion.h2>
<motion.p
variants={fadeUp}
className="text-[#5E5866] mt-4"
style={{ fontSize: 18, lineHeight: 1.65 }}
>
Every order flows through the same three layers from the shopkeeper&rsquo;s
counter to the shopper&rsquo;s phone.
</motion.p>
</motion.div>
<motion.div
variants={staggerSlow}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="mt-12 flex flex-col items-stretch gap-2 lg:flex-row lg:items-center lg:gap-2"
>
{LAYERS.map((layer, i) => (
<div key={layer.name} className="contents">
<motion.div
variants={scaleInSpring}
className={`premium-card group relative flex-1 overflow-hidden rounded-[28px] border p-7 md:p-8 ${
layer.highlight
? "border-[#6330D6]/25 bg-[linear-gradient(135deg,#ffffff_0%,#faf5ff_55%,#f1e8fb_100%)] shadow-[0_12px_40px_rgba(99,48,214,0.14)]"
: "border-[#6330D6]/10 bg-white shadow-[0_18px_45px_rgba(22,0,29,0.07)]"
}`}
>
{/* animated aura on the intelligence layer */}
{layer.highlight && !reduce && (
<motion.div
aria-hidden="true"
className="pointer-events-none absolute -right-10 -top-10 h-40 w-40 rounded-full"
style={{ background: "radial-gradient(circle, rgba(124,58,237,0.20), transparent 70%)" }}
animate={{ scale: [1, 1.18, 1], opacity: [0.6, 0.9, 0.6] }}
transition={{ repeat: Infinity, duration: 4, ease: "easeInOut" }}
/>
)}
<motion.div
className={`relative flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-br ${layer.accent} text-white shadow-[0_10px_24px_rgba(99,48,214,0.28)]`}
animate={reduce ? {} : { y: [0, -6, 0] }}
transition={{ repeat: Infinity, duration: 3.4 + i * 0.4, ease: "easeInOut" }}
>
<MaterialIcon icon={layer.icon} size={28} />
</motion.div>
<p className="relative mt-5 text-xs font-black uppercase tracking-widest text-[#6330D6]/60">
{layer.tag}
</p>
<p className="relative mt-1 font-display font-black text-[#16001D] text-2xl leading-tight">
{layer.name}
</p>
<p className="relative mt-3 text-[15px] leading-relaxed text-[#5E5866]">
{layer.desc}
</p>
</motion.div>
{i < LAYERS.length - 1 && <FlowConnector />}
</div>
))}
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,134 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import { motion, useReducedMotion } from "framer-motion";
import { HeroLogo } from "@/components/heroes/HeroLogo";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const badgeVariant = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, delay: 0.9, ease: EASE } },
};
const headingVariant = {
hidden: { opacity: 0, y: 24 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 1.0, ease: EASE } },
};
const descVariant = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.55, delay: 1.1, ease: EASE } },
};
const pillsVariant = {
hidden: { opacity: 0, y: 16 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, delay: 1.2, ease: EASE } },
};
const PILLARS = ["MerchantOS", "ProductLLM", "ConsumerOS"];
export function PlatformHero() {
const shouldReduce = useReducedMotion();
return (
<section className="w-full flex flex-col select-none bg-white">
<div className="relative overflow-hidden border-b border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D]">
{/* Ambient background image */}
<motion.div
aria-hidden="true"
className="pointer-events-none absolute inset-0 z-0"
initial={shouldReduce ? undefined : { opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1.4, ease: "easeOut" }}
>
<Image
src="/platform.png"
alt=""
fill
sizes="100vw"
className="absolute inset-0 h-full w-full object-cover object-center"
priority
fetchPriority="high"
/>
{/* Dark gradient overlay to make text pop while keeping the image vibrant */}
<div className="absolute inset-0 bg-gradient-to-b from-[#1A0322]/80 via-black/45 to-[#0A000D]/90" />
</motion.div>
<div className="page-container relative z-10 flex min-h-[520px] items-center justify-center px-6 pt-20 pb-14 sm:px-8 sm:pt-24 md:min-h-[76vh] md:py-16">
<div className="relative z-20 flex w-full max-w-[900px] flex-col items-center text-center px-2 md:px-8">
<HeroLogo />
<motion.span
variants={badgeVariant}
initial="hidden"
animate="visible"
className="inline-flex items-center gap-1.5 px-4 py-1.5 rounded-full text-xs font-bold tracking-widest bg-white text-[#6330D6] shadow-md uppercase mb-6 w-fit"
>
The Nearle Platform
</motion.span>
<motion.h1
variants={headingVariant}
initial="hidden"
animate="visible"
className="font-display text-4xl sm:text-5xl lg:text-[52px] xl:text-[60px] font-extrabold text-white leading-[1.08] tracking-tight [text-shadow:0_4px_32px_rgba(0,0,0,0.5)]"
>
The AI Infrastructure
<br />
Behind Every
<br />
<span className="text-[#C9A2FF]">Neighborhood Store.</span>
</motion.h1>
<motion.p
variants={descVariant}
initial="hidden"
animate="visible"
className="max-w-[660px] text-lg md:text-xl text-white/90 font-medium mt-6 leading-[1.6]"
>
Three systems, one platform. MerchantOS runs the store, ProductLLM
understands the products, and ConsumerOS brings the neighborhood to
shop.
</motion.p>
<motion.div
variants={pillsVariant}
initial="hidden"
animate="visible"
className="mt-9 flex flex-wrap items-center justify-center gap-3"
>
{PILLARS.map((p) => (
<span
key={p}
className="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/[0.06] px-4 py-2 text-sm font-black text-white backdrop-blur-sm"
>
<span className="h-1.5 w-1.5 rounded-full bg-[#C9A2FF]" />
{p}
</span>
))}
</motion.div>
<motion.div
variants={pillsVariant}
initial="hidden"
animate="visible"
className="mt-8"
>
<Link
href="/businesses"
className="group inline-flex items-center gap-2 rounded-full bg-white px-8 py-4 text-base font-black text-[#16001D] shadow-[0_16px_30px_rgba(0,0,0,0.35)] transition-transform duration-200 hover:scale-[1.03] active:scale-95"
>
Bring Your Store Online
<MaterialIcon
icon="arrow_forward"
size={20}
className="transition-transform duration-200 group-hover:translate-x-0.5"
/>
</Link>
</motion.div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,153 @@
"use client";
import { useRef } from "react";
import { motion, useReducedMotion, useScroll, useTransform } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { DashboardMockup } from "@/components/platform/DeviceShowcase";
import { ConsumerShowcase } from "@/components/platform/ConsumerShowcase";
import { fadeUp, staggerContainer, staggerCards, viewport, viewport20 } from "@/lib/animations";
export type ProductFeature = {
icon: string;
title: string;
desc: string;
};
type ProductFeatureGridProps = {
id?: string;
eyebrow: string;
name: string;
tagline: string;
features: ProductFeature[];
variant?: "light" | "lavender";
footnote?: string;
/** Which animated device illustration to pair with the feature list. */
showcase?: "dashboard" | "phone";
};
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const rowVariant = {
hidden: { opacity: 0, y: 18 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: EASE } },
};
export function ProductFeatureGrid({
id,
eyebrow,
name,
tagline,
features,
variant = "light",
footnote,
showcase = "dashboard",
}: ProductFeatureGridProps) {
const bg = variant === "lavender" ? "section-soft" : "bg-white";
// Alternate the media side to give the two sections opposite rhythm.
const mediaRight = variant === "light";
const isPhone = showcase === "phone";
const reduce = useReducedMotion();
const sectionRef = useRef<HTMLElement>(null);
const { scrollYProgress } = useScroll({
target: sectionRef,
offset: ["start end", "end start"],
});
const mediaY = useTransform(scrollYProgress, [0, 1], reduce ? [0, 0] : [42, -42]);
return (
<section
id={id}
ref={sectionRef}
className={`section w-full border-b border-[#6330D6]/10 overflow-hidden ${bg}`}
>
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="max-w-[760px]"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
>
{eyebrow}
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
{name}
</motion.h2>
<motion.p
variants={fadeUp}
className="text-[#5E5866] mt-4"
style={{ fontSize: 18, lineHeight: 1.65 }}
>
{tagline}
</motion.p>
</motion.div>
<div className="mt-12 grid items-center gap-10 lg:grid-cols-[minmax(0,0.92fr)_minmax(0,1.08fr)] lg:gap-16">
{/* ── Animated device illustration (parallax) ── */}
<motion.div
style={{ y: mediaY }}
className={`relative mx-auto w-full ${isPhone ? "max-w-[600px]" : "max-w-[440px]"} ${mediaRight ? "lg:order-2" : "lg:order-1"}`}
>
{/* ambient glow */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 -z-10 blur-2xl"
style={{
background:
"radial-gradient(circle at 50% 40%, rgba(99,48,214,0.16), transparent 62%)",
}}
/>
{isPhone ? <ConsumerShowcase /> : <DashboardMockup />}
</motion.div>
{/* ── Editorial feature list (was a 4-col card grid) ── */}
<motion.ul
variants={staggerCards}
initial="hidden"
whileInView="visible"
viewport={viewport20}
className={`relative ${mediaRight ? "lg:order-1" : "lg:order-2"}`}
>
{/* connection line threading the icons */}
<span
aria-hidden="true"
className="pointer-events-none absolute left-[23px] top-6 bottom-6 w-px bg-gradient-to-b from-[#6330D6]/25 via-[#6330D6]/12 to-transparent"
/>
{features.map((f) => (
<motion.li
key={f.title}
variants={rowVariant}
className="group relative flex items-start gap-4 rounded-2xl px-3 py-3.5 transition-colors duration-200 hover:bg-[#6330D6]/[0.04]"
>
<span className="relative z-10 mt-0.5 flex h-[38px] w-[38px] shrink-0 items-center justify-center rounded-xl bg-[#6330D6]/[0.08] text-[#6330D6] transition-transform duration-200 group-hover:scale-110 group-hover:bg-[#6330D6] group-hover:text-white">
<MaterialIcon icon={f.icon} size={20} />
</span>
<span className="min-w-0">
<span className="block font-display font-black text-[#16001D] text-[17px] leading-tight">
{f.title}
</span>
<span className="mt-1 block text-[14px] leading-relaxed text-[#5E5866]">
{f.desc}
</span>
</span>
</motion.li>
))}
</motion.ul>
</div>
{footnote && (
<p className="mt-8 text-sm font-bold text-[#6330D6]">{footnote}</p>
)}
</div>
</section>
);
}

View File

@@ -0,0 +1,322 @@
"use client";
import { useRef } from "react";
import Image from "next/image";
import { motion, useReducedMotion, useScroll, useTransform } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const STEPS = [
{ icon: "photo_camera", title: "Shelf Scan", desc: "A single photo of the shelf starts everything." },
{ icon: "barcode_reader", title: "Barcode Detection", desc: "Every barcode on the shelf is found and read." },
{ icon: "visibility", title: "Product Recognition", desc: "Each item is matched to a known product." },
{ icon: "category", title: "Category", desc: "Products are sorted into the right aisles." },
{ icon: "nutrition", title: "Nutrition", desc: "Nutritional facts are pulled in automatically." },
{ icon: "science", title: "Ingredients", desc: "Ingredient lists are attached to each product." },
{ icon: "sell", title: "Pricing", desc: "Prices are set and kept in sync with the store." },
{ icon: "inventory_2", title: "Inventory", desc: "Live stock counts flow into the catalog." },
{ icon: "check_circle", title: "Ready", desc: "A complete, shoppable catalog — in minutes." },
];
const connectorVariant = {
hidden: { scaleY: 0 },
visible: { scaleY: 1, transition: { duration: 1.1, ease: EASE } },
};
const stepVariant = {
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0, transition: { duration: 0.5, ease: EASE } },
};
/** A shelf photo being scanned and turned into live catalog cards. */
function ShelfScanSVG() {
const reduce = useReducedMotion();
const detect = {
hidden: { opacity: 0, scale: 0.7 },
visible: (i: number) => ({
opacity: 1,
scale: 1,
transition: { delay: 0.5 + i * 0.18, duration: 0.4, ease: EASE },
}),
};
const cardIn = {
hidden: { opacity: 0, x: 14 },
visible: (i: number) => ({
opacity: 1,
x: 0,
transition: { delay: 1.1 + i * 0.16, duration: 0.5, ease: EASE },
}),
};
return (
<motion.svg
viewBox="0 0 460 300"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="w-full h-auto"
role="img"
aria-label="ProductLLM scanning a shelf photo into a digital catalog"
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.35 }}
>
{/* ── Photo of the shelf ── */}
<motion.g variants={fadeUp}>
<rect x="20" y="40" width="210" height="220" rx="18" fill="#160022" stroke="#3B2357" strokeWidth="1.5" />
<rect x="34" y="54" width="182" height="14" rx="7" fill="#2A1740" />
<circle cx="45" cy="61" r="3" fill="#C9A2FF" />
{/* shelves */}
{[96, 150, 204].map((y) => (
<rect key={y} x="34" y={y} width="182" height="4" rx="2" fill="#3B2357" />
))}
{/* products on shelves */}
{[
{ x: 44, y: 74, w: 20, h: 22, c: "#7C3AED" },
{ x: 70, y: 70, w: 16, h: 26, c: "#A855F7" },
{ x: 92, y: 78, w: 22, h: 18, c: "#6330D6" },
{ x: 120, y: 72, w: 18, h: 24, c: "#8B5CF6" },
{ x: 146, y: 76, w: 24, h: 20, c: "#7C3AED" },
{ x: 178, y: 70, w: 18, h: 26, c: "#A855F7" },
{ x: 46, y: 128, w: 18, h: 22, c: "#6330D6" },
{ x: 72, y: 124, w: 22, h: 26, c: "#8B5CF6" },
{ x: 102, y: 130, w: 18, h: 20, c: "#A855F7" },
{ x: 128, y: 122, w: 20, h: 28, c: "#7C3AED" },
{ x: 156, y: 126, w: 22, h: 24, c: "#6330D6" },
{ x: 184, y: 128, w: 16, h: 22, c: "#8B5CF6" },
{ x: 50, y: 182, w: 22, h: 22, c: "#8B5CF6" },
{ x: 80, y: 178, w: 18, h: 26, c: "#7C3AED" },
{ x: 106, y: 184, w: 22, h: 20, c: "#6330D6" },
{ x: 138, y: 180, w: 18, h: 24, c: "#A855F7" },
{ x: 166, y: 184, w: 24, h: 20, c: "#7C3AED" },
].map((p, i) => (
<rect key={i} x={p.x} y={p.y} width={p.w} height={p.h} rx="3" fill={p.c} opacity="0.9" />
))}
{/* detection boxes */}
{[
{ x: 42, y: 68, w: 46, h: 30 },
{ x: 118, y: 70, w: 54, h: 28 },
{ x: 70, y: 120, w: 50, h: 32 },
{ x: 154, y: 122, w: 46, h: 30 },
].map((b, i) => (
<motion.rect
key={i}
x={b.x}
y={b.y}
width={b.w}
height={b.h}
rx="4"
fill="none"
stroke="#4ADE80"
strokeWidth="1.6"
custom={i}
variants={detect}
/>
))}
{/* scanning line */}
{!reduce && (
<motion.rect
x="30"
y="52"
width="190"
height="3"
rx="1.5"
fill="#C9A2FF"
style={{ filter: "drop-shadow(0 0 6px rgba(201,162,255,0.9))" }}
animate={{ y: [56, 244, 56] }}
transition={{ repeat: Infinity, duration: 3.4, ease: "easeInOut" }}
/>
)}
</motion.g>
{/* ── Flow arrow ── */}
<motion.g
variants={fadeUp}
animate={reduce ? {} : { x: [0, 5, 0] }}
transition={{ repeat: Infinity, duration: 1.8, ease: "easeInOut" }}
>
<path d="M244 150 h28" stroke="#C9A2FF" strokeWidth="2.5" strokeLinecap="round" strokeDasharray="3 4" />
<path d="M270 143 l9 7 l-9 7" stroke="#C9A2FF" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" fill="none" />
</motion.g>
{/* ── Resulting catalog cards ── */}
{[0, 1, 2].map((i) => (
<motion.g key={i} custom={i} variants={cardIn}>
<rect x="292" y={58 + i * 62} width="150" height="52" rx="12" fill="#FFFFFF" />
<rect x="304" y={70 + i * 62} width="30" height="28" rx="7" fill={["#EDE9FE", "#DCFCE7", "#FFF3D6"][i]} />
<rect x="344" y={72 + i * 62} width="76" height="7" rx="3.5" fill="#16001D" opacity="0.8" />
<rect x="344" y={84 + i * 62} width="48" height="6" rx="3" fill="#B9A6E0" />
<circle cx="428" cy={76 + i * 62} r="7" fill="#22C55E" />
<path d={`M425 ${76 + i * 62} l2 2 l4 -4.5`} stroke="#fff" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" fill="none" />
</motion.g>
))}
</motion.svg>
);
}
export function ProductLLMWorkflow() {
const reduce = useReducedMotion();
const sectionRef = useRef<HTMLElement>(null);
const { scrollYProgress } = useScroll({
target: sectionRef,
offset: ["start end", "end start"],
});
const heroY = useTransform(scrollYProgress, [0, 1], reduce ? [0, 0] : [56, -56]);
const illustrationY = useTransform(scrollYProgress, [0, 1], reduce ? [0, 0] : [40, -40]);
return (
<section
id="productllm"
ref={sectionRef}
className="section w-full border-b border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] overflow-hidden"
>
<div className="page-container">
<div className="grid items-center gap-10 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] lg:gap-14">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="max-w-[620px]"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/[0.06] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#C9A2FF]"
>
<MaterialIcon icon="auto_awesome" size={14} />
The Differentiator
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-white mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
ProductLLM turns a shelf into a store.
</motion.h2>
<motion.p
variants={fadeUp}
className="text-white/70 mt-4"
style={{ fontSize: 18, lineHeight: 1.65 }}
>
The hardest part of going online is the catalog. ProductLLM does it
from a single photo recognizing every product, then enriching it
with category, nutrition, ingredients, price, and stock.
</motion.p>
</motion.div>
{/* animated scan illustration */}
<div className="w-full">
<ShelfScanSVG />
</div>
</div>
{/* Workflow timeline (left) supported by branded illustration (right) */}
<div className="mt-14 grid items-center gap-12 lg:grid-cols-[minmax(0,58fr)_minmax(0,42fr)] lg:gap-16">
<div className="relative w-full max-w-3xl">
{/* Animated vertical connector */}
<motion.div
className="absolute left-[26px] md:left-[30px] top-8 bottom-8 w-0.5 origin-top bg-gradient-to-b from-[#C9A2FF]/60 via-[#8B5CF6]/40 to-[#22C55E]/50 hidden sm:block"
variants={connectorVariant}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.05 }}
/>
{/* Pulse of light travelling down the pipeline */}
{!reduce && (
<motion.span
aria-hidden="true"
className="absolute left-[23px] md:left-[27px] top-8 hidden h-3 w-3 rounded-full bg-[#C9A2FF] shadow-[0_0_14px_4px_rgba(201,162,255,0.6)] sm:block"
animate={{ y: [0, 380], opacity: [0, 1, 1, 0] }}
transition={{ repeat: Infinity, duration: 3.6, ease: "easeInOut" }}
/>
)}
<motion.div
className="space-y-4"
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.09, delayChildren: 0.1 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.05 }}
>
{STEPS.map((step, i) => {
const isLast = i === STEPS.length - 1;
return (
<motion.div
key={step.title}
className="relative flex gap-5 sm:gap-7"
variants={stepVariant}
>
<motion.div
className={`relative z-10 flex h-[52px] w-[52px] md:h-[60px] md:w-[60px] shrink-0 items-center justify-center rounded-full border shadow-[0_8px_20px_rgba(20,0,30,0.4)] ${
isLast
? "border-[#22C55E]/40 bg-[#22C55E]/15 text-[#4ADE80]"
: "border-white/12 bg-white/[0.07] text-[#C9A2FF]"
}`}
animate={isLast && !reduce ? { scale: [1, 1.06, 1] } : {}}
transition={{ repeat: Infinity, duration: 2.4, ease: "easeInOut" }}
>
<MaterialIcon icon={step.icon} size={26} />
</motion.div>
<div
className={`flex-1 rounded-[22px] border p-5 md:p-6 ${
isLast
? "border-[#22C55E]/25 bg-[#22C55E]/[0.08]"
: "border-white/10 bg-white/[0.04]"
}`}
>
<div className="flex items-center gap-3">
<span className="text-xs font-black tracking-widest text-white/40">
{String(i + 1).padStart(2, "0")}
</span>
<p className="font-display font-black text-white text-lg leading-tight">
{step.title}
</p>
</div>
<p className="mt-1.5 text-sm leading-relaxed text-white/65">
{step.desc}
</p>
</div>
</motion.div>
);
})}
</motion.div>
</div>
{/* Branded illustration filling the right side */}
<motion.div
style={{ y: illustrationY }}
className="relative flex items-center justify-center justify-self-center lg:justify-self-end"
>
{/* soft purple radial glow */}
<div
aria-hidden="true"
className="pointer-events-none absolute left-1/2 top-1/2 h-[75%] w-[75%] -translate-x-1/2 -translate-y-1/2 rounded-full blur-3xl"
style={{
background:
"radial-gradient(circle, rgba(139,92,246,0.38) 0%, rgba(124,58,237,0.18) 45%, transparent 72%)",
}}
/>
{/* subtle floating illustration */}
<motion.div
animate={reduce ? {} : { y: [0, -14, 0] }}
transition={{ repeat: Infinity, duration: 7, ease: "easeInOut" }}
className="relative"
>
<Image
src="/superhero.png"
alt="ProductLLM building a live catalog"
width={1341}
height={1533}
sizes="(max-width: 640px) 300px, (max-width: 1024px) 380px, 520px"
className="h-auto w-[300px] sm:w-[320px] md:w-[380px] lg:w-full lg:max-w-[520px]"
/>
</motion.div>
</motion.div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,192 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import {
fadeUp,
slideInLeftSubtle,
slideInRight,
staggerContainer,
staggerCards,
viewport,
viewport20,
} from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const rowContainer = {
hidden: {},
visible: { transition: { staggerChildren: 0.06 } },
};
type Row = {
topic: string;
icon: string;
quick: string;
ai: string;
};
const ROWS: Row[] = [
{
topic: "Fulfillment",
icon: "local_shipping",
quick: "Central dark stores hold the stock and ship it out.",
ai: "The neighborhood store you already trust fulfills the order.",
},
{
topic: "Store Ownership",
icon: "storefront",
quick: "Inventory and brand belong to the platform.",
ai: "The merchant keeps their shop, stock, and customers.",
},
{
topic: "Trust",
icon: "verified_user",
quick: "A logo and a delivery promise.",
ai: "A shopkeeper the neighborhood has known for years.",
},
{
topic: "AI Catalog",
icon: "auto_awesome",
quick: "Catalogs are built and maintained by hand.",
ai: "ProductLLM turns a shelf into a live digital catalog.",
},
{
topic: "Sustainability",
icon: "eco",
quick: "New warehouses, new fleets, new footprint.",
ai: "Runs on stores and trips that already exist.",
},
{
topic: "Offline Support",
icon: "wifi_off",
quick: "Everything depends on being online.",
ai: "Billing and inventory keep working offline.",
},
{
topic: "Customer Relationship",
icon: "favorite",
quick: "Owned by the platform, not the store.",
ai: "Stays between the shopper and their local store.",
},
];
export function AICommerceComparison() {
const reduce = useReducedMotion();
return (
<section className="section section-soft w-full border-y border-[#6330D6]/10 overflow-hidden">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[760px] mx-auto"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
>
The Difference
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
AI Commerce vs Quick Commerce
</motion.h2>
<motion.p
variants={fadeUp}
className="text-[#5E5866] mt-4"
style={{ fontSize: 18, lineHeight: 1.65 }}
>
Two ways to bring local shopping online. One builds around the
neighborhood store the other around a warehouse.
</motion.p>
</motion.div>
{/* Column headers (desktop) */}
<div className="mt-12 hidden md:grid grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] gap-5 px-1">
<div className="pl-2">
<span className="text-xs font-black uppercase tracking-widest text-[#5E5866]">
Quick Commerce
</span>
</div>
<div className="pl-2">
<span className="text-xs font-black uppercase tracking-widest text-[#6330D6]">
AI Commerce · Nearle
</span>
</div>
</div>
<motion.div
variants={staggerCards}
initial="hidden"
whileInView="visible"
viewport={viewport20}
className="mt-3 flex flex-col gap-4"
>
{ROWS.map((row) => (
<motion.div
key={row.topic}
variants={rowContainer}
className="grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] md:items-stretch"
>
{/* Quick commerce side — slides in from the left */}
<motion.div
variants={slideInLeftSubtle}
whileHover={reduce ? undefined : { y: -3 }}
transition={{ duration: 0.25, ease: EASE }}
className="relative rounded-[24px] border border-[#6330D6]/10 bg-white p-6 shadow-[0_10px_30px_rgba(22,0,29,0.05)]"
>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-[#F3F3F6] text-[#6D6172]">
<MaterialIcon icon={row.icon} size={20} />
</div>
<p className="font-display font-black text-[#16001D] text-base md:hidden">
{row.topic}
</p>
<span className="ml-auto text-[10px] font-black uppercase tracking-widest text-[#6D6172] md:hidden">
Quick
</span>
</div>
<p className="mt-3 text-[15px] leading-relaxed text-[#5E5866]">
{row.quick}
</p>
</motion.div>
{/* AI commerce side — slides in from the right */}
<motion.div
variants={slideInRight}
whileHover={reduce ? undefined : { y: -5 }}
transition={{ duration: 0.25, ease: EASE }}
className="group relative overflow-hidden rounded-[24px] border border-[#6330D6]/20 bg-[linear-gradient(135deg,#ffffff_0%,#fbf7ff_55%,#f1e8fb_100%)] p-6 shadow-[0_4px_28px_rgba(99,48,214,0.10)] transition-shadow duration-300 hover:shadow-[0_14px_40px_rgba(99,48,214,0.18)]"
>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-[#6330D6]/10 text-[#6330D6]">
<MaterialIcon icon={row.icon} size={20} />
</div>
<p className="font-display font-black text-[#16001D] text-base">
{row.topic}
</p>
<motion.span
className="ml-auto flex h-6 w-6 items-center justify-center rounded-full bg-[#DCFCE7] text-[#16A34A]"
animate={reduce ? {} : { scale: [1, 1.12, 1] }}
transition={{ repeat: Infinity, duration: 2.4, ease: "easeInOut" }}
>
<MaterialIcon icon="check" size={14} />
</motion.span>
</div>
<p className="mt-3 text-[15px] leading-relaxed text-[#3F3A47]">
{row.ai}
</p>
</motion.div>
</motion.div>
))}
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,63 @@
"use client";
import { motion } from "framer-motion";
import { fadeUp, staggerSlow, viewport } from "@/lib/animations";
type PullQuoteProps = {
lines: string[];
attribution?: string;
};
export function PullQuote({ lines, attribution }: PullQuoteProps) {
return (
<section className="bg-white px-2 py-2 md:px-3 md:py-3 lg:px-4 lg:py-4">
<motion.div
variants={staggerSlow}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="relative overflow-hidden rounded-[28px] bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] px-6 py-16 text-center md:px-12 md:py-20"
>
{/* Ambient glow */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0"
style={{
background:
"radial-gradient(circle at 30% 20%, rgba(124,58,237,0.28), transparent 45%), radial-gradient(circle at 75% 80%, rgba(168,85,247,0.18), transparent 45%)",
}}
/>
<motion.span
variants={fadeUp}
aria-hidden="true"
className="relative block font-display text-6xl font-black leading-none text-[#C9A2FF]/40"
>
&ldquo;
</motion.span>
<motion.blockquote
variants={fadeUp}
className="relative mx-auto max-w-[860px] font-display font-black text-white tracking-tight"
style={{ fontSize: "clamp(26px, 3.4vw, 46px)", lineHeight: 1.18 }}
>
{lines.map((line, i) => (
<span key={i} className="block">
{i === lines.length - 1 ? (
<span className="text-[#C9A2FF]">{line}</span>
) : (
line
)}
</span>
))}
</motion.blockquote>
{attribution && (
<motion.p
variants={fadeUp}
className="relative mt-6 text-sm font-black uppercase tracking-widest text-white/50"
>
{attribution}
</motion.p>
)}
</motion.div>
</section>
);
}

View File

@@ -0,0 +1,121 @@
"use client";
import Link from "next/link";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const VP = { once: true, amount: 0.3 } as const;
const stagger = { hidden: {}, visible: { transition: { staggerChildren: 0.12 } } };
const reveal = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.55, ease: EASE } },
};
const VALUES = [
{ icon: "auto_awesome", label: "AI-built catalog" },
{ icon: "qr_code_2", label: "QR ordering" },
{ icon: "point_of_sale", label: "Free POS" },
];
export function AICommerceCTA() {
const reduce = useReducedMotion();
return (
<section className="bg-white px-2 pt-2 pb-2 md:px-3 md:pt-3 md:pb-3 lg:px-4 lg:pt-4 lg:pb-4 2xl:px-6 2xl:pb-5">
<motion.div
variants={stagger}
initial="hidden"
whileInView="visible"
viewport={VP}
className="relative w-full overflow-hidden rounded-[28px] bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] text-white"
>
{/* Ambient animated glows */}
{!reduce && (
<>
<motion.div
aria-hidden="true"
className="pointer-events-none absolute -left-24 -top-24 h-72 w-72 rounded-full blur-3xl"
style={{ background: "radial-gradient(circle, rgba(124,58,237,0.45), rgba(124,58,237,0) 70%)" }}
animate={{ opacity: [0.4, 0.75, 0.4], scale: [1, 1.15, 1] }}
transition={{ repeat: Infinity, duration: 5, ease: "easeInOut" }}
/>
<motion.div
aria-hidden="true"
className="pointer-events-none absolute -right-24 -bottom-24 h-72 w-72 rounded-full blur-3xl"
style={{ background: "radial-gradient(circle, rgba(201,162,255,0.35), rgba(201,162,255,0) 70%)" }}
animate={{ opacity: [0.35, 0.65, 0.35], scale: [1.1, 1, 1.1] }}
transition={{ repeat: Infinity, duration: 6, ease: "easeInOut" }}
/>
</>
)}
<div className="relative z-10 px-6 py-16 text-center md:px-10 md:py-20 lg:px-14">
<div className="mx-auto max-w-2xl">
<motion.span
variants={reveal}
className="mb-6 inline-flex items-center gap-2 rounded-full border border-white/15 px-3 py-1 text-xs font-black uppercase tracking-widest text-white/70"
>
<MaterialIcon icon="hub" size={14} />
Join the ecosystem
</motion.span>
<motion.h2
variants={reveal}
className="font-display font-black tracking-tight leading-[1.08]"
style={{ fontSize: "clamp(32px, 4vw, 56px)" }}
>
Bring your store into
<br />
the age of <span className="text-[#C9A2FF]">AI Commerce</span>
</motion.h2>
<motion.p
variants={reveal}
className="mx-auto mt-5 max-w-lg text-base leading-relaxed text-white/70 md:text-lg"
>
Keep the trust, the customers and the corner you&rsquo;ve built.
Nearle adds the technology for free.
</motion.p>
{/* Value chips */}
<motion.div variants={reveal} className="mt-8 flex flex-wrap justify-center gap-2.5">
{VALUES.map((v) => (
<span
key={v.label}
className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-sm font-bold text-white/85"
>
<MaterialIcon icon={v.icon} size={16} className="text-[#C9A2FF]" />
{v.label}
</span>
))}
</motion.div>
<motion.div variants={reveal} className="mt-10 flex flex-wrap justify-center gap-3.5">
<Link
href="/businesses"
className="group flex items-center gap-2 rounded-xl bg-white px-8 py-3.5 text-sm font-black text-[#16001D] shadow-[0_16px_30px_rgba(0,0,0,0.35)] transition-transform duration-200 hover:scale-[1.03] active:scale-95"
>
<MaterialIcon icon="storefront" size={18} />
<span>Start Your Store</span>
<MaterialIcon
icon="arrow_forward"
size={18}
className="transition-transform duration-200 group-hover:translate-x-0.5"
/>
</Link>
<Link
href="/platform"
className="flex items-center gap-2 rounded-xl border border-white/20 bg-white/10 px-8 py-3.5 text-sm font-black text-white transition-colors duration-200 hover:bg-white/15 active:scale-95"
>
<MaterialIcon icon="hub" size={18} />
<span>Explore the Platform</span>
</Link>
</motion.div>
</div>
</div>
</motion.div>
</section>
);
}

View File

@@ -0,0 +1,195 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { cardReveal, fadeUp, staggerContainer, staggerSlow, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const HAVE = [
{ img: "/trust.svg", label: "Trust", desc: "Built over years, face to face." },
{ img: "/customers.svg", label: "Customers", desc: "Regulars who already walk in." },
{ img: "/location.svg", label: "Location", desc: "Minutes from every doorstep." },
];
const ADD = [
{ icon: "auto_awesome", label: "AI", desc: "ProductLLM builds the catalog." },
{ icon: "storefront", label: "Digital Storefront", desc: "Their shop, now online." },
{ icon: "inventory_2", label: "Inventory", desc: "Live stock, always accurate." },
{ icon: "shopping_bag", label: "Ordering", desc: "Pickup and delivery, built in." },
];
export function AICommerceExplainer() {
const reduce = useReducedMotion();
return (
<section className="section bg-white w-full border-b border-[#6330D6]/10 overflow-hidden">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[760px] mx-auto"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
>
The Idea
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
AI Commerce doesn&rsquo;t replace the neighborhood store. It upgrades it.
</motion.h2>
<motion.p
variants={fadeUp}
className="text-[#5E5866] mt-4"
style={{ fontSize: 18, lineHeight: 1.65 }}
>
Local shops already own the hardest part of retail. Nearle simply
adds the technology layer on top.
</motion.p>
</motion.div>
<div className="relative mt-12 grid gap-6 lg:grid-cols-2 lg:items-stretch">
{/* Center merge badge — trust + technology */}
<motion.div
aria-hidden="true"
initial={{ opacity: 0, scale: 0.6 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true, amount: 0.5 }}
transition={{ duration: 0.5, delay: 0.3, ease: [0.22, 1, 0.36, 1] }}
className="pointer-events-none absolute left-1/2 top-1/2 z-20 hidden h-14 w-14 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-[#6330D6]/15 bg-white text-[#6330D6] shadow-[0_10px_30px_rgba(99,48,214,0.22)] lg:flex"
>
{!reduce && (
<motion.span
className="absolute inset-0 rounded-full border-2 border-[#7C3AED]"
animate={{ scale: [1, 1.5], opacity: [0.5, 0] }}
transition={{ repeat: Infinity, duration: 2.2, ease: "easeOut" }}
/>
)}
<MaterialIcon icon="add" size={26} />
</motion.div>
{/* Already have */}
<motion.div
variants={staggerSlow}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="rounded-[32px] border border-[#6330D6]/10 bg-[#FAF7FD] p-7 md:p-9 shadow-[0_18px_45px_rgba(22,0,29,0.06)]"
>
<motion.p
variants={fadeUp}
className="text-xs font-black uppercase tracking-widest text-[#5E5866]"
>
Stores already have
</motion.p>
<div className="mt-5 flex flex-col gap-4">
{HAVE.map((item, i) => (
<motion.div
key={item.label}
variants={cardReveal}
whileHover={reduce ? undefined : { y: -6 }}
transition={{ duration: 0.3, ease: EASE }}
className="group flex min-h-[220px] flex-col gap-3 rounded-3xl bg-white px-6 py-5 md:min-h-0 md:px-7 md:py-6 border border-[#6330D6]/[0.08] shadow-[0_10px_30px_rgba(22,0,29,0.05)] transition-shadow duration-300 hover:shadow-[0_22px_50px_rgba(99,48,214,0.14)] md:flex-row md:items-center md:gap-6"
>
{/* Left — heading + supporting copy (48%) */}
<div className="min-w-0 md:basis-[48%]">
<p className="font-display font-black text-[#16001D] leading-[1.05] tracking-tight" style={{ fontSize: "clamp(24px, 2.1vw, 30px)" }}>
{item.label}
</p>
<p className="mt-2 text-[15px] leading-relaxed text-[#5E5866]">
{item.desc}
</p>
</div>
{/* Right — animated illustration (52%, floating + gentle rotation) */}
<div className="relative flex w-full items-center justify-center md:basis-[52%]">
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 m-auto rounded-full opacity-70 blur-2xl transition-opacity duration-300 group-hover:opacity-100"
style={{
width: "58%",
height: "58%",
background:
"radial-gradient(circle, rgba(124,58,237,0.16), rgba(124,58,237,0) 70%)",
}}
/>
<motion.img
src={item.img}
alt=""
aria-hidden="true"
className="relative h-auto"
style={{ width: "clamp(148px, 34vw, 168px)" }}
animate={reduce ? {} : { y: [0, -9, 0], rotate: [-1.5, 1.5, -1.5] }}
transition={{ repeat: Infinity, duration: 4 + i * 0.6, ease: "easeInOut" }}
/>
</div>
</motion.div>
))}
</div>
</motion.div>
{/* Nearle adds */}
<motion.div
variants={staggerSlow}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="flex flex-col rounded-[32px] border border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] p-7 md:p-9 shadow-[0_20px_50px_rgba(20,0,30,0.28)]"
>
<motion.p
variants={fadeUp}
className="text-xs font-black uppercase tracking-widest text-[#C9A2FF]"
>
Nearle adds
</motion.p>
<div className="mt-6 grid flex-1 gap-4 sm:grid-cols-2 sm:auto-rows-fr">
{ADD.map((item, i) => (
<motion.div
key={item.label}
variants={cardReveal}
whileHover={reduce ? undefined : { y: -6 }}
transition={{ duration: 0.28, ease: EASE }}
className="group relative flex min-h-[196px] flex-col items-start overflow-hidden rounded-3xl bg-white/[0.05] p-6 border border-white/10 transition-colors duration-300 hover:border-[#C9A2FF]/45 hover:bg-white/[0.09]"
>
{/* Illustration — glowing icon medallion (top of the stack) */}
<div className="relative mb-5">
{/* Stronger purple radial glow behind the illustration */}
{!reduce && (
<motion.div
aria-hidden="true"
className="pointer-events-none absolute -inset-5 rounded-full blur-2xl"
style={{ background: "radial-gradient(circle, rgba(124,58,237,0.55), rgba(124,58,237,0) 70%)" }}
animate={{ opacity: [0.5, 0.9, 0.5], scale: [1, 1.14, 1] }}
transition={{ repeat: Infinity, duration: 3.6 + i * 0.4, ease: "easeInOut" }}
/>
)}
<motion.div
className="relative flex h-14 w-14 items-center justify-center rounded-2xl border border-[#C9A2FF]/35 bg-gradient-to-br from-[#7C3AED]/45 to-[#4C1D95]/25 text-[#EBDBFF] shadow-[0_12px_32px_rgba(124,58,237,0.5)] transition-colors duration-300 group-hover:border-[#C9A2FF]/60"
animate={reduce ? {} : { y: [0, -6, 0] }}
transition={{ repeat: Infinity, duration: 5 + i * 0.5, ease: "easeInOut" }}
>
<MaterialIcon icon={item.icon} size={30} />
</motion.div>
</div>
{/* Heading */}
<p className="relative z-10 font-display font-black text-white text-xl leading-tight">
{item.label}
</p>
{/* Description */}
<p className="relative z-10 mt-1.5 text-sm leading-relaxed text-white/65">{item.desc}</p>
</motion.div>
))}
</div>
</motion.div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,196 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const TRADITIONAL = [
{ icon: "edit_note", label: "Manual Catalog" },
{ icon: "store", label: "Offline Store" },
{ icon: "directions_walk", label: "Walk-in Customers" },
{ icon: "receipt_long", label: "Manual Billing" },
];
const NEARLE = [
{ icon: "auto_awesome", label: "ProductLLM" },
{ icon: "storefront", label: "Digital Storefront" },
{ icon: "shopping_bag", label: "Omnichannel Ordering" },
{ icon: "travel_explore", label: "AI Powered Discovery" },
];
const nodeVariant = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: EASE } },
};
const connectorVariant = {
hidden: { scaleY: 0 },
visible: { scaleY: 1, transition: { duration: 0.5, ease: EASE } },
};
function FlowColumn({
eyebrow,
items,
tone,
reduce,
}: {
eyebrow: string;
items: { icon: string; label: string }[];
tone: "muted" | "brand";
reduce: boolean | null;
}) {
const isBrand = tone === "brand";
return (
<motion.div
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.12 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
className={`relative flex flex-col items-center rounded-[32px] border p-8 md:p-10 ${
isBrand
? "border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] shadow-[0_24px_60px_rgba(20,0,30,0.32)]"
: "border-[#6330D6]/10 bg-[#FAF7FD] shadow-[0_14px_40px_rgba(22,0,29,0.05)]"
}`}
>
<motion.p
variants={nodeVariant}
className={`text-xs font-black uppercase tracking-widest ${
isBrand ? "text-[#C9A2FF]" : "text-[#5E5866]"
}`}
>
{eyebrow}
</motion.p>
<div className="mt-8 flex w-full max-w-[300px] flex-col items-center">
{items.map((item, i) => (
<div key={item.label} className="flex w-full flex-col items-center">
<motion.div
variants={nodeVariant}
whileHover={reduce ? undefined : { y: -4, scale: 1.02 }}
transition={{ duration: 0.25, ease: EASE }}
className={`flex w-full items-center gap-4 rounded-2xl border px-5 py-4 ${
isBrand
? "border-white/10 bg-white/[0.06] hover:border-[#C9A2FF]/40 hover:bg-white/[0.1]"
: "border-[#6330D6]/10 bg-white shadow-[0_8px_22px_rgba(22,0,29,0.04)]"
} transition-colors duration-300`}
>
<div
className={`flex h-11 w-11 shrink-0 items-center justify-center rounded-xl ${
isBrand ? "bg-white/10 text-[#C9A2FF]" : "bg-[#6330D6]/[0.08] text-[#6330D6]"
}`}
>
<MaterialIcon icon={item.icon} size={22} />
</div>
<p
className={`font-display font-black leading-tight ${
isBrand ? "text-white" : "text-[#16001D]"
} text-[15px] md:text-base`}
>
{item.label}
</p>
</motion.div>
{/* Animated connector + travelling pulse */}
{i < items.length - 1 && (
<div className="relative my-1.5 h-8 w-0.5">
<motion.span
variants={connectorVariant}
className={`absolute inset-0 origin-top rounded-full ${
isBrand ? "bg-[#C9A2FF]/40" : "bg-[#6330D6]/25"
}`}
/>
{!reduce && (
<motion.span
aria-hidden="true"
className={`absolute left-1/2 top-0 h-2 w-2 -translate-x-1/2 rounded-full ${
isBrand
? "bg-[#C9A2FF] shadow-[0_0_10px_2px_rgba(201,162,255,0.6)]"
: "bg-[#7C3AED] shadow-[0_0_10px_2px_rgba(124,58,237,0.5)]"
}`}
animate={{ y: [0, 26], opacity: [0, 1, 0] }}
transition={{
repeat: Infinity,
duration: 1.8,
ease: "easeInOut",
delay: i * 0.3,
}}
/>
)}
</div>
)}
</div>
))}
</div>
</motion.div>
);
}
export function BeforeAfterFlow() {
const reduce = useReducedMotion();
return (
<section className="section section-soft w-full border-b border-[#6330D6]/10 overflow-hidden">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[720px] mx-auto"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
>
Before vs After
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
The same store, reimagined
</motion.h2>
</motion.div>
<div className="relative mt-14 grid gap-6 md:grid-cols-2 md:items-start md:gap-8">
<FlowColumn
eyebrow="Traditional Commerce"
items={TRADITIONAL}
tone="muted"
reduce={reduce}
/>
{/* Center VS badge (desktop) */}
<motion.div
aria-hidden="true"
initial={{ opacity: 0, scale: 0.6 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true, amount: 0.5 }}
transition={{ duration: 0.5, delay: 0.25, ease: EASE }}
className="pointer-events-none absolute left-1/2 top-1/2 z-20 hidden h-14 w-14 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-[#6330D6]/15 bg-white text-xs font-black uppercase tracking-widest text-[#6330D6] shadow-[0_10px_30px_rgba(99,48,214,0.22)] md:flex"
>
{!reduce && (
<motion.span
className="absolute inset-0 rounded-full border-2 border-[#7C3AED]"
animate={{ scale: [1, 1.45], opacity: [0.5, 0] }}
transition={{ repeat: Infinity, duration: 2.2, ease: "easeOut" }}
/>
)}
VS
</motion.div>
<FlowColumn
eyebrow="Nearle AI Commerce"
items={NEARLE}
tone="brand"
reduce={reduce}
/>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,156 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const STAGES = [
{
era: "The corner shop",
title: "Traditional Retail",
desc: "Trust and relationships, built one street at a time.",
icon: "storefront",
},
{
era: "The catalog goes online",
title: "E-Commerce",
desc: "Endless shelves, but the neighborhood disappears.",
icon: "shopping_cart",
},
{
era: "Speed at all costs",
title: "Quick Commerce",
desc: "Minutes-fast delivery, powered by dark stores.",
icon: "bolt",
},
{
era: "The store, upgraded",
title: "AI Commerce",
desc: "Local stores digitized by AI — trust and technology together.",
icon: "auto_awesome",
highlight: true,
},
];
const connectorVariant = {
hidden: { scaleY: 0 },
visible: { scaleY: 1, transition: { duration: 1, ease: EASE } },
};
export function CommerceEvolution() {
const reduce = useReducedMotion();
return (
<section className="section bg-white w-full border-b border-[#6330D6]/10 overflow-hidden">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[720px] mx-auto"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
>
The Evolution
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
How commerce got here
</motion.h2>
</motion.div>
<div className="relative mt-12 max-w-2xl mx-auto">
{/* Vertical connector */}
<motion.div
className="absolute left-[26px] md:left-[30px] top-8 bottom-8 w-0.5 origin-top bg-gradient-to-b from-[#6330D6]/30 via-[#6330D6]/25 to-[#6330D6]/40 hidden sm:block"
variants={connectorVariant}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
/>
{/* Pulse travelling down the timeline */}
{!reduce && (
<motion.span
aria-hidden="true"
className="absolute left-[23px] md:left-[27px] top-8 hidden h-2.5 w-2.5 rounded-full bg-[#7C3AED] shadow-[0_0_12px_3px_rgba(124,58,237,0.5)] sm:block"
animate={{ y: [0, 300], opacity: [0, 1, 1, 0] }}
transition={{ repeat: Infinity, duration: 3.4, ease: "easeInOut" }}
/>
)}
<motion.div
className="space-y-5"
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.12, delayChildren: 0.1 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
>
{STAGES.map((stage, i) => (
<motion.div
key={stage.title}
className="relative flex gap-5 sm:gap-7"
variants={{
hidden: { opacity: 0, x: -24 },
visible: { opacity: 1, x: 0, transition: { duration: 0.55, ease: EASE } },
}}
>
<motion.div
className={`relative z-10 flex h-[52px] w-[52px] md:h-[60px] md:w-[60px] shrink-0 items-center justify-center rounded-full shadow-[0_8px_20px_rgba(99,48,214,0.25)] ${
stage.highlight
? "bg-gradient-to-br from-[#7C3AED] to-[#6330D6] text-white"
: "bg-[#F3E8F6] text-[#6330D6]"
}`}
animate={reduce ? {} : { y: [0, -5, 0] }}
transition={{ repeat: Infinity, duration: 3.2 + i * 0.35, ease: "easeInOut" }}
>
{/* pulsing ring on the current era */}
{stage.highlight && !reduce && (
<motion.span
aria-hidden="true"
className="absolute inset-0 rounded-full border-2 border-[#7C3AED]"
animate={{ scale: [1, 1.4], opacity: [0.6, 0] }}
transition={{ repeat: Infinity, duration: 2, ease: "easeOut" }}
/>
)}
<MaterialIcon icon={stage.icon} size={26} />
</motion.div>
<div
className={`flex-1 rounded-[24px] border p-5 md:p-6 transition-shadow ${
stage.highlight
? "border-[#6330D6]/20 bg-[linear-gradient(135deg,#ffffff_0%,#faf5ff_55%,#f1e8fb_100%)] shadow-[0_12px_35px_rgba(99,48,214,0.12)]"
: "border-[#6330D6]/10 bg-white shadow-[0_10px_30px_rgba(22,0,29,0.05)]"
}`}
>
<div className="flex items-center gap-3">
<p className="font-display font-black text-[#16001D] text-lg md:text-xl leading-tight">
{stage.title}
</p>
{stage.highlight && (
<span className="rounded-full bg-[#6330D6] px-2.5 py-0.5 text-[10px] font-black uppercase tracking-wide text-white">
Now
</span>
)}
</div>
<p className="mt-0.5 text-xs font-black uppercase tracking-widest text-[#6330D6]/60">
{stage.era}
</p>
<p className="mt-2 text-[15px] leading-relaxed text-[#5E5866]">
{stage.desc}
</p>
</div>
</motion.div>
))}
</motion.div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,149 @@
"use client";
import { Fragment } from "react";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const NODES = [
{ icon: "store", label: "Merchant", sub: "The local shop" },
{ icon: "point_of_sale", label: "MerchantOS", sub: "Billing & stock" },
{ icon: "auto_awesome", label: "ProductLLM", sub: "AI catalog engine", hub: true },
{ icon: "smartphone", label: "ConsumerOS", sub: "Shopping app" },
{ icon: "mood", label: "Customer", sub: "The neighbour" },
];
const nodeVariant = {
hidden: { opacity: 0, scale: 0.85, y: 16 },
visible: { opacity: 1, scale: 1, y: 0, transition: { duration: 0.55, ease: EASE } },
};
function Connector({ reduce, delay }: { reduce: boolean | null; delay: number }) {
return (
<motion.div
variants={{
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { duration: 0.4, ease: EASE } },
}}
className="relative my-1 h-9 w-0.5 overflow-hidden rounded-full bg-white/10 md:mx-2 md:my-0 md:mt-9 md:h-0.5 md:w-auto md:flex-1"
>
{!reduce && (
<>
{/* Vertical pulse (mobile) */}
<motion.span
aria-hidden="true"
className="absolute left-1/2 top-0 h-3 w-1 -translate-x-1/2 rounded-full bg-[#C9A2FF] shadow-[0_0_10px_2px_rgba(201,162,255,0.7)] md:hidden"
animate={{ y: [-4, 36], opacity: [0, 1, 0] }}
transition={{ repeat: Infinity, duration: 1.8, ease: "easeInOut", delay }}
/>
{/* Horizontal pulse (desktop) */}
<motion.span
aria-hidden="true"
className="absolute left-0 top-1/2 hidden h-1 w-4 -translate-y-1/2 rounded-full bg-[#C9A2FF] shadow-[0_0_10px_2px_rgba(201,162,255,0.7)] md:block"
animate={{ x: ["-10%", "460%"], opacity: [0, 1, 0] }}
transition={{ repeat: Infinity, duration: 2, ease: "easeInOut", delay }}
/>
</>
)}
</motion.div>
);
}
export function EcosystemDiagram() {
const reduce = useReducedMotion();
return (
<section className="section w-full overflow-hidden border-b border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D]">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[720px] mx-auto"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center rounded-full bg-white/10 px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#C9A2FF]"
>
The Ecosystem
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-white mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
One connected loop, powered by AI
</motion.h2>
<motion.p
variants={fadeUp}
className="text-white/70 mt-4"
style={{ fontSize: 18, lineHeight: 1.65 }}
>
Every part of Nearle talks to the next from the merchant&rsquo;s
shelf to the customer&rsquo;s doorstep.
</motion.p>
</motion.div>
<motion.div
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.12 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
className="mt-16 flex flex-col items-center md:flex-row md:items-start md:justify-between"
>
{NODES.map((node, i) => (
<Fragment key={node.label}>
<motion.div
variants={nodeVariant}
whileHover={reduce ? undefined : { y: -6 }}
transition={{ duration: 0.25, ease: EASE }}
className="flex shrink-0 flex-col items-center text-center"
>
<div className="relative">
{/* Glow */}
<div
aria-hidden="true"
className={`pointer-events-none absolute inset-0 rounded-full blur-2xl ${
node.hub ? "opacity-90" : "opacity-50"
}`}
style={{
background:
"radial-gradient(circle, rgba(124,58,237,0.7), rgba(124,58,237,0) 70%)",
}}
/>
{/* Pulsing ring on the hub */}
{node.hub && !reduce && (
<motion.span
aria-hidden="true"
className="absolute inset-0 rounded-full border-2 border-[#C9A2FF]"
animate={{ scale: [1, 1.35], opacity: [0.6, 0] }}
transition={{ repeat: Infinity, duration: 2, ease: "easeOut" }}
/>
)}
<div
className={`relative flex items-center justify-center rounded-full ${
node.hub
? "h-[84px] w-[84px] bg-gradient-to-br from-[#7C3AED] to-[#6330D6] text-white shadow-[0_16px_40px_rgba(124,58,237,0.5)]"
: "h-[72px] w-[72px] bg-white/[0.08] text-[#C9A2FF] border border-white/15 shadow-[0_12px_30px_rgba(0,0,0,0.35)]"
}`}
>
<MaterialIcon icon={node.icon} size={node.hub ? 36 : 30} />
</div>
</div>
<p className="mt-4 font-display font-black text-white text-base md:text-lg leading-tight">
{node.label}
</p>
<p className="mt-0.5 text-xs text-white/50">{node.sub}</p>
</motion.div>
{i < NODES.length - 1 && <Connector reduce={reduce} delay={i * 0.25} />}
</Fragment>
))}
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,153 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const MILESTONES = [
{
phase: "Today",
title: "AI Commerce, live",
desc: "Digital catalogs, QR ordering and live inventory.",
icon: "bolt",
now: true,
},
{
phase: "Next",
title: "Digitizing Stores",
desc: "Every neighbourhood shop, online in minutes.",
icon: "storefront",
},
{
phase: "Soon",
title: "Connected Neighborhoods",
desc: "Stores, shoppers and riders in one network.",
icon: "hub",
},
{
phase: "The Horizon",
title: "Future of AI Commerce",
desc: "Local commerce that runs itself.",
icon: "rocket_launch",
},
];
const nodeVariant = {
hidden: { opacity: 0, y: 22 },
visible: { opacity: 1, y: 0, transition: { duration: 0.55, ease: EASE } },
};
export function FutureVision() {
const reduce = useReducedMotion();
return (
<section className="section w-full overflow-hidden border-b border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D]">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[720px] mx-auto"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center rounded-full bg-white/10 px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#C9A2FF]"
>
The Road Ahead
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-white mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
Where AI Commerce is headed
</motion.h2>
</motion.div>
<motion.div
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.16 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.25 }}
className="relative mt-16"
>
{/* Rails */}
<div className="absolute left-[8%] right-[8%] top-[26px] hidden h-0.5 bg-white/10 md:block" />
<motion.div
className="absolute left-[8%] top-[26px] hidden h-0.5 origin-left bg-gradient-to-r from-[#C9A2FF] via-[#7C3AED] to-[#6330D6] md:block"
style={{ width: "84%" }}
variants={{ hidden: { scaleX: 0 }, visible: { scaleX: 1, transition: { duration: 1.8, ease: EASE } } }}
/>
{/* travelling glow (desktop) */}
{!reduce && (
<motion.span
aria-hidden="true"
className="absolute top-[22px] left-[8%] hidden h-2.5 w-2.5 rounded-full bg-white shadow-[0_0_14px_4px_rgba(201,162,255,0.7)] md:block"
animate={{ left: ["8%", "92%"], opacity: [0, 1, 1, 0] }}
transition={{ repeat: Infinity, duration: 3.4, ease: "easeInOut" }}
/>
)}
{/* vertical rail (mobile) */}
<div className="absolute left-[26px] top-2 bottom-2 w-0.5 -translate-x-1/2 bg-white/10 md:hidden" />
<div className="grid gap-8 md:grid-cols-4 md:gap-6">
{MILESTONES.map((m, i) => (
<motion.div
key={m.title}
variants={nodeVariant}
className="relative flex items-start gap-5 md:flex-col md:items-center md:text-center"
>
{/* node dot */}
<div className="relative z-10 shrink-0">
<div
aria-hidden="true"
className={`pointer-events-none absolute inset-0 rounded-full blur-lg ${
m.now ? "opacity-90" : "opacity-40"
}`}
style={{ background: "radial-gradient(circle, rgba(124,58,237,0.7), rgba(124,58,237,0) 70%)" }}
/>
{m.now && !reduce && (
<motion.span
aria-hidden="true"
className="absolute inset-0 rounded-full border-2 border-[#C9A2FF]"
animate={{ scale: [1, 1.5], opacity: [0.6, 0] }}
transition={{ repeat: Infinity, duration: 2, ease: "easeOut" }}
/>
)}
<div
className={`relative flex h-[52px] w-[52px] items-center justify-center rounded-full ${
m.now
? "bg-gradient-to-br from-[#7C3AED] to-[#6330D6] text-white shadow-[0_14px_34px_rgba(124,58,237,0.5)]"
: "border border-white/15 bg-white/[0.08] text-[#C9A2FF]"
}`}
>
<MaterialIcon icon={m.icon} size={24} />
</div>
</div>
<div className="md:mt-5">
<span
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-[10px] font-black uppercase tracking-widest ${
m.now ? "bg-[#6330D6] text-white" : "bg-white/10 text-[#C9A2FF]"
}`}
>
{m.phase}
</span>
<p className="mt-2 font-display font-black text-white text-lg leading-tight">
{m.title}
</p>
<p className="mt-1 text-sm leading-relaxed text-white/55 md:max-w-[220px] md:mx-auto">
{m.desc}
</p>
</div>
</motion.div>
))}
</div>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,102 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { cardReveal, fadeUp, staggerContainer, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const CARDS = [
{
img: "/trust.svg",
label: "Trust",
desc: "A face the street has known for years.",
},
{
img: "/customers.svg",
label: "Community",
desc: "Regulars who shop where they belong.",
},
{
img: "/location.svg",
label: "Convenience",
desc: "Always minutes from the doorstep.",
},
];
export function NeighborhoodStores() {
const reduce = useReducedMotion();
return (
<section className="section bg-white w-full border-b border-[#6330D6]/10 overflow-hidden">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[720px] mx-auto"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
>
Why It Works
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
Why neighborhood stores matter
</motion.h2>
</motion.div>
<motion.div
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.14 } } }}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="mt-14 grid gap-6 md:grid-cols-3 md:items-stretch"
>
{CARDS.map((card, i) => (
<motion.div
key={card.label}
variants={cardReveal}
whileHover={reduce ? undefined : { y: -8 }}
transition={{ duration: 0.3, ease: EASE }}
className="group relative flex flex-col items-center overflow-hidden rounded-[32px] border border-[#6330D6]/[0.08] bg-[linear-gradient(180deg,#ffffff_0%,#faf7fd_100%)] px-8 pt-12 pb-9 text-center shadow-[0_14px_40px_rgba(22,0,29,0.05)] transition-shadow duration-300 hover:shadow-[0_26px_60px_rgba(99,48,214,0.14)]"
>
{/* Soft glow behind the illustration */}
<div
aria-hidden="true"
className="pointer-events-none absolute left-1/2 top-14 h-48 w-48 -translate-x-1/2 rounded-full opacity-60 blur-3xl transition-opacity duration-300 group-hover:opacity-100"
style={{
background:
"radial-gradient(circle, rgba(124,58,237,0.16), rgba(124,58,237,0) 70%)",
}}
/>
{/* Large floating illustration — the visual focus */}
<motion.img
src={card.img}
alt=""
aria-hidden="true"
className="relative h-auto"
style={{ width: "clamp(190px, 22vw, 240px)" }}
animate={reduce ? {} : { y: [0, -10, 0] }}
transition={{ repeat: Infinity, duration: 4 + i * 0.6, ease: "easeInOut" }}
/>
<p className="relative mt-8 font-display font-black text-[#16001D] leading-tight tracking-tight" style={{ fontSize: "clamp(24px, 2.2vw, 30px)" }}>
{card.label}
</p>
<p className="relative mt-2 max-w-[240px] text-[15px] leading-relaxed text-[#5E5866]">
{card.desc}
</p>
</motion.div>
))}
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,186 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const MANUAL_POINTS = [
{ icon: "keyboard", label: "Typed in by hand" },
{ icon: "error_outline", label: "Missing details & typos" },
{ icon: "hourglass_empty", label: "Goes stale quickly" },
];
const AI_POINTS = [
{ icon: "photo_camera", label: "Recognised from a photo" },
{ icon: "verified", label: "Full, structured attributes" },
{ icon: "sync", label: "Always kept in sync" },
];
const panelReveal = {
hidden: { opacity: 0, y: 28 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: EASE } },
};
function Bar({
value,
brand,
}: {
value: number; // 0100 target width
brand: boolean;
}) {
return (
<div
className={`mt-6 h-2.5 w-full overflow-hidden rounded-full ${
brand ? "bg-white/10" : "bg-[#6330D6]/10"
}`}
>
<motion.div
className={`h-full rounded-full ${
brand
? "bg-gradient-to-r from-[#C9A2FF] to-[#7C3AED]"
: "bg-gradient-to-r from-[#B7ADCB] to-[#8A7FA6]"
}`}
initial={{ width: 0 }}
whileInView={{ width: `${value}%` }}
viewport={{ once: true, amount: 0.6 }}
transition={{ duration: 1.2, ease: EASE, delay: 0.2 }}
/>
</div>
);
}
function Point({
icon,
label,
brand,
}: {
icon: string;
label: string;
brand: boolean;
}) {
return (
<motion.li
variants={fadeUp}
className="flex items-center gap-3"
>
<span
className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-xl ${
brand ? "bg-white/10 text-[#C9A2FF]" : "bg-[#6330D6]/[0.07] text-[#8A7FA6]"
}`}
>
<MaterialIcon icon={icon} size={18} />
</span>
<span className={`text-[15px] font-medium ${brand ? "text-white/85" : "text-[#5E5866]"}`}>
{label}
</span>
</motion.li>
);
}
export function ProductLLMCompare() {
const reduce = useReducedMotion();
return (
<section className="section section-soft w-full border-b border-[#6330D6]/10 overflow-hidden">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[720px] mx-auto"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
>
Why ProductLLM
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
Catalogs built in minutes, not days
</motion.h2>
</motion.div>
<div className="mt-14 grid gap-6 md:grid-cols-2 md:items-stretch">
{/* Manual */}
<motion.div
variants={panelReveal}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
className="flex flex-col rounded-[32px] border border-[#6330D6]/10 bg-white p-8 md:p-10 shadow-[0_14px_40px_rgba(22,0,29,0.05)]"
>
<p className="text-xs font-black uppercase tracking-widest text-[#8A7FA6]">
Traditional catalog creation
</p>
<div className="mt-5 flex items-end gap-2">
<span className="font-display font-black leading-none text-[#16001D]" style={{ fontSize: "clamp(44px, 6vw, 68px)" }}>
~6
</span>
<span className="mb-2 text-lg font-bold text-[#5E5866]">hrs / store</span>
</div>
<Bar value={100} brand={false} />
<motion.ul
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.1, delayChildren: 0.3 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
className="mt-8 flex flex-col gap-4"
>
{MANUAL_POINTS.map((p) => (
<Point key={p.label} icon={p.icon} label={p.label} brand={false} />
))}
</motion.ul>
</motion.div>
{/* ProductLLM */}
<motion.div
variants={panelReveal}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
className="relative flex flex-col overflow-hidden rounded-[32px] border border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] p-8 md:p-10 shadow-[0_24px_60px_rgba(20,0,30,0.32)]"
>
{/* soft glow */}
{!reduce && (
<motion.div
aria-hidden="true"
className="pointer-events-none absolute -right-10 -top-10 h-48 w-48 rounded-full blur-3xl"
style={{ background: "radial-gradient(circle, rgba(124,58,237,0.5), rgba(124,58,237,0) 70%)" }}
animate={{ opacity: [0.5, 0.85, 0.5], scale: [1, 1.12, 1] }}
transition={{ repeat: Infinity, duration: 4, ease: "easeInOut" }}
/>
)}
<p className="relative text-xs font-black uppercase tracking-widest text-[#C9A2FF]">
AI-powered with ProductLLM
</p>
<div className="relative mt-5 flex items-end gap-2">
<span className="font-display font-black leading-none text-white" style={{ fontSize: "clamp(44px, 6vw, 68px)" }}>
~2
</span>
<span className="mb-2 text-lg font-bold text-white/70">min / store</span>
</div>
<Bar value={14} brand />
<motion.ul
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.1, delayChildren: 0.3 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
className="relative mt-8 flex flex-col gap-4"
>
{AI_POINTS.map((p) => (
<Point key={p.label} icon={p.icon} label={p.label} brand />
))}
</motion.ul>
</motion.div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,189 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const STAGES = [
{ icon: "photo_camera", label: "Shelf Scan" },
{ icon: "barcode_scanner", label: "Barcode Detection" },
{ icon: "center_focus_strong", label: "Recognition" },
{ icon: "category", label: "Category" },
{ icon: "nutrition", label: "Nutrition" },
{ icon: "science", label: "Ingredients" },
{ icon: "sell", label: "Pricing" },
{ icon: "inventory_2", label: "Inventory" },
{ icon: "check_circle", label: "Ready", done: true },
];
const nodeVariant = {
hidden: { opacity: 0, scale: 0.7, y: 12 },
visible: { opacity: 1, scale: 1, y: 0, transition: { duration: 0.45, ease: EASE } },
};
function StageNode({
stage,
index,
reduce,
size = "md",
}: {
stage: (typeof STAGES)[number];
index: number;
reduce: boolean | null;
size?: "md" | "sm";
}) {
const dim = size === "md" ? "h-[68px] w-[68px]" : "h-14 w-14";
return (
<motion.div variants={nodeVariant} className="relative">
{/* light-up glow */}
<div
aria-hidden="true"
className={`pointer-events-none absolute inset-0 rounded-2xl blur-xl ${
stage.done ? "opacity-90" : "opacity-50"
}`}
style={{
background: stage.done
? "radial-gradient(circle, rgba(34,197,94,0.5), rgba(34,197,94,0) 70%)"
: "radial-gradient(circle, rgba(124,58,237,0.35), rgba(124,58,237,0) 70%)",
}}
/>
{stage.done && !reduce && (
<motion.span
aria-hidden="true"
className="absolute inset-0 rounded-2xl border-2 border-[#22C55E]"
animate={{ scale: [1, 1.25], opacity: [0.6, 0] }}
transition={{ repeat: Infinity, duration: 1.9, ease: "easeOut" }}
/>
)}
<div
className={`relative flex ${dim} items-center justify-center rounded-2xl border ${
stage.done
? "border-transparent bg-gradient-to-br from-[#22C55E] to-[#16A34A] text-white shadow-[0_14px_34px_rgba(34,197,94,0.4)]"
: "border-[#6330D6]/12 bg-white text-[#6330D6] shadow-[0_10px_26px_rgba(99,48,214,0.12)]"
}`}
>
<MaterialIcon icon={stage.icon} size={size === "md" ? 30 : 26} />
<span
className={`absolute -top-2 -right-2 flex h-6 w-6 items-center justify-center rounded-full text-[11px] font-black ${
stage.done ? "bg-white text-[#16A34A]" : "bg-[#6330D6] text-white"
} shadow-md`}
>
{index + 1}
</span>
</div>
</motion.div>
);
}
export function ProductLLMPipeline() {
const reduce = useReducedMotion();
return (
<section className="section bg-white w-full border-b border-[#6330D6]/10 overflow-hidden">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[720px] mx-auto"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
>
ProductLLM
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
From a shelf photo to a live catalog
</motion.h2>
<motion.p
variants={fadeUp}
className="text-[#5E5866] mt-4"
style={{ fontSize: 18, lineHeight: 1.65 }}
>
One scan runs the full pipeline no manual data entry, no
spreadsheets.
</motion.p>
</motion.div>
{/* ── Desktop: horizontal processing rail ── */}
<motion.div
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.12 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
className="relative mt-16 hidden lg:block"
>
{/* base rail */}
<div className="absolute left-[5%] right-[5%] top-[34px] h-1 rounded-full bg-[#6330D6]/10" />
{/* filling gradient */}
<motion.div
className="absolute left-[5%] top-[34px] h-1 origin-left rounded-full bg-gradient-to-r from-[#7C3AED] via-[#6330D6] to-[#22C55E]"
style={{ width: "90%" }}
variants={{
hidden: { scaleX: 0 },
visible: { scaleX: 1, transition: { duration: 1.8, ease: EASE } },
}}
/>
{/* travelling data pulse */}
{!reduce && (
<motion.span
aria-hidden="true"
className="absolute top-[30px] left-[5%] h-2.5 w-2.5 rounded-full bg-white shadow-[0_0_12px_3px_rgba(124,58,237,0.7)]"
animate={{ left: ["5%", "95%"], opacity: [0, 1, 1, 0] }}
transition={{ repeat: Infinity, duration: 3, ease: "easeInOut" }}
/>
)}
<div className="relative grid grid-cols-9">
{STAGES.map((stage, i) => (
<div key={stage.label} className="flex flex-col items-center px-1 text-center">
<StageNode stage={stage} index={i} reduce={reduce} />
<p className="mt-4 text-[13px] font-bold leading-tight text-[#16001D]">
{stage.label}
</p>
</div>
))}
</div>
</motion.div>
{/* ── Mobile / tablet: vertical pipeline ── */}
<motion.div
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.1 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
className="relative mx-auto mt-12 max-w-md lg:hidden"
>
{/* vertical rail */}
<div className="absolute left-7 top-8 bottom-8 w-1 -translate-x-1/2 rounded-full bg-[#6330D6]/10" />
<motion.div
className="absolute left-7 top-8 w-1 -translate-x-1/2 origin-top rounded-full bg-gradient-to-b from-[#7C3AED] via-[#6330D6] to-[#22C55E]"
style={{ bottom: "2rem" }}
variants={{
hidden: { scaleY: 0 },
visible: { scaleY: 1, transition: { duration: 1.6, ease: EASE } },
}}
/>
<div className="flex flex-col gap-4">
{STAGES.map((stage, i) => (
<div key={stage.label} className="flex items-center gap-5">
<StageNode stage={stage} index={i} reduce={reduce} size="sm" />
<p className="font-display font-black text-[#16001D] text-base leading-tight">
{stage.label}
</p>
</div>
))}
</div>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,119 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import { motion, useReducedMotion } from "framer-motion";
import { HeroLogo } from "@/components/heroes/HeroLogo";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const badgeVariant = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, delay: 0.9, ease: EASE } },
};
const headingVariant = {
hidden: { opacity: 0, y: 24 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 1.0, ease: EASE } },
};
const descVariant = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.55, delay: 1.1, ease: EASE } },
};
const ctaVariant = {
hidden: { opacity: 0, y: 16 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, delay: 1.2, ease: EASE } },
};
export function WhyNearleHero() {
const shouldReduce = useReducedMotion();
return (
<section className="w-full flex flex-col select-none bg-white">
<div className="relative overflow-hidden border-b border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D]">
{/* Ambient background image */}
<motion.div
aria-hidden="true"
className="pointer-events-none absolute inset-0 z-0"
initial={shouldReduce ? undefined : { opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1.4, ease: "easeOut" }}
>
<Image
src="/why-nearle.png"
alt=""
fill
sizes="100vw"
className="absolute inset-0 h-full w-full object-cover object-center"
priority
fetchPriority="high"
/>
{/* Dark gradient overlay to make text pop while keeping the image vibrant */}
<div className="absolute inset-0 bg-gradient-to-b from-[#1A0322]/80 via-black/45 to-[#0A000D]/90" />
</motion.div>
<div className="page-container relative z-10 flex min-h-[520px] items-center justify-center px-6 pt-20 pb-14 sm:px-8 sm:pt-24 md:min-h-[76vh] md:py-16">
<div className="relative z-20 flex w-full max-w-[880px] flex-col items-center text-center px-2 md:px-8">
<HeroLogo />
<motion.span
variants={badgeVariant}
initial="hidden"
animate="visible"
className="inline-flex items-center gap-1.5 px-4 py-1.5 rounded-full text-xs font-bold tracking-widest bg-white text-[#6330D6] shadow-md uppercase mb-6 w-fit"
>
What is AI Commerce?
</motion.span>
<motion.h1
variants={headingVariant}
initial="hidden"
animate="visible"
className="font-display text-4xl sm:text-5xl lg:text-[52px] xl:text-[60px] font-extrabold text-white leading-[1.08] tracking-tight [text-shadow:0_4px_32px_rgba(0,0,0,0.5)]"
>
AI Commerce Starts
<br />
Where <span className="text-[#C9A2FF]">Quick Commerce</span>
<br />
Stops.
</motion.h1>
<motion.p
variants={descVariant}
initial="hidden"
animate="visible"
className="max-w-[660px] text-lg md:text-xl text-white/90 font-medium mt-6 leading-[1.6]"
>
Neighborhood stores already have the trust, the customers, and the
location. Nearle adds the AI, the digital storefront, and the
ordering so local commerce finally scales without losing what
makes it local.
</motion.p>
<motion.div
variants={ctaVariant}
initial="hidden"
animate="visible"
className="mt-9"
>
<Link
href="/platform"
className="group inline-flex items-center gap-2 rounded-full bg-white px-8 py-4 text-base font-black text-[#16001D] shadow-[0_16px_30px_rgba(0,0,0,0.35)] transition-transform duration-200 hover:scale-[1.03] active:scale-95"
>
Learn About Our Platform
<MaterialIcon
icon="arrow_forward"
size={20}
className="transition-transform duration-200 group-hover:translate-x-0.5"
/>
</Link>
</motion.div>
</div>
</div>
</div>
</section>
);
}