Update website layout, styles, and story components

This commit is contained in:
R-Bharathraj
2026-07-21 17:48:28 +05:30
parent e41094a6fd
commit aca42a3213
7 changed files with 736 additions and 263 deletions

View File

@@ -99,6 +99,35 @@
} }
} }
/* ─────────────────────────────────────────────────────────────
Animated icons (emoji replacements) — solid black glyphs with
no border/background, each looping an idle motion. CSS-driven
so the ~120 icons on a page stay cheap.
───────────────────────────────────────────────────────────── */
.ico {
display: inline-flex;
align-items: center;
vertical-align: -0.18em;
flex-shrink: 0;
}
.ico svg { display: block; }
@keyframes icoBob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-2.5px); } }
@keyframes icoSwing { 0%, 100% { transform: rotate(-10deg); } 50% { transform: rotate(10deg); } }
@keyframes icoSpin { to { transform: rotate(360deg); } }
@keyframes icoPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(0.84); } }
@keyframes icoRock { 0%, 100% { transform: rotate(-8deg); } 50% { transform: rotate(8deg); } }
.ico-bob svg { animation: icoBob 1.15s ease-in-out infinite; }
.ico-swing svg { animation: icoSwing 1.7s ease-in-out infinite; transform-origin: 50% 10%; }
.ico-spin svg { animation: icoSpin 6s linear infinite; transform-origin: 50% 50%; }
.ico-pulse svg { animation: icoPulse 1.45s ease-in-out infinite; transform-origin: 50% 50%; }
.ico-rock svg { animation: icoRock 1.55s ease-in-out infinite; transform-origin: 50% 85%; }
@media (prefers-reduced-motion: reduce) {
.ico svg { animation: none !important; }
}
/* Custom Scrollbar — neobrutalist, on-theme (no track border) */ /* Custom Scrollbar — neobrutalist, on-theme (no track border) */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 14px; width: 14px;
@@ -275,18 +304,16 @@ h1, h2, h3, h4, h5, h6 {
border-radius: 4px !important; border-radius: 4px !important;
} }
/* Mobile view media queries for The Vision and For You sections */ /* Mobile view media queries for The Vision section */
@media (max-width: 640px) { @media (max-width: 640px) {
#the-vision, #the-vision {
#for-you {
padding-left: 1.25rem !important; padding-left: 1.25rem !important;
padding-right: 1.25rem !important; padding-right: 1.25rem !important;
padding-top: 4rem !important; padding-top: 4rem !important;
padding-bottom: 4rem !important; padding-bottom: 4rem !important;
} }
#the-vision h2, #the-vision h2 {
#for-you h2 {
font-size: 2.5rem !important; font-size: 2.5rem !important;
line-height: 1.05 !important; line-height: 1.05 !important;
letter-spacing: -0.02em !important; letter-spacing: -0.02em !important;
@@ -295,8 +322,7 @@ h1, h2, h3, h4, h5, h6 {
} }
/* Optimize card spacing and shadow sizes for mobile neobrutalism style */ /* Optimize card spacing and shadow sizes for mobile neobrutalism style */
#the-vision .nb, #the-vision .nb {
#for-you .nb {
padding: 1rem !important; padding: 1rem !important;
font-size: 0.95rem !important; font-size: 0.95rem !important;
box-shadow: 4px 4px 0 0 #0a0a0a !important; box-shadow: 4px 4px 0 0 #0a0a0a !important;
@@ -304,8 +330,7 @@ h1, h2, h3, h4, h5, h6 {
} }
/* Decrease vertical spacing inside grid for better layout flow on mobile */ /* Decrease vertical spacing inside grid for better layout flow on mobile */
#the-vision .grid, #the-vision .grid {
#for-you .grid {
gap: 1.25rem !important; gap: 1.25rem !important;
} }
} }

View File

@@ -13,8 +13,6 @@ import {
TheLoop, TheLoop,
} from "@/components/story/SectionsA"; } from "@/components/story/SectionsA";
import { import {
ForYou,
ForBusiness,
WhyNow, WhyNow,
HowItWorks, HowItWorks,
OldVsNew, OldVsNew,
@@ -30,15 +28,13 @@ export default function Home() {
<Header /> <Header />
<Intro /> <Intro />
<TheWhy /> <TheWhy />
<Difference />
<TheProblem /> <TheProblem />
<TheShift /> <TheShift />
<MeetLyts /> <MeetLyts />
<OneDay /> <OneDay />
<Feelings /> <Feelings />
<Difference />
<TheLoop /> <TheLoop />
<ForYou />
<ForBusiness />
<WhyNow /> <WhyNow />
<HowItWorks /> <HowItWorks />
<OldVsNew /> <OldVsNew />

View File

@@ -4,6 +4,7 @@ import { useState, useEffect, useRef } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image'; import Image from 'next/image';
import { gsap, useGSAP, syncLenisScrollTrigger } from '../story/gsapkit'; import { gsap, useGSAP, syncLenisScrollTrigger } from '../story/gsapkit';
import { Ico } from '../story/Ico';
const INK = '#0a0a0a'; const INK = '#0a0a0a';
const BLUE = '#2F6BFF'; const BLUE = '#2F6BFF';
@@ -13,18 +14,18 @@ const NAV = [
title: 'Earn', title: 'Earn',
color: '#C8FF37', // lime color: '#C8FF37', // lime
links: [ links: [
{ label: 'Walk to Earn', href: '#for-you' }, { label: 'Walk to Earn', href: '#how-it-works' },
{ label: 'Play & Win', href: '#for-you' }, { label: 'Play & Win', href: '#how-it-works' },
{ label: 'Refer Friends', href: '#for-you' }, { label: 'Refer Friends', href: '#how-it-works' },
], ],
}, },
{ {
title: 'Platform', title: 'Platform',
color: '#FF2E88', // pink color: '#FF2E88', // pink
links: [ links: [
{ label: 'For You', href: '#for-you' }, { label: 'Why Now', href: '#why-now' },
{ label: 'For Business', href: '#for-business' },
{ label: 'How It Works', href: '#how-it-works' }, { label: 'How It Works', href: '#how-it-works' },
{ label: 'The Loop', href: '#the-loop' },
], ],
}, },
{ {
@@ -33,7 +34,7 @@ const NAV = [
links: [ links: [
{ label: 'The Vision', href: '#the-vision' }, { label: 'The Vision', href: '#the-vision' },
{ label: 'The Why', href: '#the-why' }, { label: 'The Why', href: '#the-why' },
{ label: 'Contact Us', href: '#for-business' }, { label: 'Our Belief', href: '#our-belief' },
], ],
}, },
]; ];
@@ -79,10 +80,111 @@ export default function Footer() {
syncLenisScrollTrigger(); syncLenisScrollTrigger();
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// Infinite marquee ticker (two identical halves → seamless).
if (!reduce) { if (!reduce) {
/* Continuous continuous animations */
gsap.to('.foot-ticker-track', { xPercent: -50, duration: 22, ease: 'none', repeat: -1 }); gsap.to('.foot-ticker-track', { xPercent: -50, duration: 22, ease: 'none', repeat: -1 });
gsap.to('.foot-cta-star', { rotate: 360, duration: 9, ease: 'none', repeat: -1 }); gsap.to('.foot-cta-star-spin', { rotate: 360, duration: 9, ease: 'none', repeat: -1 });
/* ── 3D Sticker Fold-Out & Stamp Burst Reveal for Footer CTA Banner ── */
gsap.set('.lu-cta', {
transformStyle: 'preserve-3d',
transformOrigin: '50% 100%',
rotationX: 35,
rotationY: -10,
scale: 0.85,
y: 70,
autoAlpha: 0,
boxShadow: '0 40px 80px rgba(0,0,0,0.4)',
});
gsap.set('.foot-cta-star', { scale: 0, rotation: -180, autoAlpha: 0 });
gsap.set('.lu-cta-pill', { y: -25, autoAlpha: 0 });
gsap.set('.lu-cta-heading', { y: 30, rotationX: -40, autoAlpha: 0 });
gsap.set('.lu-cta-btn', { scale: 0.6, rotationZ: -12, autoAlpha: 0 });
gsap.set('.lu-columns > *', { y: 40, autoAlpha: 0 });
const tl = gsap.timeline({
scrollTrigger: {
trigger: '.lu-cta-wrap',
start: 'top 78%',
once: true,
},
defaults: { ease: 'power3.out' },
});
/* Phase 1: Card Unfolds from 3D Stack */
tl.to('.lu-cta', {
rotationX: 0,
rotationY: 0,
scale: 1,
y: 0,
autoAlpha: 1,
boxShadow: '8px 8px 0 0 #0a0a0a',
duration: 0.85,
ease: 'back.out(1.5)',
onComplete: () => {
gsap.set('.lu-cta', { clearProps: 'transform,transformStyle' });
},
})
/* Phase 2: Pink Star Pops Out from Top Corner */
.to(
'.foot-cta-star',
{
scale: 1,
rotation: 0,
autoAlpha: 1,
duration: 0.5,
ease: 'back.out(2)',
},
'-=0.4'
)
/* Phase 3: Purple Pill Tag Drops Down */
.to(
'.lu-cta-pill',
{
y: 0,
autoAlpha: 1,
duration: 0.4,
ease: 'back.out(1.8)',
},
'-=0.3'
)
/* Phase 4: Heading 3D Unrolls */
.to(
'.lu-cta-heading',
{
y: 0,
rotationX: 0,
autoAlpha: 1,
duration: 0.55,
ease: 'power4.out',
},
'-=0.3'
)
/* Phase 5: Yellow Download Button Stamps On */
.to(
'.lu-cta-btn',
{
scale: 1,
rotationZ: 0,
autoAlpha: 1,
duration: 0.5,
ease: 'back.out(2.2)',
},
'-=0.35'
)
/* Phase 6: Footer columns cascade in */
.to(
'.lu-columns > *',
{
y: 0,
autoAlpha: 1,
duration: 0.6,
stagger: 0.1,
ease: 'power3.out',
},
'-=0.2'
);
} }
}, },
{ scope: rootRef }, { scope: rootRef },
@@ -120,8 +222,8 @@ export default function Footer() {
</div> </div>
</div> </div>
{/* ── BIG CTA STICKER CARD ── */} {/* ── BIG CTA STICKER CARD (3D Fold-Out Reveal) ── */}
<div className="lu-cta-wrap" style={{ padding: '56px 48px 40px' }}> <div className="lu-cta-wrap" style={{ padding: '56px 48px 40px', perspective: 1200 }}>
<div className="lu-cta" style={{ <div className="lu-cta" style={{
position: 'relative', position: 'relative',
background: BLUE, background: BLUE,
@@ -136,13 +238,13 @@ export default function Footer() {
}}> }}>
{/* rotating star sticker */} {/* rotating star sticker */}
<span className="foot-cta-star" aria-hidden style={{ position: 'absolute', top: -20, right: 30, willChange: 'transform' }}> <span className="foot-cta-star" aria-hidden style={{ position: 'absolute', top: -20, right: 30, willChange: 'transform' }}>
<span style={{ display: 'inline-flex', background: '#FF2E88', border: `3px solid ${INK}`, borderRadius: '50%', padding: 10, boxShadow: `3px 3px 0 0 ${INK}` }}> <span className="foot-cta-star-spin" style={{ display: 'inline-flex', background: '#FF2E88', border: `3px solid ${INK}`, borderRadius: '50%', padding: 10, boxShadow: `3px 3px 0 0 ${INK}` }}>
<Star size={22} color="#fff" /> <Star size={22} color="#fff" />
</span> </span>
</span> </span>
<div style={{ maxWidth: 640 }}> <div style={{ maxWidth: 640 }}>
<span style={{ display: 'inline-block', background: '#8B3DFF', color: '#fff', border: `3px solid ${INK}`, borderRadius: 10, padding: '5px 12px', fontSize: 11, fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.14em', marginBottom: 18, boxShadow: `3px 3px 0 0 ${INK}` }}> <span className="lu-cta-pill" style={{ display: 'inline-block', background: '#8B3DFF', color: '#fff', border: `3px solid ${INK}`, borderRadius: 10, padding: '5px 12px', fontSize: 11, fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.14em', marginBottom: 18, boxShadow: `3px 3px 0 0 ${INK}` }}>
Earn. Spend. Live More. Earn. Spend. Live More.
</span> </span>
<h2 className="lu-cta-heading" style={{ fontSize: 'clamp(32px, 4.8vw, 62px)', fontWeight: 800, letterSpacing: '-0.03em', lineHeight: 0.96, margin: 0 }}> <h2 className="lu-cta-heading" style={{ fontSize: 'clamp(32px, 4.8vw, 62px)', fontWeight: 800, letterSpacing: '-0.03em', lineHeight: 0.96, margin: 0 }}>
@@ -154,7 +256,7 @@ export default function Footer() {
</h2> </h2>
</div> </div>
<Link href="#for-business" className="lu-cta-btn" style={{ <Link href="#how-it-works" className="lu-cta-btn" style={{
display: 'inline-flex', alignItems: 'center', gap: 10, display: 'inline-flex', alignItems: 'center', gap: 10,
padding: '18px 36px', background: '#FFC72C', color: INK, padding: '18px 36px', background: '#FFC72C', color: INK,
fontSize: 17, fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.03em', fontSize: 17, fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.03em',
@@ -231,7 +333,7 @@ export default function Footer() {
}}> }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontWeight: 600 }}> <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontWeight: 600 }}>
© {new Date().getFullYear()} LytsUp. Made with © {new Date().getFullYear()} LytsUp. Made with
<span style={{ color: '#FF2E88' }}></span> for Gen Z. <Ico e="♥" size={14} /> for Gen Z.
</span> </span>
<div style={{ display: 'flex', gap: 20 }}> <div style={{ display: 'flex', gap: 20 }}>
{[['Terms', '#'], ['Privacy', '#'], ['Cookies', '#']].map(([l, h]) => ( {[['Terms', '#'], ['Privacy', '#'], ['Cookies', '#']].map(([l, h]) => (

View File

@@ -5,32 +5,36 @@ import Link from 'next/link';
import Image from 'next/image'; import Image from 'next/image';
const INK = '#0a0a0a'; const INK = '#0a0a0a';
const CREAM = '#FFFDF5';
const GOLD = '#FFC72C'; const GOLD = '#FFC72C';
const CREAM = '#FFFDF5';
/* Scroll distance (px) before the bar solidifies into the sticky sticker. */
const STICK_AFTER = 500;
const NAV_LINKS = [ const NAV_LINKS = [
{ label: 'Earn', href: '#for-you', color: '#C8FF37' }, // lime { label: 'Earn', href: '#how-it-works', color: '#C8FF37' }, // lime
{ label: 'Spend', href: '#difference', color: '#FF2E88' }, // pink { label: 'Spend', href: '#difference', color: '#FF2E88' }, // pink
{ label: 'Live', href: '#the-vision', color: '#8B3DFF' }, // purple { label: 'Live', href: '#the-vision', color: '#8B3DFF' }, // purple
{ label: 'For Business', href: '#for-business', color: GOLD }, { label: 'Why Now', href: '#why-now', color: GOLD },
]; ];
const MOBILE_QUERY = '(max-width: 1023px)';
export default function Header() { export default function Header() {
const [mobileOpen, setMobileOpen] = useState(false); const [mobileOpen, setMobileOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const [hidden, setHidden] = useState(false); const [hidden, setHidden] = useState(false);
const [stuck, setStuck] = useState(false);
const [activeHash, setActiveHash] = useState(''); const [activeHash, setActiveHash] = useState('');
const mobileMenuRef = useRef<HTMLDivElement>(null); const mobileMenuRef = useRef<HTMLDivElement>(null);
const lastScrollYRef = useRef(0); const lastScrollYRef = useRef(0);
// Deepen the header shadow once scrolled; hide on scroll-down, reveal on up. /* Sticky behaviour (same model as the loyaly header):
· above STICK_AFTER → `stuck = false`, invisible chrome over the hero
· past STICK_AFTER → `stuck = true`, solidifies into the cream sticker bar
· scroll down → hide, scroll up → reveal */
useEffect(() => { useEffect(() => {
lastScrollYRef.current = window.scrollY; lastScrollYRef.current = window.scrollY;
const onScroll = () => { const onScroll = () => {
const y = window.scrollY; const y = window.scrollY;
setScrolled(y > 50); setStuck(y > STICK_AFTER);
const lastY = lastScrollYRef.current; const lastY = lastScrollYRef.current;
if (y <= 80) setHidden(false); if (y <= 80) setHidden(false);
else if (y > lastY + 4) setHidden(true); else if (y > lastY + 4) setHidden(true);
@@ -82,15 +86,6 @@ export default function Header() {
return () => { document.body.style.overflow = ''; }; return () => { document.body.style.overflow = ''; };
}, [mobileOpen]); }, [mobileOpen]);
const [isMobileViewport, setIsMobileViewport] = useState(false);
useEffect(() => {
const mql = window.matchMedia(MOBILE_QUERY);
setIsMobileViewport(mql.matches);
const update = () => setIsMobileViewport(mql.matches);
mql.addEventListener('change', update);
return () => mql.removeEventListener('change', update);
}, []);
const isActive = (href: string) => href === activeHash; const isActive = (href: string) => href === activeHash;
return ( return (
@@ -194,7 +189,7 @@ export default function Header() {
</div> </div>
<Link <Link
href="#for-business" href="#how-it-works"
onClick={() => setMobileOpen(false)} onClick={() => setMobileOpen(false)}
className="font-sora" className="font-sora"
style={{ style={{
@@ -219,22 +214,21 @@ export default function Header() {
{/* ——— Header: cream neobrutalist bar, loyaly layout ——— */} {/* ——— Header: cream neobrutalist bar, loyaly layout ——— */}
<header <header
className={`header-bar${stuck ? ' is-stuck' : ''}`}
style={{ style={{
position: 'fixed', position: 'fixed',
top: isMobileViewport ? '16px' : 0,
left: '20px',
right: '20px',
width: 'auto', width: 'auto',
zIndex: 99999999, zIndex: 99999999,
background: CREAM, /* ── two-state sticky skin ──
border: `3px solid ${INK}`, top of page → transparent chrome · scrolled → solid cream sticker.
borderTopLeftRadius: isMobileViewport ? '22px' : '0px', The border stays present but transparent so nothing shifts on flip.
borderTopRightRadius: isMobileViewport ? '22px' : '0px', Geometry (top/left/right/radius) lives in the media queries below. */
borderBottomLeftRadius: '22px', background: stuck ? CREAM : 'transparent',
borderBottomRightRadius: '22px', border: `3px solid ${stuck ? INK : 'transparent'}`,
boxShadow: scrolled ? `0 9px 0 0 ${INK}` : `0 6px 0 0 ${INK}`, boxShadow: stuck ? `0 6px 0 0 ${INK}` : 'none',
transform: hidden ? 'translateY(-160%)' : 'translateY(0)', transform: hidden ? 'translateY(-160%)' : 'translateY(0)',
transition: 'transform 0.35s cubic-bezier(0.4,0,0.2,1), box-shadow 0.25s ease', transition:
'transform 0.35s cubic-bezier(0.4,0,0.2,1), background 0.28s ease, border-color 0.28s ease, box-shadow 0.28s ease',
}} }}
> >
<div className="header-inner" style={{ <div className="header-inner" style={{
@@ -249,14 +243,30 @@ export default function Header() {
}}> }}>
{/* Logo */} {/* Logo */}
<div style={{ flexShrink: 0, flex: 1, minWidth: 120, display: 'flex', alignItems: 'center' }}> <div style={{ flexShrink: 0, flex: 1, minWidth: 120, display: 'flex', alignItems: 'center' }}>
<Link href="#intro" aria-label="LytsUp home" style={{ display: 'inline-flex', alignItems: 'center', textDecoration: 'none' }}> {/* Light sticker chip behind the mark — the "lyts" wordmark is dark
navy, so it vanishes on the dark hero without a light backing. */}
<Link
href="#intro"
aria-label="LytsUp home"
className="header-logo-chip"
style={{
display: 'inline-flex',
alignItems: 'center',
textDecoration: 'none',
background: CREAM,
border: `3px solid ${INK}`,
borderRadius: 14,
padding: '8px 14px',
boxShadow: `3px 3px 0 0 ${INK}`,
}}
>
<Image <Image
src="/images/lytsup_app.png" src="/images/lytsup_app.png"
alt="LytsUp" alt="LytsUp"
width={2048} width={2048}
height={554} height={554}
className="header-logo-img" className="header-logo-img"
style={{ objectFit: 'contain', height: 40, width: 'auto' }} style={{ objectFit: 'contain', height: 34, width: 'auto' }}
priority priority
/> />
</Link> </Link>
@@ -323,7 +333,7 @@ export default function Header() {
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flex: 1, justifyContent: 'flex-end' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '12px', flex: 1, justifyContent: 'flex-end' }}>
{/* Download Now — chunky gold sticker button */} {/* Download Now — chunky gold sticker button */}
<Link <Link
href="#for-business" href="#how-it-works"
className="header-cta desktop-nav font-sora" className="header-cta desktop-nav font-sora"
style={{ style={{
display: 'inline-flex', display: 'inline-flex',
@@ -368,11 +378,60 @@ export default function Header() {
@media (max-width: 1023px) { @media (max-width: 1023px) {
.desktop-nav { display: none !important; } .desktop-nav { display: none !important; }
.menu-trigger { display: flex !important; } .menu-trigger { display: flex !important; }
.header-inner { height: 62px !important; } .header-inner { height: 58px !important; padding: 0 12px !important; }
.header-logo-img { height: 28px !important; } .header-logo-img { height: 26px !important; }
} }
@media (max-width: 600px) { /* ─────────────────────────────────────────────────────────────
header { left: 10px !important; right: 10px !important; top: 10px !important; } FLOATING CARD MOBILE HEADER & STICKY GEOMETRY
───────────────────────────────────────────────────────────── */
.header-bar {
top: 20px;
left: 20px;
right: 20px;
border-radius: 22px;
}
/* Once it sticks on desktop, drop the top margin so it sits flush */
@media (min-width: 769px) {
.header-bar.is-stuck {
top: 0;
border-top: none !important;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
}
/* ── Mobile Floating Card Header (Media Query) ── */
@media (max-width: 768px) {
.header-bar {
top: 12px !important;
left: 12px !important;
right: 12px !important;
border-radius: 20px !important;
background: ${CREAM} !important;
border: 3px solid ${INK} !important;
box-shadow: 4px 4px 0 0 ${INK} !important;
}
.header-bar.is-stuck {
top: 10px !important;
border-top: 3px solid ${INK} !important;
border-top-left-radius: 20px !important;
border-top-right-radius: 20px !important;
box-shadow: 5px 5px 0 0 ${INK} !important;
}
.header-logo-chip {
padding: 5px 10px !important;
border-radius: 12px !important;
box-shadow: 2px 2px 0 0 ${INK} !important;
}
.header-logo-img {
height: 24px !important;
}
.menu-trigger {
width: 38px !important;
height: 38px !important;
border-radius: 10px !important;
}
} }
@media (min-width: 1024px) and (max-width: 1280px) { @media (min-width: 1024px) and (max-width: 1280px) {
.header-nav-row { gap: 7px !important; } .header-nav-row { gap: 7px !important; }

View File

@@ -0,0 +1,99 @@
"use client";
/* Site-wide emoji → animated icon replacement.
Every emoji used across the story sections maps to a solid black Lucide
glyph with no border and no background, plus a looping idle motion that
suits its meaning. Motion is pure CSS (see `globals.css`) so ~100 icons on
a page cost nothing, and it's disabled under prefers-reduced-motion.
Two entry points:
<Ico e="🚶" /> — drop an icon into JSX where an emoji used to sit
{withIcons(str)} — render a string that has emoji baked into it
(used for copy that lives inside const arrays) */
import type { ReactNode } from "react";
import {
Heart, Flame, Gift, Mail, Clapperboard, Users, Sparkles, Zap, X, Check,
CircleAlert, Tv, ShoppingCart, CreditCard, Smartphone, Gamepad2, Footprints,
Coffee, MapPin, Store, Sprout, Trophy, TreePalm, MessageCircle, Laptop,
Rocket, Recycle, Sunrise, Globe, Megaphone, Hand, Handshake, Folder, Repeat,
Brain, TrendingDown, Camera, FerrisWheel, Ticket, ShoppingBag, PartyPopper,
Tag, EyeOff, Rainbow, Wallet, Fingerprint, Star, Clock,
type LucideIcon,
} from "lucide-react";
/* motion kinds — matched to CSS keyframes in globals.css */
type Motion = "bob" | "swing" | "spin" | "pulse" | "rock";
const MAP: Record<string, [LucideIcon, Motion]> = {
// hearts / feelings
"♡": [Heart, "pulse"], "♥": [Heart, "pulse"], "💗": [Heart, "pulse"],
"💞": [Heart, "pulse"], "🤍": [Heart, "pulse"], "💚": [Heart, "pulse"],
"🫂": [Users, "rock"], "🤲": [Hand, "rock"],
// sparkle / energy
"✨": [Sparkles, "pulse"], "✦": [Sparkles, "pulse"], "⚡": [Zap, "pulse"],
"🔥": [Flame, "bob"], "🌈": [Rainbow, "pulse"], "🚀": [Rocket, "bob"],
// marks
"✗": [X, "pulse"], "✓": [Check, "pulse"], "❗": [CircleAlert, "pulse"],
// people / social
"👯": [Users, "rock"], "👥": [Users, "rock"], "🙋": [Hand, "swing"],
"🤝": [Handshake, "rock"], "💬": [MessageCircle, "bob"], "📢": [Megaphone, "swing"],
"🤳": [Camera, "pulse"], "🪞": [Fingerprint, "pulse"], "🧠": [Brain, "pulse"],
// movement / places
"🚶": [Footprints, "bob"], "👟": [Footprints, "bob"], "📍": [MapPin, "bob"],
"🏪": [Store, "bob"], "🌴": [TreePalm, "swing"], "🌱": [Sprout, "bob"],
"🌅": [Sunrise, "bob"], "🌐": [Globe, "spin"], "☕": [Coffee, "bob"],
// commerce
"🛒": [ShoppingCart, "bob"], "🛍️": [ShoppingBag, "swing"], "🛍": [ShoppingBag, "swing"],
"💳": [CreditCard, "bob"], "💰": [Wallet, "bob"], "🏷️": [Tag, "swing"], "🏷": [Tag, "swing"],
"🎁": [Gift, "bob"], "📉": [TrendingDown, "bob"],
// media / tech
"📺": [Tv, "pulse"], "🎬": [Clapperboard, "rock"], "📱": [Smartphone, "bob"],
"💻": [Laptop, "bob"], "🎮": [Gamepad2, "rock"], "💌": [Mail, "bob"],
"📁": [Folder, "bob"], "🔁": [Repeat, "spin"], "♻️": [Recycle, "spin"], "♻": [Recycle, "spin"],
// fun / rewards
"🏆": [Trophy, "pulse"], "🎉": [PartyPopper, "rock"], "🎡": [FerrisWheel, "spin"],
"🎟️": [Ticket, "swing"], "🎟": [Ticket, "swing"], "🫥": [EyeOff, "pulse"],
"⏰": [Clock, "rock"], "⏳": [Clock, "rock"], "⭐": [Star, "pulse"], "🌟": [Star, "pulse"],
};
/** Matches any emoji we know how to swap (longest keys first for ZWJ/VS16). */
const EMOJI_RE = new RegExp(
`(${Object.keys(MAP)
.sort((a, b) => b.length - a.length)
.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
.join("|")})`,
"gu",
);
export function Ico({
e,
size = 16,
className = "",
}: {
e: string;
size?: number;
className?: string;
}) {
const hit = MAP[e] ?? MAP[e.replace(//g, "")];
if (!hit) return null;
const [Glyph, motion] = hit;
return (
<span aria-hidden className={`ico ico-${motion} ${className}`}>
<Glyph size={size} strokeWidth={2.6} color="#0a0a0a" />
</span>
);
}
/** Swap every known emoji inside a plain string for an animated icon. */
export function withIcons(text: string, size = 16): ReactNode {
const parts = text.split(EMOJI_RE).filter((p) => p !== "");
return parts.map((p, i) =>
MAP[p] || MAP[p.replace(//g, "")] ? (
<Ico key={i} e={p} size={size} />
) : (
<span key={i}>{p}</span>
),
);
}

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { useRef } from "react"; import { useRef } from "react";
import { motion } from "framer-motion";
import { import {
Section, Section,
Pill, Pill,
@@ -20,6 +21,7 @@ import {
usePointerParallax, usePointerParallax,
MaskWords, MaskWords,
} from "./gsapkit"; } from "./gsapkit";
import { Ico, withIcons } from "./Ico";
/* 01 — INTRO / HERO /* 01 — INTRO / HERO
GSAP: masked line-reveal on load · scroll-scrubbed depth parallax on the GSAP: masked line-reveal on load · scroll-scrubbed depth parallax on the
@@ -150,7 +152,7 @@ export function Intro() {
<div className="mt-9 flex flex-wrap items-center gap-4"> <div className="mt-9 flex flex-wrap items-center gap-4">
<Magnetic className="hero-cta"> <Magnetic className="hero-cta">
<a <a
href="#for-you" href="#how-it-works"
className="nb nb-hover inline-flex items-center gap-2 rounded-full bg-[#FFC72C] px-7 py-3.5 font-sora text-base font-extrabold uppercase tracking-wide text-[#0a0a0a]" className="nb nb-hover inline-flex items-center gap-2 rounded-full bg-[#FFC72C] px-7 py-3.5 font-sora text-base font-extrabold uppercase tracking-wide text-[#0a0a0a]"
> >
get started get started
@@ -185,7 +187,7 @@ export function Intro() {
<div className="text-xs font-bold uppercase tracking-widest text-[#FF2E88]">psst</div> <div className="text-xs font-bold uppercase tracking-widest text-[#FF2E88]">psst</div>
make everyday make everyday
<br /> <br />
count. <span className="text-[#FF2E88]"></span> count. <span className="text-[#FF2E88]"><Ico e="♡" /></span>
</div> </div>
</div> </div>
</div> </div>
@@ -198,12 +200,12 @@ export function Intro() {
</div> </div>
<div className="hero-object absolute left-10 top-1/2" data-depth="0.12" data-speed="1.0"> <div className="hero-object absolute left-10 top-1/2" data-depth="0.12" data-speed="1.0">
<span className="animate-float nb-sm inline-block rotate-3 rounded-xl bg-white px-4 py-2 font-sora text-lg font-extrabold text-[#0a0a0a]" style={{ animationDelay: "0.8s" }}> <span className="animate-float nb-sm inline-block rotate-3 rounded-xl bg-white px-4 py-2 font-sora text-lg font-extrabold text-[#0a0a0a]" style={{ animationDelay: "0.8s" }}>
🔥 7-day streak <Ico e="🔥" /> 7-day streak
</span> </span>
</div> </div>
<div className="hero-object absolute bottom-8 right-16" data-depth="0.2" data-speed="1.7"> <div className="hero-object absolute bottom-8 right-16" data-depth="0.2" data-speed="1.7">
<span className="animate-float nb-sm inline-block -rotate-3 rounded-xl bg-[#C8FF37] px-4 py-2 font-sora text-lg font-extrabold text-[#0a0a0a]" style={{ animationDelay: "1.6s" }}> <span className="animate-float nb-sm inline-block -rotate-3 rounded-xl bg-[#C8FF37] px-4 py-2 font-sora text-lg font-extrabold text-[#0a0a0a]" style={{ animationDelay: "1.6s" }}>
🎁 reward unlocked <Ico e="🎁" /> reward unlocked
</span> </span>
</div> </div>
@@ -235,7 +237,7 @@ export function TheWhy() {
return ( return (
<Section id="the-why" variant="cream" size="punch" width="narrow"> <Section id="the-why" variant="cream" size="punch" width="narrow">
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<Pill bg="#0a0a0a" text="#FFD21E">💌 hey. quick story before we dive in</Pill> <Pill bg="#0a0a0a" text="#FFD21E"><Ico e="💌" /> hey. quick story before we dive in</Pill>
<div className="grid gap-8 lg:grid-cols-[1.4fr_1fr]"> <div className="grid gap-8 lg:grid-cols-[1.4fr_1fr]">
<div> <div>
<Card bg="#0a0a0a" text="#fff" className="p-6"> <Card bg="#0a0a0a" text="#fff" className="p-6">
@@ -257,20 +259,20 @@ export function TheWhy() {
<span></span> <span></span>
<Chip bg={C.lime}>REDEEM</Chip> <Chip bg={C.lime}>REDEEM</Chip>
<span></span> <span></span>
<Chip bg="#0a0a0a" text="#fff"> POINTS EXPIRE</Chip> <Chip bg="#0a0a0a" text="#fff"><Ico e="⏳" /> POINTS EXPIRE</Chip>
</div> </div>
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<Card bg="#fff" className="-rotate-1 p-5"> <Card bg="#fff" className="-rotate-1 p-5">
<div className="font-sora font-extrabold">🎬 DISCOVER ON SOCIAL MEDIA</div> <div className="font-sora font-extrabold"><Ico e="🎬" /> DISCOVER ON SOCIAL MEDIA</div>
<div className="text-sm font-semibold text-black/70">brand relationships start before purchase</div> <div className="text-sm font-semibold text-black/70">brand relationships start before purchase</div>
</Card> </Card>
<Card bg={C.pink} text="#fff" className="rotate-1 p-5"> <Card bg={C.pink} text="#fff" className="rotate-1 p-5">
<div className="font-sora font-extrabold">👯 COMMUNITIES DECIDE</div> <div className="font-sora font-extrabold"><Ico e="👯" /> COMMUNITIES DECIDE</div>
<div className="text-sm font-semibold text-white/80">influence &gt; paid advertising</div> <div className="text-sm font-semibold text-white/80">influence &gt; paid advertising</div>
</Card> </Card>
<Card bg={C.lime} className="-rotate-1 p-5"> <Card bg={C.lime} className="-rotate-1 p-5">
<div className="font-sora font-extrabold"> REWARD PARTICIPATION</div> <div className="font-sora font-extrabold"><Ico e="✨" /> REWARD PARTICIPATION</div>
<div className="text-sm font-semibold text-black/70">recognition, not coupons</div> <div className="text-sm font-semibold text-black/70">recognition, not coupons</div>
</Card> </Card>
</div> </div>
@@ -281,40 +283,60 @@ export function TheWhy() {
} }
/* 03 — THE PROBLEM /* 03 — THE PROBLEM
GSAP: the three cards deal in like cards flipped onto a table — a 3D GSAP: 3D Horizontal Flip Card Reveal */
rotationY/lift entrance scrubbed to scroll, with each number badge
popping as its card lands. */
export function TheProblem() { export function TheProblem() {
useGSAP(() => { useGSAP(() => {
syncLenisScrollTrigger(); syncLenisScrollTrigger();
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return; if (reduce) return;
/* Play-once on enter: the cards deal in with a gentle 3D lift, then settle /* Set 3D perspective container and initial 180-degree horizontal flip state */
perfectly flat and aligned (transforms cleared) so nothing lingers gsap.set("#the-problem .prob-card-grid", { perspective: 1400 });
skewed or overlaps the note below. */
gsap.set("#the-problem .prob-card", {
transformStyle: "preserve-3d",
transformOrigin: "50% 50%",
rotationY: -180,
y: 45,
scale: 0.85,
autoAlpha: 0,
boxShadow: "0 30px 60px rgba(0,0,0,0.5)",
});
gsap.set("#the-problem .prob-num", {
scale: 0,
rotation: -30,
transformOrigin: "50% 50%",
});
const tl = gsap.timeline({ const tl = gsap.timeline({
scrollTrigger: { trigger: "#the-problem", start: "top 72%", once: true }, scrollTrigger: { trigger: "#the-problem", start: "top 72%", once: true },
defaults: { ease: "power3.out" }, defaults: { ease: "back.out(1.5)" },
onComplete: () => gsap.set("#the-problem .prob-card", { clearProps: "transform" }), onComplete: () => gsap.set("#the-problem .prob-card", { clearProps: "transform,transformStyle" }),
}); });
tl.from("#the-problem .prob-card", {
y: 60, /* Horizontal 3D flip animation sequence */
rotationY: -18, tl.to("#the-problem .prob-card", {
rotationX: 8, rotationY: 0,
opacity: 0, y: 0,
transformOrigin: "50% 100%", scale: 1,
stagger: 0.14, autoAlpha: 1,
duration: 0.9, boxShadow: "6px 6px 0 0 #0a0a0a",
duration: 0.85,
stagger: 0.2,
ease: "back.out(1.5)",
}) })
.from("#the-problem .prob-num", { .to(
scale: 0, "#the-problem .prob-num",
rotation: -20, {
transformOrigin: "50% 50%", scale: 1,
stagger: 0.14, rotation: 0,
duration: 0.5, duration: 0.45,
ease: "back.out(2.2)", stagger: 0.2,
}, 0.2); ease: "back.out(2.2)",
},
"-=0.7"
);
}, []); }, []);
const cards = [ const cards = [
@@ -324,24 +346,24 @@ export function TheProblem() {
]; ];
return ( return (
<Section id="the-problem" variant="night" size="full" width="wide"> <Section id="the-problem" variant="night" size="full" width="wide">
<Pill> the problem</Pill> <Pill><Ico e="⚡" /> the problem</Pill>
<Heading className="mt-6"> <Heading className="mt-6">
traditional loyalty is <span className="strike-pink">broken.</span> traditional loyalty is <span className="strike-pink">broken.</span>
<br /> <br />
<span className="text-[#FFD21E]">here&apos;s why.</span> <span className="text-[#FFD21E]">here&apos;s why.</span>
</Heading> </Heading>
<div className="mt-10 grid gap-5 [perspective:1200px] md:grid-cols-3"> <div className="prob-card-grid mt-10 grid gap-5 [perspective:1400px] md:grid-cols-3">
{cards.map((c) => ( {cards.map((c) => (
<div <div
key={c.n} key={c.n}
className="prob-card nb nb-hover flex flex-col rounded-2xl p-6" className="prob-card nb nb-hover flex flex-col rounded-2xl p-6 cursor-pointer"
style={{ background: c.bg, color: c.tc }} style={{ background: c.bg, color: c.tc }}
> >
<div className="prob-num mb-2 w-fit rounded-md bg-[#0a0a0a] px-2 py-0.5 text-xs font-extrabold text-white">{c.n}</div> <div className="prob-num mb-2 w-fit rounded-md bg-[#0a0a0a] px-2 py-0.5 text-xs font-extrabold text-white">{c.n}</div>
<div className="font-sora text-2xl font-extrabold">{c.t}</div> <div className="font-sora text-2xl font-extrabold">{c.t}</div>
<ul className="mt-3 flex-1 space-y-1.5 text-sm font-bold"> <ul className="mt-3 flex-1 space-y-1.5 text-sm font-bold">
{c.items.map((i) => ( {c.items.map((i) => (
<li key={i}> {i}</li> <li key={i}><Ico e="✗" /> {i}</li>
))} ))}
</ul> </ul>
<div className="mt-4 rounded-lg bg-[#0a0a0a] py-2 text-center text-sm font-extrabold text-[#FFD21E]">{c.foot}</div> <div className="mt-4 rounded-lg bg-[#0a0a0a] py-2 text-center text-sm font-extrabold text-[#FFD21E]">{c.foot}</div>
@@ -349,7 +371,7 @@ export function TheProblem() {
))} ))}
</div> </div>
<div className="mt-6"> <div className="mt-6">
<Chip bg={C.yellow}> customers create value before spending. then traditional loyalty assigns zero credit.</Chip> <Chip bg={C.yellow}><Ico e="❗" /> customers create value before spending. then traditional loyalty assigns zero credit.</Chip>
</div> </div>
</Section> </Section>
); );
@@ -360,30 +382,30 @@ export function TheShift() {
return ( return (
<Section id="the-shift" variant="plain" className="!px-0"> <Section id="the-shift" variant="plain" className="!px-0">
<div className="mx-auto max-w-6xl px-6 sm:px-10 lg:px-20"> <div className="mx-auto max-w-6xl px-6 sm:px-10 lg:px-20">
<Pill bg="#0a0a0a" text="#FFD21E"> the shift</Pill> <Pill bg="#0a0a0a" text="#FFD21E"><Ico e="⚡" /> the shift</Pill>
<Heading className="mt-6"> <Heading className="mt-6">
from <span className="strike-pink">loyalty</span> to <span className="text-[#FF2E88]">engagement.</span> from <span className="strike-pink">loyalty</span> to <span className="text-[#FF2E88]">engagement.</span>
</Heading> </Heading>
<div className="mt-10 grid gap-8 md:grid-cols-2"> <div className="mt-10 grid gap-8 md:grid-cols-2">
<div> <div>
<div className="font-sora text-2xl font-extrabold">📺 old world</div> <div className="font-sora text-2xl font-extrabold"><Ico e="📺" /> old world</div>
<div className="text-sm font-semibold text-white/50">rewards spending only</div> <div className="text-sm font-semibold text-white/50">rewards spending only</div>
<div className="mt-5 space-y-3"> <div className="mt-5 space-y-3">
{[["🛒 OFFLINE COMMERCE", "still rewards only purchases"], ["📺 TV ADS · BANNERS", "one-way broadcast · no loop"], ["💳 SPEND-BASED POINTS", "transaction, no participation"]].map(([t, s]) => ( {[["🛒 OFFLINE COMMERCE", "still rewards only purchases"], ["📺 TV ADS · BANNERS", "one-way broadcast · no loop"], ["💳 SPEND-BASED POINTS", "transaction, no participation"]].map(([t, s]) => (
<Card key={t} bg="#fff" className="p-4"> <Card key={t} bg="#fff" className="p-4">
<div className="font-sora font-extrabold">{t}</div> <div className="font-sora font-extrabold"><span className="inline-flex items-center gap-2">{withIcons(t, 16)}</span></div>
<div className="text-sm font-semibold text-black/60">{s}</div> <div className="text-sm font-semibold text-black/60">{s}</div>
</Card> </Card>
))} ))}
</div> </div>
</div> </div>
<div> <div>
<div className="font-sora text-2xl font-extrabold"> new world</div> <div className="font-sora text-2xl font-extrabold"><Ico e="✨" /> new world</div>
<div className="text-sm font-semibold text-[#C8FF37]">rewards participation</div> <div className="text-sm font-semibold text-[#C8FF37]">rewards participation</div>
<div className="mt-5 space-y-3"> <div className="mt-5 space-y-3">
{[["📱 SOCIAL MEDIA", "rewards engagement + time", C.purple], ["🎮 GAMING", "rewards achievements + grind", C.pink], ["🎬 CREATOR PLATFORMS", "rewards contribution + community", C.orange]].map(([t, s, bg]) => ( {[["📱 SOCIAL MEDIA", "rewards engagement + time", C.purple], ["🎮 GAMING", "rewards achievements + grind", C.pink], ["🎬 CREATOR PLATFORMS", "rewards contribution + community", C.orange]].map(([t, s, bg]) => (
<Card key={t as string} bg={bg as string} text="#fff" className="p-4"> <Card key={t as string} bg={bg as string} text="#fff" className="p-4">
<div className="font-sora font-extrabold">{t}</div> <div className="font-sora font-extrabold"><span className="inline-flex items-center gap-2">{withIcons(t, 16)}</span></div>
<div className="text-sm font-semibold text-white/80">{s}</div> <div className="text-sm font-semibold text-white/80">{s}</div>
</Card> </Card>
))} ))}
@@ -391,13 +413,37 @@ export function TheShift() {
</div> </div>
</div> </div>
<div className="mt-8"> <div className="mt-8">
<Chip bg={C.yellow}>🔥 biggest miss in consumer tech right now: offline commerce still rewards only spending. that changes with LYTs.</Chip> <Chip bg={C.yellow}><Ico e="🔥" /> biggest miss in consumer tech right now: offline commerce still rewards only spending. that changes with LYTs.</Chip>
</div> </div>
</div> </div>
</Section> </Section>
); );
} }
/* Solid "slab" icons — bare black glyphs sitting inline with the chip label.
Each one loops a motion matching its meaning (see MeetLyts' GSAP hook). */
const SLAB_PATHS: Record<string, string> = {
walk: "M13.5 5.5c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zM9.8 8.9 7 23h2.1l1.8-8 2.1 2v6h2v-7.5l-2.1-2 .6-3C14.8 12 16.8 13 19 13v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1L6 8.3V13h2V9.6l1.8-.7z",
visit: "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z",
friend: "M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z",
shop: "M18 6h-2c0-2.21-1.79-4-4-4S8 3.79 8 6H6c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-6-2c1.1 0 2 .9 2 2h-4c0-1.1.9-2 2-2z",
event: "M12 .6l3.1 7.5 8.1.7-6.1 5.3 1.8 7.9L12 17.8 5.1 22l1.8-7.9L.8 8.8l8.1-.7z",
};
function SlabIcon({ name }: { name: keyof typeof SLAB_PATHS | string }) {
return (
<span
aria-hidden
data-icon={name}
className="slab-icon inline-flex shrink-0 items-center justify-center"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="#0a0a0a" style={{ willChange: "transform" }}>
<path d={SLAB_PATHS[name]} />
</svg>
</span>
);
}
/* 05 — MEET LYTs /* 05 — MEET LYTs
GSAP: the wordmark "LYTs." lands letter-by-letter in 3D as the section GSAP: the wordmark "LYTs." lands letter-by-letter in 3D as the section
enters, then the value flow pops in beat by beat. The giant word also enters, then the value flow pops in beat by beat. The giant word also
@@ -433,12 +479,27 @@ export function MeetLyts() {
ease: "none", ease: "none",
scrollTrigger: { trigger: "#lyts", start: "top bottom", end: "bottom top", scrub: 0.6 }, scrollTrigger: { trigger: "#lyts", start: "top bottom", end: "bottom top", scrub: 0.6 },
}); });
/* ── living slab icons — each glyph loops a motion that matches its meaning ── */
const glyph = (name: string) => `#lyts .slab-icon[data-icon="${name}"] svg`;
// walk → steps up and down
gsap.to(glyph("walk"), { y: -2.5, duration: 0.42, yoyo: true, repeat: -1, ease: "sine.inOut" });
// visit → the pin drops and settles
gsap.to(glyph("visit"), { y: -3.5, duration: 0.75, yoyo: true, repeat: -1, ease: "power2.inOut" });
// friend → the pair rocks side to side
gsap.to(glyph("friend"), { rotation: 9, transformOrigin: "50% 80%", duration: 0.8, yoyo: true, repeat: -1, ease: "sine.inOut" });
// shop → the bag swings from its handle
gsap.to(glyph("shop"), { rotation: -11, transformOrigin: "50% 0%", duration: 0.9, yoyo: true, repeat: -1, ease: "sine.inOut" });
// event → the star spins and twinkles
gsap.to(glyph("event"), { rotation: 360, transformOrigin: "50% 50%", duration: 6, ease: "none", repeat: -1 });
gsap.to(glyph("event"), { scale: 0.78, duration: 0.7, yoyo: true, repeat: -1, ease: "sine.inOut" });
}, []); }, []);
return ( return (
<Section id="lyts" variant="deep" size="normal" width="narrow"> <Section id="lyts" variant="deep" size="normal" width="narrow">
<span className="lyts-pill inline-block"> <span className="lyts-pill inline-block">
<Pill bg={C.yellow} text="#0a0a0a"> introducing</Pill> <Pill bg={C.yellow} text="#0a0a0a"><Ico e="✦" /> introducing</Pill>
</span> </span>
<h2 className="lyts-word mt-6 flex font-sora text-[24vw] font-extrabold leading-none tracking-tight text-white [perspective:1000px] sm:text-9xl"> <h2 className="lyts-word mt-6 flex font-sora text-[24vw] font-extrabold leading-none tracking-tight text-white [perspective:1000px] sm:text-9xl">
{chars.map((ch, i) => ( {chars.map((ch, i) => (
@@ -456,11 +517,11 @@ export function MeetLyts() {
</p> </p>
</div> </div>
<div className="mt-12 flex flex-wrap items-center gap-2"> <div className="mt-12 flex flex-wrap items-center gap-2">
<span className="lyts-chip inline-block"><Chip>walk 🚶</Chip></span> <span className="lyts-chip inline-block"><Chip>walk <SlabIcon name="walk" /></Chip></span>
<span className="lyts-chip inline-block"><Chip bg={C.yellow}>visit 📍</Chip></span> <span className="lyts-chip inline-block"><Chip bg={C.yellow}>visit <SlabIcon name="visit" /></Chip></span>
<span className="lyts-chip inline-block"><Chip bg={C.lime}>friend 🤝</Chip></span> <span className="lyts-chip inline-block"><Chip bg={C.lime}>friend <SlabIcon name="friend" /></Chip></span>
<span className="lyts-chip inline-block"><Chip bg={C.orange} text="#fff">shop 🛍</Chip></span> <span className="lyts-chip inline-block"><Chip bg={C.orange} text="#fff">shop <SlabIcon name="shop" /></Chip></span>
<span className="lyts-chip inline-block"><Chip bg={C.purple} text="#fff">event 🎉</Chip></span> <span className="lyts-chip inline-block"><Chip bg={C.purple} text="#fff">event <SlabIcon name="event" /></Chip></span>
<span className="lyts-chip"></span> <span className="lyts-chip"></span>
<span className="lyts-chip inline-block"><Chip bg="#0a0a0a" text={C.lime}>your LYTs balance</Chip></span> <span className="lyts-chip inline-block"><Chip bg="#0a0a0a" text={C.lime}>your LYTs balance</Chip></span>
<span className="lyts-chip"></span> <span className="lyts-chip"></span>
@@ -481,41 +542,155 @@ export function OneDay() {
["🏪", "supported", "a local business.", C.orange], ["🏪", "supported", "a local business.", C.orange],
["🌱", "showed up", "for life.", C.purple], ["🌱", "showed up", "for life.", C.purple],
] as const; ] as const;
useGSAP(() => {
syncLenisScrollTrigger();
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return;
const lineBeam = document.querySelector("#one-day .line-beam");
const lineOrb = document.querySelector("#one-day .line-orb");
const iconBadges = gsap.utils.toArray<HTMLElement>("#one-day .icon-badge");
const iconTexts = gsap.utils.toArray<HTMLElement>("#one-day .icon-text");
const cardResult = document.querySelector("#one-day .card-result");
if (!lineBeam || !lineOrb) return;
/* Hide / dim elements initially so they reveal as line touches them */
gsap.set(lineBeam, { width: "0%", opacity: 0 });
gsap.set(lineOrb, { left: "10%", opacity: 0, scale: 0.5 });
iconBadges.forEach((badge) => {
gsap.set(badge, { opacity: 0.2, scale: 0.82, filter: "brightness(0.4) blur(1px)", boxShadow: "0 0 0px transparent" });
});
iconTexts.forEach((text) => {
gsap.set(text, { opacity: 0.15, y: 12 });
});
if (cardResult) {
gsap.set(cardResult, { opacity: 0, y: 30, scale: 0.9 });
}
/* Pinned ScrollTrigger timeline: pins section until line reaches last round icon */
const tl = gsap.timeline({
scrollTrigger: {
trigger: "#one-day",
start: "top top",
end: "+=130%",
pin: true,
scrub: 0.8,
anticipatePin: 1,
},
});
const glowColors = ["#C8FF37", "#FFD21E", "#FF2E88", "#FF6B2C", "#9D4EDD"];
const positions = ["10%", "30%", "50%", "70%", "90%"];
/* Initial orb reveal */
tl.to(lineOrb, { opacity: 1, scale: 1.3, duration: 0.2 });
/* Draw line & reveal each round icon when line touches it */
positions.forEach((pos, i) => {
const targetWidth = `${(i / 4) * 80}%`;
const label = `step-${i}`;
tl.to(lineOrb, { left: pos, ease: "none", duration: 0.45 }, label);
tl.to(lineBeam, { width: targetWidth, opacity: 1, ease: "none", duration: 0.45 }, label);
tl.to(
iconBadges[i],
{
opacity: 1,
scale: 1.15,
filter: "brightness(1.15) blur(0px)",
boxShadow: `0 0 35px ${glowColors[i]}, 6px 6px 0 0 #0a0a0a`,
duration: 0.25,
},
`${label}+=0.1`
);
tl.to(
iconTexts[i],
{
opacity: 1,
y: 0,
duration: 0.25,
},
`${label}+=0.1`
);
});
/* Final reward card reveal after line reaches last round icon */
if (cardResult) {
tl.to(cardResult, {
opacity: 1,
y: 0,
scale: 1,
duration: 0.4,
ease: "back.out(1.5)",
});
}
});
return ( return (
<Section id="one-day" variant="deep" size="normal"> <Section id="one-day" variant="deep" size="full" className="min-h-screen flex flex-col justify-center py-12">
<Pill bg={C.lime} text="#0a0a0a">💚 one ordinary day</Pill> <Pill bg={C.lime} text="#0a0a0a"><Ico e="💚" /> one ordinary day</Pill>
<Heading className="mt-6"> <Heading className="mt-6">
here is what one ordinary day with <span className="text-[#C8FF37]">LYTs</span> looks like. here is what one ordinary day with <span className="text-[#C8FF37]">LYTs</span> looks like.
</Heading> </Heading>
<div className="mt-8 max-w-3xl space-y-3">
{rows.map(([e, a, b, c], i) => ( <div className="relative mt-14 max-w-5xl w-full">
<Reveal key={a} delay={i * 0.08} y={18}> {/* Connecting Track Line & Ultra-Bright Glow Beam */}
<div className="nb flex items-center gap-3 rounded-xl px-5 py-3 font-bold text-[#0a0a0a]" style={{ background: c }}> <div className="absolute top-6 sm:top-10 md:top-12 left-0 right-0 -translate-y-1/2 z-0 pointer-events-none">
<span className="text-xl">{e}</span> {/* Base Dark Track */}
<span>{a}</span> <div className="absolute left-[10%] right-[10%] top-1/2 -translate-y-1/2 h-3 bg-[#0a0a0a] rounded-full border border-white/20 shadow-inner" />
<span className="font-extrabold">{b}</span>
{/* Super-Bright Neon Beam */}
<div className="line-beam absolute top-1/2 -translate-y-1/2 left-[10%] h-3 rounded-full bg-gradient-to-r from-[#C8FF37] via-[#FF2E88] to-[#FFD21E] shadow-[0_0_30px_#C8FF37,0_0_15px_#FF2E88]" />
{/* High-Intensity Energy Orb */}
<div className="line-orb absolute top-1/2 -translate-y-1/2 -translate-x-1/2 w-7 h-7 rounded-full bg-[#C8FF37] border-3 border-[#0a0a0a] shadow-[0_0_30px_#C8FF37,0_0_15px_#ffffff] z-20 flex items-center justify-center">
<div className="w-2 h-2 rounded-full bg-[#0a0a0a]" />
</div>
</div>
{/* 5 Icons Row */}
<div className="relative z-10 grid grid-cols-5 gap-2 sm:gap-6 md:gap-8">
{rows.map(([e, a, b, c], i) => (
<div key={a} className="flex flex-col items-center text-center group">
<div
className="icon-badge nb nb-hover relative z-10 flex h-12 w-12 sm:h-20 sm:w-20 md:h-24 md:w-24 items-center justify-center rounded-full shrink-0 text-[#0a0a0a] transition-all duration-300"
style={{ background: c }}
>
<Ico e={e} size={28} />
</div>
<div className="icon-text mt-3.5 flex flex-col items-center text-center font-sora text-xs sm:text-sm md:text-base text-white leading-tight">
<span>{a}</span>
<span className="font-extrabold">{b}</span>
</div>
</div> </div>
</Reveal> ))}
))} </div>
</div> </div>
<Card bg="#fff" className="mt-6 w-fit p-5">
<Card bg="#fff" className="card-result mt-12 w-fit p-5">
<div className="font-sora text-xl font-extrabold">you earned LYTs</div> <div className="font-sora text-xl font-extrabold">you earned LYTs</div>
<div className="mt-1 w-fit bg-[#FFD21E] px-2 font-sora text-xl font-extrabold">because your life has value. 🤍</div> <div className="mt-1 w-fit bg-[#FFD21E] px-2 font-sora text-xl font-extrabold">because your life has value. <Ico e="🤍" /></div>
</Card> </Card>
</Section> </Section>
); );
} }
/* 07 — FEELINGS /* 07 — FEELINGS
GSAP: the headline reveals word-by-word out of masks, then the five cards GSAP: card game dealer reveal from center stack one by one */
wipe up like curtains (clip-path) coming from blur into sharp focus. */
export function Feelings() { export function Feelings() {
const cards = [ const cards = [
["🏆", "Recognition", "be seen, be credited", C.pink, "#fff"], ["🏆", "Recognition", "be seen, be credited", C.pink, "#fff"],
["🌴", "Experiences", "real life, not receipts", C.yellow, "#0a0a0a"], ["🌴", "Experiences", "real life, not receipts", C.yellow, "#0a0a0a"],
["🫂", "Belonging", "community = currency", C.lime, "#0a0a0a"], ["🫂", "Belonging", "community = currency", C.lime, "#0a0a0a"],
["🪞", "Identity", "who you are matters", C.orange, "#fff"], ["🪞", "Identity", "who you are matters", C.orange, "#fff"],
["💬", "Community", "tribes win", "#0a0a0a", C.lime], ["💬", "Community", "tribes win", C.purple, "#fff"],
] as const; ] as const;
useGSAP(() => { useGSAP(() => {
@@ -523,11 +698,31 @@ export function Feelings() {
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return; if (reduce) return;
/* hide immediately (before paint) so nothing flashes before its trigger */ /* Hide headline & sum box */
gsap.set("#feelings .feel-w-in", { yPercent: 118, rotationZ: 4, autoAlpha: 0 }); gsap.set("#feelings .feel-w-in", { yPercent: 118, rotationZ: 4, autoAlpha: 0 });
gsap.set("#feelings .feel-card", { clipPath: "inset(100% 0% 0% 0%)", yPercent: 14, scale: 0.92, autoAlpha: 0, filter: "blur(12px)" });
gsap.set("#feelings .feel-sum", { y: 34, autoAlpha: 0 }); gsap.set("#feelings .feel-sum", { y: 34, autoAlpha: 0 });
/* Set 3D perspective for card game dealer grid */
gsap.set("#feelings .feel-grid", { perspective: 1200 });
const cardEls = gsap.utils.toArray<HTMLElement>("#feelings .feel-card");
/* Stack all cards at the CENTER of the grid before dealer deals them out */
cardEls.forEach((card, i) => {
gsap.set(card, {
transformStyle: "preserve-3d",
transformOrigin: "50% 50%",
autoAlpha: 0,
xPercent: (2 - i) * 110, // Shifts all cards to center column (index 2)
y: -100,
scale: 0.5,
rotationY: -90,
rotationZ: (i - 2) * 8,
boxShadow: "0 30px 60px rgba(0,0,0,0.5)",
});
});
/* Headline reveal */
gsap.to("#feelings .feel-w-in", { gsap.to("#feelings .feel-w-in", {
scrollTrigger: { trigger: "#feelings", start: "top 78%", once: true }, scrollTrigger: { trigger: "#feelings", start: "top 78%", once: true },
yPercent: 0, yPercent: 0,
@@ -538,17 +733,22 @@ export function Feelings() {
ease: "power4.out", ease: "power4.out",
}); });
gsap.to("#feelings .feel-card", { /* Dealer deals cards from center out to their positions one by one */
scrollTrigger: { trigger: "#feelings .feel-grid", start: "top 82%", once: true }, gsap.to(cardEls, {
clipPath: "inset(0% 0% 0% 0%)", scrollTrigger: { trigger: "#feelings .feel-grid", start: "top 75%", once: true },
yPercent: 0, xPercent: 0,
y: 0,
scale: 1, scale: 1,
rotationY: 0,
rotationZ: 0,
autoAlpha: 1, autoAlpha: 1,
filter: "blur(0px)", boxShadow: "6px 6px 0 0 #0a0a0a",
stagger: 0.1, duration: 0.65,
duration: 0.95, stagger: 0.15,
ease: "power3.out", ease: "back.out(1.5)",
onComplete: () => gsap.set("#feelings .feel-card", { clearProps: "filter,clipPath,transform" }), onComplete: () => {
gsap.set(cardEls, { clearProps: "transform,transformStyle,perspective" });
},
}); });
gsap.to("#feelings .feel-sum", { gsap.to("#feelings .feel-sum", {
@@ -562,7 +762,7 @@ export function Feelings() {
return ( return (
<Section id="feelings" variant="cream" size="epic" width="wide"> <Section id="feelings" variant="cream" size="epic" width="wide">
<Pill bg={C.purple} text="#fff">💗 why social currency</Pill> <Pill bg={C.purple} text="#fff"><Ico e="💗" /> why social currency</Pill>
<h2 className="mt-6 font-sora text-4xl font-extrabold leading-[1.02] tracking-tight [perspective:800px] sm:text-5xl lg:text-6xl"> <h2 className="mt-6 font-sora text-4xl font-extrabold leading-[1.02] tracking-tight [perspective:800px] sm:text-5xl lg:text-6xl">
<MaskWords text="people don't want points. they want" wordClass="feel-w-in" /> <MaskWords text="people don't want points. they want" wordClass="feel-w-in" />
<MaskWords text="feelings." wordClass="feel-w-in" inner="text-[#FF2E88]" /> <MaskWords text="feelings." wordClass="feel-w-in" inner="text-[#FF2E88]" />
@@ -571,12 +771,14 @@ export function Feelings() {
{cards.map(([e, t, s, bg, tc]) => ( {cards.map(([e, t, s, bg, tc]) => (
<div <div
key={t} key={t}
className="feel-card nb nb-hover rounded-2xl p-5 [will-change:transform,filter]" className="feel-card nb nb-hover flex flex-col justify-between rounded-2xl p-5 cursor-pointer [will-change:transform]"
style={{ background: bg, color: tc }} style={{ background: bg, color: tc }}
> >
<div className="text-2xl">{e}</div> <div>
<div className="mt-6 font-sora text-lg font-extrabold">{t}</div> <div className="mb-1"><Ico e={e} size={28} /></div>
<div className="text-xs font-bold opacity-80">{s}</div> <div className="mt-6 font-sora text-lg font-extrabold leading-tight">{t}</div>
<div className="mt-1 text-xs font-bold opacity-80">{s}</div>
</div>
</div> </div>
))} ))}
</div> </div>
@@ -619,29 +821,29 @@ export function Difference() {
return ( return (
<Section id="difference" variant="night" size="full" width="wide" bgImage="/images/background/FD_bg.png"> <Section id="difference" variant="night" size="full" width="wide" bgImage="/images/background/FD_bg.png">
<Pill> the fundamental difference</Pill> <Pill><Ico e="⚡" /> the fundamental difference</Pill>
<h2 className="mt-6 font-sora text-4xl font-extrabold leading-[0.95] tracking-tight text-[#FFD21E] [perspective:800px] sm:text-5xl lg:text-6xl"> <h2 className="mt-6 font-sora text-4xl font-extrabold leading-[0.95] tracking-tight text-[#FFD21E] [perspective:800px] sm:text-5xl lg:text-6xl">
<span className="block"><MaskWords text="loyalty starts at spend." wordClass="diff-w-in" /></span> <span className="block"><MaskWords text="loyalty starts at spend." wordClass="diff-w-in" /></span>
<span className="block"><MaskWords text="LYTs starts at engage." wordClass="diff-w-in" /></span> <span className="block"><MaskWords text="LYTs starts at engage." wordClass="diff-w-in" /></span>
</h2> </h2>
<div className="mt-10 grid items-start gap-4 md:grid-cols-2"> <div className="mt-10 grid items-start gap-4 md:grid-cols-2">
<div className="diff-left"> <div className="diff-left">
<Pill bg={C.yellow} text="#0a0a0a">💻 OLD WORLD</Pill> <Pill bg={C.yellow} text="#0a0a0a"><Ico e="💻" /> OLD WORLD</Pill>
<div className="mt-3 font-sora text-3xl font-extrabold">TRADITIONAL LOYALTY</div> <div className="mt-3 font-sora text-3xl font-extrabold">TRADITIONAL LOYALTY</div>
<div className="mt-4 space-y-2"> <div className="mt-4 space-y-2">
<div className="diff-step nb rounded-lg bg-white px-4 py-2 text-center font-extrabold text-[#0a0a0a]">PURCHASE </div> <div className="diff-step nb rounded-lg bg-white px-4 py-2 text-center font-extrabold text-[#0a0a0a]">PURCHASE </div>
<div className="diff-step nb rounded-lg px-4 py-2 text-center font-extrabold text-[#0a0a0a]" style={{ background: C.yellow }}>EARN POINTS </div> <div className="diff-step nb rounded-lg px-4 py-2 text-center font-extrabold text-[#0a0a0a]" style={{ background: C.yellow }}>EARN POINTS </div>
<div className="diff-step nb rounded-lg px-4 py-2 text-center font-extrabold text-white" style={{ background: C.orange }}>REDEEM </div> <div className="diff-step nb rounded-lg px-4 py-2 text-center font-extrabold text-white" style={{ background: C.orange }}>REDEEM </div>
<div className="diff-step font-extrabold text-[#C8FF37]"> POINTS EXPIRE</div> <div className="diff-step font-extrabold text-[#C8FF37]"><Ico e="⏳" /> POINTS EXPIRE</div>
</div> </div>
<div className="mt-4"><Chip bg={C.lime}>🔥 not a better coupon. a whole new currency. 🌱</Chip></div> <div className="mt-4"><Chip bg={C.lime}><Ico e="🔥" /> not a better coupon. a whole new currency. <Ico e="🌱" /></Chip></div>
</div> </div>
<div className="diff-right nb-lg rounded-2xl bg-gradient-to-br from-[#FF2E88] to-[#A020F0] p-5"> <div className="diff-right nb-lg rounded-2xl bg-gradient-to-br from-[#FF2E88] to-[#A020F0] p-5">
<Pill bg={C.yellow} text="#0a0a0a"> LYTS WAY</Pill> <Pill bg={C.yellow} text="#0a0a0a"><Ico e="✨" /> LYTS WAY</Pill>
<div className="mt-3 font-sora text-3xl font-extrabold text-white">LYTS SOCIAL CURRENCY</div> <div className="mt-3 font-sora text-3xl font-extrabold text-white">LYTS SOCIAL CURRENCY</div>
<div className="mt-4 space-y-2"> <div className="mt-4 space-y-2">
{[["DISCOVER", C.lime, "#0a0a0a"], ["ENGAGE", C.yellow, "#0a0a0a"], ["EARN LYTS", C.orange, "#fff"], ["SPEND", C.pink, "#fff"], ["LIVE MORE", "#fff", "#0a0a0a"], ["💞 LYTIAN", "#0a0a0a", C.lime]].map(([t, bg, tc]) => ( {[["DISCOVER", C.lime, "#0a0a0a"], ["ENGAGE", C.yellow, "#0a0a0a"], ["EARN LYTS", C.orange, "#fff"], ["SPEND", C.pink, "#fff"], ["LIVE MORE", "#fff", "#0a0a0a"], ["💞 LYTIAN", "#0a0a0a", C.lime]].map(([t, bg, tc]) => (
<div key={t} className="diff-step nb rounded-lg px-4 py-2 text-center font-extrabold" style={{ background: bg, color: tc }}>{t} {t !== "💞 LYTIAN" && "↓"}</div> <div key={t} className="diff-step nb rounded-lg px-4 py-2 text-center font-extrabold" style={{ background: bg, color: tc }}><span className="inline-flex items-center justify-center gap-1.5">{withIcons(t, 15)} {t !== "💞 LYTIAN" && "↓"}</span></div>
))} ))}
</div> </div>
</div> </div>
@@ -695,7 +897,7 @@ export function TheLoop() {
return ( return (
<Section id="the-loop" variant="night" size="normal"> <Section id="the-loop" variant="night" size="normal">
<Pill bg={C.lime} text="#0a0a0a">🚀 your 6-step loop</Pill> <Pill bg={C.lime} text="#0a0a0a"><Ico e="🚀" /> your 6-step loop</Pill>
<h2 className="mt-6 font-sora text-4xl font-extrabold leading-[1.02] tracking-tight [perspective:900px] sm:text-5xl lg:text-6xl"> <h2 className="mt-6 font-sora text-4xl font-extrabold leading-[1.02] tracking-tight [perspective:900px] sm:text-5xl lg:text-6xl">
<span className="block"><MaskWords text="six small moments." wordClass="loop-w-in" /></span> <span className="block"><MaskWords text="six small moments." wordClass="loop-w-in" /></span>
<span className="block"> <span className="block">
@@ -712,11 +914,11 @@ export function TheLoop() {
> >
<div className="mb-2 w-fit rounded bg-[#0a0a0a] px-1.5 text-xs font-extrabold text-white">{n}</div> <div className="mb-2 w-fit rounded bg-[#0a0a0a] px-1.5 text-xs font-extrabold text-white">{n}</div>
<div className="font-sora text-lg font-extrabold leading-tight">{t}</div> <div className="font-sora text-lg font-extrabold leading-tight">{t}</div>
<div className="mt-2 text-xs font-bold opacity-80">{s}</div> <div className="mt-2 text-xs font-bold opacity-80"><span className="inline-flex flex-wrap items-center gap-1">{withIcons(s, 13)}</span></div>
</div> </div>
))} ))}
</div> </div>
<div className="mt-6"><Chip bg={C.purple} text="#fff"> every interaction creates value. every LYTIAN invites another.</Chip></div> <div className="mt-6"><Chip bg={C.purple} text="#fff"><Ico e="♻️" /> every interaction creates value. every LYTIAN invites another.</Chip></div>
</Section> </Section>
); );
} }

View File

@@ -13,88 +13,7 @@ import {
} from "./ui"; } from "./ui";
import { C } from "./palette"; import { C } from "./palette";
import { gsap, useGSAP, syncLenisScrollTrigger, MaskWords } from "./gsapkit"; import { gsap, useGSAP, syncLenisScrollTrigger, MaskWords } from "./gsapkit";
import { Ico, withIcons } from "./Ico";
/* 10 — FOR YOU */
export function ForYou() {
const cards = [
["🚶", "earn before spending", "value starts before purchase. discovery, visits, sharing.", C.pink, "#fff"],
["🌅", "everyday life = value", "walking. exploring. visiting. sharing. everything matters.", C.yellow, "#0a0a0a"],
["🌐", "one connected currency", "one LYTs balance across experiences and brands. no silos.", C.lime, "#0a0a0a"],
["📢", "real recognition", "rewarded for contribution and engagement, not just transactions.", C.purple, "#fff"],
] as const;
return (
<Section id="for-you" variant="cream" size="full" width="wide">
<div className="grid gap-8 lg:grid-cols-[1.3fr_1fr] lg:items-start">
<div>
<Pill>🙋 for you</Pill>
<h2 className="mt-6 font-sora text-4xl sm:text-6xl md:text-7xl font-extrabold leading-[0.9] tracking-tight">
your life
<br />
is <span className="text-[#A020F0]">worth</span>
<br />
something.
</h2>
<p className="mt-4 font-bold">every step. every visit. every selfie. it&apos;s all worth it here. 💗</p>
</div>
<div className="space-y-4">
<Card bg="#0a0a0a" text={C.lime} className="p-5">
<div className="font-sora text-lg sm:text-xl font-extrabold">value begins before purchase. 🌱</div>
</Card>
<Card bg={C.orange} text="#fff" className="p-5">
<div className="font-sora text-xl sm:text-2xl font-extrabold">walk in. win more.</div>
<div className="mt-1 text-sm font-bold">
10k steps? <span className="rounded bg-[#FFD21E] px-1 text-[#0a0a0a]">earn.</span> coffee with a friend?{" "}
<span className="rounded bg-[#FFD21E] px-1 text-[#0a0a0a]">earn.</span> sharing a vibe?{" "}
<span className="rounded bg-[#FFD21E] px-1 text-[#0a0a0a]">earn.</span>
</div>
</Card>
</div>
</div>
<Group className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{cards.map(([e, t, s, bg, tc]) => (
<Card key={t} group bg={bg} text={tc} className="p-5">
<div className="text-2xl">{e}</div>
<div className="mt-4 font-sora text-lg font-extrabold leading-tight">{t}</div>
<div className="mt-1 text-xs font-bold opacity-85">{s}</div>
</Card>
))}
</Group>
</Section>
);
}
/* 11 — FOR BUSINESS */
export function ForBusiness() {
const cards = [
["1", "🤝 ACQUIRE", "CUSTOMERS EARLIER", "Engage them BEFORE the transaction.", C.pink, "#fff"],
["2", "👟 FOOTFALL", "INCREASE VISITS", "Reward the showing up.", C.lime, "#0a0a0a"],
["3", "📁 CAC", "SLASH ACQUISITION COSTS", "Organic loops replace paid ads.", C.yellow, "#0a0a0a"],
["4", "🔁 KEEP", "STICKIER CUSTOMERS", "Continuous engagement = love.", C.orange, "#fff"],
["5", "👥 TRIBE", "BUILD ADVOCATES", "Customers turn into LYTIANS.", C.purple, "#fff"],
["6", "🧠 INSIGHT", "BETTER INTENT SIGNALS", "See them before the receipt.", "#fff", "#0a0a0a"],
] as const;
return (
<Section id="for-business" variant="night" size="full" width="wide">
<Pill bg={C.yellow} text="#0a0a0a">🤝 for businesses</Pill>
<Heading className="mt-6">
acquire earlier.
<br />
retain <span className="text-[#FFD21E]">forever.</span>
</Heading>
<p className="mt-4 font-bold text-[#C8FF37]">meet people before they buy. keep them warm after. 💞</p>
<Group className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{cards.map(([n, tag, t, s, bg, tc]) => (
<Card key={n} group bg={bg} text={tc} className="relative overflow-hidden p-5">
<span className="absolute right-3 top-2 font-sora text-4xl font-extrabold opacity-20">{n}</span>
<div className="w-fit rounded bg-[#0a0a0a] px-2 py-0.5 text-xs font-extrabold text-[#FFD21E]">{tag}</div>
<div className="mt-3 font-sora text-xl font-extrabold leading-tight">{t}</div>
<div className="mt-1 text-sm font-bold opacity-85">{s}</div>
</Card>
))}
</Group>
</Section>
);
}
/* 12 — WHY NOW */ /* 12 — WHY NOW */
export function WhyNow() { export function WhyNow() {
@@ -105,24 +24,95 @@ export function WhyNow() {
["🎮", "GAMIFIED LIFE", "Systems make it real.", "↑", C.orange, "#fff"], ["🎮", "GAMIFIED LIFE", "Systems make it real.", "↑", C.orange, "#fff"],
["📉", "OLD LOYALTY FADING", "No real bond anymore.", "↓↓", C.pink, "#fff"], ["📉", "OLD LOYALTY FADING", "No real bond anymore.", "↓↓", C.pink, "#fff"],
] as const; ] as const;
useGSAP(() => {
syncLenisScrollTrigger();
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return;
const cardsEl = gsap.utils.toArray<HTMLElement>("#why-now .game-card");
if (!cardsEl.length) return;
/* 3D perspective wrapper */
gsap.set("#why-now .game-card-grid", { perspective: 1200 });
/* Set cards in a fanned-out HAND at bottom center */
cardsEl.forEach((card, i) => {
gsap.set(card, {
transformStyle: "preserve-3d",
transformOrigin: "50% 120%",
autoAlpha: 0,
xPercent: (2 - i) * 110, // Gather cards at bottom center hand position
y: 140, // Lowered down in hand
scale: 0.6,
rotationZ: (i - 2) * 12, // Fan out angle in hand (-24deg to +24deg)
rotationY: -45, // Slightly tilted in hand
boxShadow: "0 25px 50px rgba(0,0,0,0.6)",
});
});
/* Card deal animation timeline on ScrollTrigger */
const tl = gsap.timeline({
scrollTrigger: {
trigger: "#why-now",
start: "top 72%",
once: true,
},
});
/* Step 1: Hand appears fanned open */
tl.to(cardsEl, {
autoAlpha: 1,
duration: 0.3,
stagger: 0.05,
ease: "power2.out",
});
/* Step 2: Deal cards from hand onto grid one by one */
tl.to(
cardsEl,
{
xPercent: 0,
y: 0,
scale: 1,
rotationZ: 0,
rotationY: 0,
boxShadow: "6px 6px 0 0 #0a0a0a",
duration: 0.65,
stagger: 0.16,
ease: "back.out(1.6)",
onComplete: () => {
gsap.set(cardsEl, { clearProps: "transform,transformStyle,perspective" });
},
},
"-=0.1"
);
});
return ( return (
<Section id="why-now" variant="deep" size="normal" width="wide"> <Section id="why-now" variant="deep" size="normal" width="wide">
<Pill bg={C.lime} text="#0a0a0a"> why this matters</Pill> <Pill bg={C.lime} text="#0a0a0a"><Ico e="⏰" /> why this matters</Pill>
<Heading className="mt-6"> <Heading className="mt-6">
5 currents 5 currents
<br /> <br />
are converging <span className="text-[#FFD21E]">right now.</span> are converging <span className="text-[#FFD21E]">right now.</span>
</Heading> </Heading>
<Group className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-5"> <div className="game-card-grid mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
{cards.map(([e, t, s, a, bg, tc]) => ( {cards.map(([e, t, s, a, bg, tc]) => (
<Card key={t} group bg={bg} text={tc} className="flex flex-col p-5"> <div
<div className="text-2xl">{e}</div> key={t}
<div className="mt-4 font-sora text-lg font-extrabold leading-tight">{t}</div> className="game-card nb nb-hover flex flex-col justify-between p-5 rounded-2xl cursor-pointer transition-transform duration-200"
<div className="mt-1 text-xs font-bold opacity-80">{s}</div> style={{ background: bg, color: tc }}
<div className="mt-4 text-2xl font-extrabold">{a}</div> >
</Card> <div>
<div className="mb-1"><Ico e={e} size={28} /></div>
<div className="mt-4 font-sora text-lg font-extrabold leading-tight">{t}</div>
<div className="mt-1 text-xs font-bold opacity-80">{s}</div>
</div>
<div className="mt-6 text-2xl font-extrabold">{a}</div>
</div>
))} ))}
</Group> </div>
</Section> </Section>
); );
} }
@@ -145,14 +135,14 @@ export function HowItWorks() {
const bottom = cards.slice(5); const bottom = cards.slice(5);
return ( return (
<Section id="how-it-works" variant="cream" size="epic" width="wide"> <Section id="how-it-works" variant="cream" size="epic" width="wide">
<Pill bg="#0a0a0a" text={C.lime}> how it works</Pill> <Pill bg="#0a0a0a" text={C.lime}><Ico e="⚡" /> how it works</Pill>
<h2 className="mt-6 font-sora text-5xl font-extrabold leading-[0.95] tracking-tight sm:text-6xl"> <h2 className="mt-6 font-sora text-5xl font-extrabold leading-[0.95] tracking-tight sm:text-6xl">
every action earns points{" "} every action earns points{" "}
<span className="nb-sm bg-[#C8FF37] px-2 text-[#0a0a0a]">LYTs.</span> <span className="nb-sm bg-[#C8FF37] px-2 text-[#0a0a0a]">LYTs.</span>
</h2> </h2>
<p className="mt-4 font-bold"> <p className="mt-4 font-bold">
no app-opening rituals. no complicated loyalty rules.{" "} no app-opening rituals. no complicated loyalty rules.{" "}
<span className="bg-[#FFD21E] px-1">no waiting until you spend.</span> just live your life, engage the world, and watch your LYTs grow. 🌱 <span className="bg-[#FFD21E] px-1">no waiting until you spend.</span> just live your life, engage the world, and watch your LYTs grow. <Ico e="🌱" />
</p> </p>
{/* Two counter-scrolling tracks. The opposing directions read as a {/* Two counter-scrolling tracks. The opposing directions read as a
@@ -196,7 +186,7 @@ function ActionTile({
<span className="absolute right-2 top-2 rounded bg-[#0a0a0a] px-1.5 py-0.5 text-[10px] font-extrabold text-[#FFD21E]"> <span className="absolute right-2 top-2 rounded bg-[#0a0a0a] px-1.5 py-0.5 text-[10px] font-extrabold text-[#FFD21E]">
+LYTs +LYTs
</span> </span>
<div className="text-2xl">{emoji}</div> <div className="mb-1"><Ico e={emoji} size={26} /></div>
<div className="mt-2 font-sora text-lg font-extrabold leading-tight">{title}</div> <div className="mt-2 font-sora text-lg font-extrabold leading-tight">{title}</div>
<div className="mt-0.5 text-xs font-bold opacity-80">{sub}</div> <div className="mt-0.5 text-xs font-bold opacity-80">{sub}</div>
</div> </div>
@@ -233,7 +223,7 @@ export function OldVsNew() {
return ( return (
<Section id="old-vs-new" variant="deep" size="full" width="wide"> <Section id="old-vs-new" variant="deep" size="full" width="wide">
<Pill bg={C.yellow} text="#0a0a0a">💞 a new kind of loyalty</Pill> <Pill bg={C.yellow} text="#0a0a0a"><Ico e="💞" /> a new kind of loyalty</Pill>
<p className="mt-4 font-bold text-[#C8FF37]">traditional loyalty said one thing.</p> <p className="mt-4 font-bold text-[#C8FF37]">traditional loyalty said one thing.</p>
<h2 className="mt-1 font-sora text-4xl font-extrabold leading-[0.95] tracking-tight [perspective:800px] sm:text-5xl lg:text-6xl"> <h2 className="mt-1 font-sora text-4xl font-extrabold leading-[0.95] tracking-tight [perspective:800px] sm:text-5xl lg:text-6xl">
<MaskWords text="LYTs says" wordClass="ovn-w-in" inner="text-[#FFD21E]" /> <MaskWords text="LYTs says" wordClass="ovn-w-in" inner="text-[#FFD21E]" />
@@ -241,25 +231,25 @@ export function OldVsNew() {
</h2> </h2>
<div className="mt-10 grid items-stretch gap-4 md:grid-cols-[1fr_auto_1fr]"> <div className="mt-10 grid items-stretch gap-4 md:grid-cols-[1fr_auto_1fr]">
<div className="ovn-left nb nb-hover rounded-2xl bg-[#0a0a0a] p-6 text-white [will-change:transform]"> <div className="ovn-left nb nb-hover rounded-2xl bg-[#0a0a0a] p-6 text-white [will-change:transform]">
<Pill bg={C.yellow} text="#0a0a0a">💻 TRADITIONAL LOYALTY</Pill> <Pill bg={C.yellow} text="#0a0a0a"><Ico e="💻" /> TRADITIONAL LOYALTY</Pill>
<div className="mt-4 font-sora text-3xl font-extrabold leading-tight text-[#FFD21E]"> <div className="mt-4 font-sora text-3xl font-extrabold leading-tight text-[#FFD21E]">
<span className="ovn-rule block">the old rule</span> <span className="ovn-rule block">the old rule</span>
<span className="ovn-rule block text-white">spend first.</span> <span className="ovn-rule block text-white">spend first.</span>
<span className="ovn-rule block text-white">earn later.</span> <span className="ovn-rule block text-white">earn later.</span>
</div> </div>
<div className="ovn-rule mt-4 text-sm font-bold text-white/70">value is created only when you buy. participation = invisible. 🫥</div> <div className="ovn-rule mt-4 text-sm font-bold text-white/70">value is created only when you buy. participation = invisible. <Ico e="🫥" /></div>
</div> </div>
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<span className="ovn-vs nb flex h-16 w-16 items-center justify-center rounded-full bg-[#FFD21E] font-sora text-xl font-extrabold text-[#0a0a0a] [will-change:transform]">VS</span> <span className="ovn-vs nb flex h-16 w-16 items-center justify-center rounded-full bg-[#FFD21E] font-sora text-xl font-extrabold text-[#0a0a0a] [will-change:transform]">VS</span>
</div> </div>
<div className="ovn-right nb-lg rounded-2xl bg-gradient-to-br from-[#FF2E88] to-[#A020F0] p-6 [will-change:transform]"> <div className="ovn-right nb-lg rounded-2xl bg-gradient-to-br from-[#FF2E88] to-[#A020F0] p-6 [will-change:transform]">
<Pill bg="#0a0a0a" text="#fff"> THE LYTS WAY</Pill> <Pill bg="#0a0a0a" text="#fff"><Ico e="✨" /> THE LYTS WAY</Pill>
<div className="mt-4 font-sora text-3xl font-extrabold leading-tight text-white"> <div className="mt-4 font-sora text-3xl font-extrabold leading-tight text-white">
<span className="ovn-rule block">the new rule</span> <span className="ovn-rule block">the new rule</span>
<span className="ovn-rule block">live first.</span> <span className="ovn-rule block">live first.</span>
<span className="ovn-rule block">earn everywhere.</span> <span className="ovn-rule block">earn everywhere.</span>
</div> </div>
<div className="ovn-rule mt-4 text-sm font-bold text-white/85">value is created when you participate. everyday life is the unlock. 🎉</div> <div className="ovn-rule mt-4 text-sm font-bold text-white/85">value is created when you participate. everyday life is the unlock. <Ico e="🎉" /></div>
</div> </div>
</div> </div>
<div className="mt-6"> <div className="mt-6">
@@ -274,7 +264,7 @@ export function OldVsNew() {
/* 15 — THE VISION /* 15 — THE VISION
GSAP: the headline reveals word by word with "social" punching in on an GSAP: the headline reveals word by word with "social" punching in on an
overshoot, while the value list stacks up — each row slides in from the overshoot, while the value list stacks up — each row slides in from the
right and its ticks in with a springy pop, like a checklist completing. */ right and its <Ico e="✓" /> ticks in with a springy pop, like a checklist completing. */
export function TheVision() { export function TheVision() {
const items = [ const items = [
["every discovery has value", "#fff", "#0a0a0a"], ["every discovery has value", "#fff", "#0a0a0a"],
@@ -308,7 +298,7 @@ export function TheVision() {
<Section id="the-vision" variant="deep" size="normal"> <Section id="the-vision" variant="deep" size="normal">
<div className="grid gap-8 lg:grid-cols-[1.2fr_1fr] lg:items-center"> <div className="grid gap-8 lg:grid-cols-[1.2fr_1fr] lg:items-center">
<div> <div>
<Pill bg={C.yellow} text="#0a0a0a">🌈 the vision</Pill> <Pill bg={C.yellow} text="#0a0a0a"><Ico e="🌈" /> the vision</Pill>
<h2 className="mt-6 font-sora text-4xl font-extrabold leading-[0.95] tracking-tight [perspective:800px] sm:text-6xl md:text-7xl"> <h2 className="mt-6 font-sora text-4xl font-extrabold leading-[0.95] tracking-tight [perspective:800px] sm:text-6xl md:text-7xl">
<MaskWords text="world's largest" wordClass="vis-w-in" /> <MaskWords text="world's largest" wordClass="vis-w-in" />
<span className="mr-[0.24em] inline-block overflow-hidden align-bottom pb-[0.08em]"> <span className="mr-[0.24em] inline-block overflow-hidden align-bottom pb-[0.08em]">
@@ -326,14 +316,14 @@ export function TheVision() {
className="vis-row nb flex items-center gap-3 rounded-xl px-4 py-3 font-extrabold [will-change:transform]" className="vis-row nb flex items-center gap-3 rounded-xl px-4 py-3 font-extrabold [will-change:transform]"
style={{ background: bg, color: tc }} style={{ background: bg, color: tc }}
> >
<span className="vis-check flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[#C8FF37] text-sm text-[#0a0a0a]"></span> <span className="vis-check flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[#C8FF37] text-sm text-[#0a0a0a]"><Ico e="✓" /></span>
{t} {t}
</div> </div>
))} ))}
</div> </div>
</div> </div>
<div className="mt-8"> <div className="mt-8">
<Chip bg="#0a0a0a" text={C.lime}>every meaningful interaction has value. 🤝</Chip> <Chip bg="#0a0a0a" text={C.lime}>every meaningful interaction has value. <Ico e="🤝" /></Chip>
</div> </div>
</Section> </Section>
); );
@@ -344,7 +334,7 @@ export function OurBelief() {
return ( return (
<Section id="our-belief" variant="night" size="normal" width="wide"> <Section id="our-belief" variant="night" size="normal" width="wide">
<div className="mx-auto flex flex-col items-center text-center"> <div className="mx-auto flex flex-col items-center text-center">
<Pill bg="#0a0a0a" text={C.yellow}>🌱 our belief</Pill> <Pill bg="#0a0a0a" text={C.yellow}><Ico e="🌱" /> our belief</Pill>
</div> </div>
{/* The thesis, lit word by word as it scrolls past. */} {/* The thesis, lit word by word as it scrolls past. */}
@@ -388,7 +378,7 @@ export function ThePromise() {
return ( return (
<Section id="the-promise" variant="cream" size="full" width="wide" align="center"> <Section id="the-promise" variant="cream" size="full" width="wide" align="center">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<Pill bg="#0a0a0a" text={C.yellow}> the promise</Pill> <Pill bg="#0a0a0a" text={C.yellow}><Ico e="⚡" /> the promise</Pill>
<Logo /> <Logo />
</div> </div>
<h2 className="mt-8 font-sora text-6xl font-extrabold leading-[0.9] tracking-tight sm:text-7xl"> <h2 className="mt-8 font-sora text-6xl font-extrabold leading-[0.9] tracking-tight sm:text-7xl">
@@ -398,15 +388,15 @@ export function ThePromise() {
</h2> </h2>
<div className="mt-8 flex flex-wrap gap-3"> <div className="mt-8 flex flex-wrap gap-3">
{chips.map(([t, bg, tc]) => ( {chips.map(([t, bg, tc]) => (
<Chip key={t} bg={bg} text={tc}>{t}</Chip> <Chip key={t} bg={bg} text={tc}>{withIcons(t, 15)}</Chip>
))} ))}
</div> </div>
<div className="mt-10 flex flex-col items-start justify-between gap-6 sm:flex-row sm:items-center"> <div className="mt-10 flex flex-col items-start justify-between gap-6 sm:flex-row sm:items-center">
<Chip bg={C.purple} text="#fff"> <Chip bg={C.purple} text="#fff">
👉 Earn. Spend. Live More. <span className="ml-1 text-[#FFD21E]"> the rest is just coupons.</span> <Ico e="👉" /> Earn. Spend. Live More. <span className="ml-1 text-[#FFD21E]"> the rest is just coupons.</span>
</Chip> </Chip>
<span className="nb flex h-28 w-28 shrink-0 rotate-6 items-center justify-center rounded-full bg-[#C8FF37] text-center font-sora text-sm font-extrabold text-[#0a0a0a]"> <span className="nb flex h-28 w-28 shrink-0 rotate-6 items-center justify-center rounded-full bg-[#C8FF37] text-center font-sora text-sm font-extrabold text-[#0a0a0a]">
join the LYTIANS 🤝 join the LYTIANS <Ico e="🤝" />
</span> </span>
</div> </div>
</Section> </Section>