feat: update story sections, header, footer, styles, and add gsapkit component

This commit is contained in:
R-Bharathraj
2026-07-21 05:30:43 +05:30
parent 53547a03bf
commit e41094a6fd
7 changed files with 837 additions and 261 deletions

View File

@@ -70,13 +70,33 @@
--hdr-gutter: 48px; --hdr-gutter: 48px;
} }
/* Phones — full-bleed sections, tighter header padding */ /* Phones — full-bleed sections, tighter header padding, neat eyebrow pills */
@media (max-width: 600px) { @media (max-width: 600px) {
:root { :root {
--sec-gutter: 12px; --sec-gutter: 10px;
--sec-gap: 12px; --sec-gap: 10px;
--hdr-gutter: 20px; --hdr-gutter: 20px;
} }
/* All stacked sections use a 20px (m-5) margin on desktop; tighten to 10px
on phones so side + vertical gaps match the request. */
section.m-5 {
margin: 10px !important;
}
/* Eyebrow "Pill" badges — rounded-full + wrapped text turns them into tall
ovals on phones. Shrink type/padding, use a neat rounded-rectangle radius,
and keep the label to one line so they read as tidy tags. */
.story-pill {
padding: 8px 14px !important;
font-size: 12.5px !important;
line-height: 1.1 !important;
letter-spacing: 0.02em !important;
gap: 7px !important;
border-radius: 14px !important;
border-width: 2.5px !important;
box-shadow: 4px 4px 0 0 #0a0a0a !important;
white-space: nowrap !important;
}
} }
/* Custom Scrollbar — neobrutalist, on-theme (no track border) */ /* Custom Scrollbar — neobrutalist, on-theme (no track border) */

View File

@@ -1,10 +1,12 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; 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';
const INK = '#0a0a0a'; const INK = '#0a0a0a';
const BLUE = '#2F6BFF';
const NAV = [ const NAV = [
{ {
@@ -51,8 +53,20 @@ const SOCIALS = [
}, },
]; ];
/* Kinetic ticker phrases + the giant wordmark letters */
const TICKER = ['Earn', 'Spend', 'Live More', 'Make Everyday Count', 'Join The Loop', 'Real Value'];
function Star({ size = 22, color = INK }: { size?: number; color?: string }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill={color} aria-hidden="true" style={{ flexShrink: 0 }}>
<path d="M12 0l2.5 8.2L22 6l-5.4 6.2L22 18l-7.5-2.2L12 24l-2.5-8.2L2 18l5.4-6.2L2 6l7.5 2.2z" />
</svg>
);
}
export default function Footer() { export default function Footer() {
const [showScroll, setShowScroll] = useState(false); const [showScroll, setShowScroll] = useState(false);
const rootRef = useRef<HTMLElement>(null);
useEffect(() => { useEffect(() => {
const onScroll = () => setShowScroll(window.scrollY > 400); const onScroll = () => setShowScroll(window.scrollY > 400);
@@ -60,46 +74,82 @@ export default function Footer() {
return () => window.removeEventListener('scroll', onScroll); return () => window.removeEventListener('scroll', onScroll);
}, []); }, []);
useGSAP(
() => {
syncLenisScrollTrigger();
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// Infinite marquee ticker (two identical halves → seamless).
if (!reduce) {
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 });
}
},
{ scope: rootRef },
);
return ( return (
<> <>
<footer className="lu-footer bg-night font-sora" style={{ <footer ref={rootRef} className="lu-footer font-sora" style={{
color: '#fff', background: '#FFFDF5',
color: INK,
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
margin: '0 var(--sec-gutter) var(--sec-gap)', margin: '0 var(--sec-gutter) var(--sec-gap)',
border: `4px solid ${INK}`, border: `4px solid ${INK}`,
borderRadius: 28, borderRadius: 28,
boxShadow: `0 12px 0 0 rgba(0,0,0,0.35)`, boxShadow: '0 12px 0 0 rgba(0,0,0,0.35)',
}}> }}>
{/* ── KINETIC TICKER STRIP ── */}
<div className="foot-ticker" style={{
overflow: 'hidden',
background: '#C8FF37',
borderBottom: `4px solid ${INK}`,
}}>
<div className="foot-ticker-track" style={{ display: 'flex', width: 'max-content', willChange: 'transform' }}>
{[0, 1].map((half) => (
<div key={half} aria-hidden={half === 1} style={{ display: 'flex', alignItems: 'center' }}>
{TICKER.map((t, i) => (
<span key={`${half}-${i}`} style={{ display: 'inline-flex', alignItems: 'center', gap: 22, padding: '13px 22px' }}>
<span style={{ fontSize: 'clamp(18px, 2.4vw, 30px)', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '-0.01em', color: INK, whiteSpace: 'nowrap' }}>{t}</span>
<Star size={20} />
</span>
))}
</div>
))}
</div>
</div>
{/* ── BIG CTA STICKER CARD ── */} {/* ── BIG CTA STICKER CARD ── */}
<div className="lu-cta-wrap" style={{ padding: '56px 48px 40px' }}> <div className="lu-cta-wrap" style={{ padding: '56px 48px 40px' }}>
<div className="lu-cta bg-cream" style={{ <div className="lu-cta" style={{
position: 'relative', position: 'relative',
color: INK, background: BLUE,
color: '#fff',
border: `4px solid ${INK}`, border: `4px solid ${INK}`,
borderRadius: 24, borderRadius: 24,
boxShadow: `8px 8px 0 0 ${INK}`, boxShadow: `8px 8px 0 0 ${INK}`,
padding: '44px 48px', padding: '48px 48px',
display: 'flex', alignItems: 'center', justifyContent: 'space-between', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
flexWrap: 'wrap', gap: 28, flexWrap: 'wrap', gap: 28,
overflow: 'hidden', overflow: 'hidden',
}}> }}>
{/* corner sticker */} {/* rotating star sticker */}
<span style={{ <span className="foot-cta-star" aria-hidden style={{ position: 'absolute', top: -20, right: 30, willChange: 'transform' }}>
position: 'absolute', top: -18, right: 34, transform: 'rotate(8deg)', <span style={{ display: 'inline-flex', background: '#FF2E88', border: `3px solid ${INK}`, borderRadius: '50%', padding: 10, boxShadow: `3px 3px 0 0 ${INK}` }}>
background: '#FF2E88', color: '#fff', border: `3px solid ${INK}`, <Star size={22} color="#fff" />
borderRadius: 12, padding: '6px 14px', fontSize: 12, fontWeight: 900, </span>
textTransform: 'uppercase', letterSpacing: '0.05em', boxShadow: `3px 3px 0 0 ${INK}`, </span>
}}>join the loop</span>
<div style={{ maxWidth: 620 }}> <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 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.6vw, 60px)', fontWeight: 800, letterSpacing: '-0.03em', lineHeight: 0.98, margin: 0 }}> <h2 className="lu-cta-heading" style={{ fontSize: 'clamp(32px, 4.8vw, 62px)', fontWeight: 800, letterSpacing: '-0.03em', lineHeight: 0.96, margin: 0 }}>
Turn everyday moments<br /> Turn everyday moments<br />
<span style={{ position: 'relative', display: 'inline-block' }}> into <span style={{ position: 'relative', display: 'inline-block', color: '#FF2E88' }}>
into <span style={{ color: '#FF2E88' }}>real value.</span> real value.
<span aria-hidden style={{ position: 'absolute', left: 0, right: 0, bottom: 4, height: 6, background: '#C8FF37', zIndex: -1, borderRadius: 4 }} />
</span> </span>
</h2> </h2>
</div> </div>
@@ -112,7 +162,7 @@ export default function Footer() {
boxShadow: `5px 5px 0 0 ${INK}`, whiteSpace: 'nowrap', boxShadow: `5px 5px 0 0 ${INK}`, whiteSpace: 'nowrap',
transition: 'transform 0.12s ease, box-shadow 0.12s ease', transition: 'transform 0.12s ease, box-shadow 0.12s ease',
}}> }}>
Get Started Download Now
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</Link> </Link>
</div> </div>
@@ -122,20 +172,18 @@ export default function Footer() {
<div className="lu-columns" style={{ <div className="lu-columns" style={{
display: 'grid', display: 'grid',
gridTemplateColumns: '1.6fr 1fr 1fr 1fr', gridTemplateColumns: '1.6fr 1fr 1fr 1fr',
gap: 40, padding: '24px 48px 44px', gap: 40, padding: '24px 48px 36px',
alignItems: 'start', alignItems: 'start',
}}> }}>
{/* Brand */} {/* Brand */}
<div className="lu-brand" style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}> <div className="lu-brand" style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
<Link href="#intro" aria-label="LytsUp home" style={{ <Link href="#intro" aria-label="LytsUp home" style={{
display: 'inline-flex', marginBottom: 20, textDecoration: 'none', display: 'inline-flex', marginBottom: 22, textDecoration: 'none',
background: '#fff', border: `3px solid ${INK}`, borderRadius: 14,
padding: '10px 14px', boxShadow: `4px 4px 0 0 ${INK}`,
}}> }}>
<Image src="/images/lytsup_app.png" alt="LytsUp" width={2048} height={554} style={{ height: 30, width: 'auto', display: 'block' }} /> <Image src="/images/lytsup_app.png" alt="LytsUp" width={2048} height={554} style={{ height: 52, width: 'auto', display: 'block' }} />
</Link> </Link>
<p style={{ fontSize: 14.5, lineHeight: 1.7, color: 'rgba(255,255,255,0.6)', margin: '0 0 24px', maxWidth: 300 }}> <p style={{ fontSize: 15, lineHeight: 1.6, color: 'rgba(10,10,10,0.6)', margin: '0 0 24px', maxWidth: 300 }}>
Social currency for real life. Earn LYTs for the things you already do then spend them on stuff you actually want. Offline shopping. Online intelligence.
</p> </p>
<div style={{ display: 'flex', gap: 12 }}> <div style={{ display: 'flex', gap: 12 }}>
{SOCIALS.map(({ label, href, icon, bg }) => ( {SOCIALS.map(({ label, href, icon, bg }) => (
@@ -162,9 +210,9 @@ export default function Footer() {
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 12 }}> <ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 12 }}>
{col.links.map(({ label, href }) => ( {col.links.map(({ label, href }) => (
<li key={label}> <li key={label}>
<Link href={href} className="lu-link" style={{ textDecoration: 'none', fontSize: 15, color: 'rgba(255,255,255,0.65)', fontWeight: 600, transition: 'color 0.15s, transform 0.15s', display: 'inline-block' }} <Link href={href} className="lu-link" style={{ textDecoration: 'none', fontSize: 15, color: 'rgba(10,10,10,0.62)', fontWeight: 600, transition: 'color 0.15s, transform 0.15s', display: 'inline-block' }}
onMouseEnter={e => { const el = e.currentTarget as HTMLElement; el.style.color = col.color; el.style.transform = 'translateX(4px)'; }} onMouseEnter={e => { const el = e.currentTarget as HTMLElement; el.style.color = INK; el.style.transform = 'translateX(4px)'; }}
onMouseLeave={e => { const el = e.currentTarget as HTMLElement; el.style.color = 'rgba(255,255,255,0.65)'; el.style.transform = 'translateX(0)'; }} onMouseLeave={e => { const el = e.currentTarget as HTMLElement; el.style.color = 'rgba(10,10,10,0.62)'; el.style.transform = 'translateX(0)'; }}
>{label}</Link> >{label}</Link>
</li> </li>
))} ))}
@@ -178,8 +226,8 @@ export default function Footer() {
display: 'flex', alignItems: 'center', justifyContent: 'space-between', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
flexWrap: 'wrap', gap: 14, flexWrap: 'wrap', gap: 14,
padding: '18px 48px 30px', padding: '18px 48px 30px',
borderTop: `2px dashed rgba(255,255,255,0.2)`, borderTop: '2px dashed rgba(10,10,10,0.2)',
fontSize: 13, color: 'rgba(255,255,255,0.5)', fontSize: 13, color: 'rgba(10,10,10,0.55)',
}}> }}>
<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
@@ -188,8 +236,8 @@ export default function Footer() {
<div style={{ display: 'flex', gap: 20 }}> <div style={{ display: 'flex', gap: 20 }}>
{[['Terms', '#'], ['Privacy', '#'], ['Cookies', '#']].map(([l, h]) => ( {[['Terms', '#'], ['Privacy', '#'], ['Cookies', '#']].map(([l, h]) => (
<Link key={l} href={h} style={{ textDecoration: 'none', color: 'inherit', fontWeight: 600, transition: 'color 0.15s' }} <Link key={l} href={h} style={{ textDecoration: 'none', color: 'inherit', fontWeight: 600, transition: 'color 0.15s' }}
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.color = '#FFC72C')} onMouseEnter={e => ((e.currentTarget as HTMLElement).style.color = INK)}
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.color = 'rgba(255,255,255,0.5)')} onMouseLeave={e => ((e.currentTarget as HTMLElement).style.color = 'rgba(10,10,10,0.55)')}
>{l}</Link> >{l}</Link>
))} ))}
</div> </div>
@@ -203,7 +251,7 @@ export default function Footer() {
.lu-cta-wrap { padding: 32px 20px 24px !important; } .lu-cta-wrap { padding: 32px 20px 24px !important; }
.lu-cta { padding: 32px 26px !important; align-items: flex-start !important; } .lu-cta { padding: 32px 26px !important; align-items: flex-start !important; }
.lu-cta-btn { width: 100%; justify-content: center; } .lu-cta-btn { width: 100%; justify-content: center; }
.lu-columns { grid-template-columns: 1fr 1fr !important; gap: 32px 24px !important; padding: 16px 24px 36px !important; } .lu-columns { grid-template-columns: 1fr 1fr !important; gap: 32px 24px !important; padding: 16px 24px 28px !important; }
.lu-brand { grid-column: 1 / -1; } .lu-brand { grid-column: 1 / -1; }
.lu-bottom { padding: 18px 24px 26px !important; flex-direction: column !important; align-items: flex-start !important; } .lu-bottom { padding: 18px 24px 26px !important; flex-direction: column !important; align-items: flex-start !important; }
} }

View File

@@ -5,11 +5,14 @@ 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 NAV_LINKS = [ const NAV_LINKS = [
{ label: 'Earn', href: '#for-you', color: '#C8FF37' }, // lime { label: 'Earn', href: '#for-you', 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 },
]; ];
const MOBILE_QUERY = '(max-width: 1023px)'; const MOBILE_QUERY = '(max-width: 1023px)';
@@ -18,15 +21,16 @@ export default function Header() {
const [mobileOpen, setMobileOpen] = useState(false); const [mobileOpen, setMobileOpen] = useState(false);
const [scrolled, setScrolled] = useState(false); const [scrolled, setScrolled] = useState(false);
const [hidden, setHidden] = useState(false); const [hidden, setHidden] = useState(false);
const [activeHash, setActiveHash] = useState('');
const mobileMenuRef = useRef<HTMLDivElement>(null); const mobileMenuRef = useRef<HTMLDivElement>(null);
const lastScrollYRef = useRef(0); const lastScrollYRef = useRef(0);
// Hide on scroll-down, reveal on scroll-up; deepen the shadow once scrolled. // Deepen the header shadow once scrolled; hide on scroll-down, reveal on up.
useEffect(() => { useEffect(() => {
lastScrollYRef.current = window.scrollY; lastScrollYRef.current = window.scrollY;
const onScroll = () => { const onScroll = () => {
const y = window.scrollY; const y = window.scrollY;
setScrolled(y > 40); setScrolled(y > 50);
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);
@@ -37,6 +41,27 @@ export default function Header() {
return () => window.removeEventListener('scroll', onScroll); return () => window.removeEventListener('scroll', onScroll);
}, []); }, []);
// Lightweight scroll-spy so the current section's nav link lights up.
useEffect(() => {
const ids = NAV_LINKS.map((l) => l.href.replace('#', ''));
const targets = ids
.map((id) => document.getElementById(id))
.filter((el): el is HTMLElement => !!el);
if (!targets.length) return;
const io = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((e) => e.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
if (visible) setActiveHash(`#${visible.target.id}`);
},
{ rootMargin: '-45% 0px -45% 0px', threshold: [0, 0.25, 0.5] },
);
targets.forEach((t) => io.observe(t));
return () => io.disconnect();
}, []);
useEffect(() => { useEffect(() => {
if (mobileOpen) setHidden(false); if (mobileOpen) setHidden(false);
}, [mobileOpen]); }, [mobileOpen]);
@@ -66,6 +91,8 @@ export default function Header() {
return () => mql.removeEventListener('change', update); return () => mql.removeEventListener('change', update);
}, []); }, []);
const isActive = (href: string) => href === activeHash;
return ( return (
<div suppressHydrationWarning> <div suppressHydrationWarning>
{/* Mobile menu overlay */} {/* Mobile menu overlay */}
@@ -77,7 +104,7 @@ export default function Header() {
/> />
)} )}
{/* Mobile slide-in panel — night dot-grid, neobrutalist links */} {/* Mobile slide-in panel — funky sticker links */}
<div <div
ref={mobileMenuRef} ref={mobileMenuRef}
role="dialog" role="dialog"
@@ -99,10 +126,8 @@ export default function Header() {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: '28px', gap: '28px',
// Border + shadow only while open, so nothing bleeds onto the screen's
// left edge when the panel is parked off-screen.
borderRight: mobileOpen ? `4px solid ${INK}` : 'none', borderRight: mobileOpen ? `4px solid ${INK}` : 'none',
boxShadow: mobileOpen ? `8px 0 0 0 rgba(0,0,0,0.35)` : 'none', boxShadow: mobileOpen ? '8px 0 0 0 rgba(0,0,0,0.35)' : 'none',
}} }}
> >
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
@@ -149,7 +174,7 @@ export default function Header() {
</nav> </nav>
<div> <div>
<p className="font-sora" style={{ margin: '0 0 12px', fontWeight: 800, textTransform: 'uppercase', fontSize: '11px', letterSpacing: '0.12em', color: '#FFC72C' }}> <p className="font-sora" style={{ margin: '0 0 12px', fontWeight: 800, textTransform: 'uppercase', fontSize: '11px', letterSpacing: '0.12em', color: GOLD }}>
Follow us Follow us
</p> </p>
<div style={{ display: 'flex', gap: '12px' }}> <div style={{ display: 'flex', gap: '12px' }}>
@@ -176,7 +201,7 @@ export default function Header() {
display: 'block', display: 'block',
marginTop: 'auto', marginTop: 'auto',
padding: '16px 24px', padding: '16px 24px',
background: '#FFC72C', background: GOLD,
color: INK, color: INK,
border: `3px solid ${INK}`, border: `3px solid ${INK}`,
borderRadius: 14, borderRadius: 14,
@@ -188,29 +213,26 @@ export default function Header() {
boxShadow: `5px 5px 0 0 ${INK}`, boxShadow: `5px 5px 0 0 ${INK}`,
}} }}
> >
Get Started Download Now
</Link> </Link>
</div> </div>
{/* ——— Funky neobrutalist floating bar ——— */} {/* ——— Header: cream neobrutalist bar, loyaly layout ——— */}
<header <header
style={{ style={{
position: 'fixed', position: 'fixed',
// Sections are inset 20px (m-5) from the viewport. Sit the bar that far top: isMobileViewport ? '16px' : 0,
// down again plus a section-sized gap, so it floats *inside* the first left: '20px',
// section with the same breathing room sections give each other. right: '20px',
top: 'calc(20px + var(--sec-gap))', width: 'auto',
left: 0,
right: 0,
width: isMobileViewport ? 'auto' : '70%',
maxWidth: 'calc(100% - 2 * var(--hdr-gutter))',
marginLeft: isMobileViewport ? 'var(--hdr-gutter)' : 'auto',
marginRight: isMobileViewport ? 'var(--hdr-gutter)' : 'auto',
zIndex: 99999999, zIndex: 99999999,
background: '#FFFDF5', background: CREAM,
border: `3px solid ${INK}`, border: `3px solid ${INK}`,
borderRadius: '20px', borderTopLeftRadius: isMobileViewport ? '22px' : '0px',
boxShadow: scrolled ? `8px 8px 0 0 ${INK}` : `5px 6px 0 0 ${INK}`, borderTopRightRadius: isMobileViewport ? '22px' : '0px',
borderBottomLeftRadius: '22px',
borderBottomRightRadius: '22px',
boxShadow: scrolled ? `0 9px 0 0 ${INK}` : `0 6px 0 0 ${INK}`,
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), box-shadow 0.25s ease',
}} }}
@@ -219,68 +241,75 @@ export default function Header() {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
padding: '0 22px', padding: '0 var(--hdr-gutter)',
height: '72px', height: '84px',
width: '100%', width: '100%',
boxSizing: 'border-box', boxSizing: 'border-box',
gap: 16, gap: 16,
}}> }}>
{/* Logo */} {/* Logo */}
<div style={{ 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' }}> <Link href="#intro" aria-label="LytsUp home" style={{ display: 'inline-flex', alignItems: 'center', textDecoration: 'none' }}>
<Image <Image
src="/images/lytsup_app.png" src="/images/lytsup_app.png"
alt="LytsUp" alt="LytsUp"
width={2048} width={2048}
height={554} height={554}
priority
className="header-logo-img" className="header-logo-img"
style={{ height: 34, width: 'auto', objectFit: 'contain', display: 'block' }} style={{ objectFit: 'contain', height: 40, width: 'auto' }}
priority
/> />
</Link> </Link>
</div> </div>
{/* Desktop nav — boxy neobrutalist sticker tabs */} {/* Desktop nav — funky tilted sticker tabs, each in its brand colour */}
<nav aria-label="Main navigation" className="desktop-nav"> <nav aria-label="Main navigation" className="desktop-nav">
<ul className="header-nav-row" style={{ <ul className="header-nav-row font-sora" style={{
display: 'flex', display: 'flex',
listStyle: 'none', listStyle: 'none',
margin: 0, margin: 0,
padding: 0, padding: 0,
alignItems: 'center', alignItems: 'center',
gap: 12, gap: 10,
}}> }}>
{NAV_LINKS.map(({ label, href, color }, i) => { {NAV_LINKS.map(({ label, href, color }, i) => {
const tilt = i % 2 === 0 ? -2 : 2; const active = isActive(href);
const tilt = i % 2 === 0 ? -2.5 : 2.5;
return ( return (
<li key={href}> <li key={href}>
<Link <Link
href={href} href={href}
className="header-nav-tab font-sora" className={`header-nav-tab${active ? ' is-active' : ''}`}
style={{ style={{
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
gap: 8, gap: 7,
padding: '8px 16px', padding: '9px 16px',
color: INK, color: active ? color : INK,
textDecoration: 'none', textDecoration: 'none',
fontWeight: 800, fontWeight: 800,
fontSize: '13px', fontSize: '13px',
letterSpacing: '0.05em', letterSpacing: '0.05em',
textTransform: 'uppercase', textTransform: 'uppercase',
borderRadius: '12px', borderRadius: '12px',
background: '#fff', background: active ? INK : color,
border: `2.5px solid ${INK}`, border: `2.5px solid ${INK}`,
boxShadow: `3px 3px 0 0 ${INK}`, boxShadow: `3px 3px 0 0 ${INK}`,
transition: 'transform 0.15s, background 0.15s, box-shadow 0.15s', transition: 'transform 0.14s ease, box-shadow 0.14s ease, background 0.15s, color 0.15s',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
transform: `rotate(${tilt}deg)`, transform: `rotate(${tilt}deg)`,
['--hov' as string]: color,
['--tilt' as string]: `${tilt}deg`, ['--tilt' as string]: `${tilt}deg`,
['--dot' as string]: color,
}} }}
> >
<span <span
style={{ width: 8, height: 8, borderRadius: 3, background: color, border: `1.5px solid ${INK}`, flexShrink: 0 }} aria-hidden
className="header-nav-dot"
style={{
width: 8, height: 8, borderRadius: 999, flexShrink: 0,
background: active ? color : '#fff',
border: `1.5px solid ${INK}`,
}}
/> />
{label} {label}
</Link> </Link>
@@ -291,7 +320,8 @@ export default function Header() {
</nav> </nav>
{/* Right actions */} {/* Right actions */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '12px', flex: 1, justifyContent: 'flex-end' }}>
{/* Download Now — chunky gold sticker button */}
<Link <Link
href="#for-business" href="#for-business"
className="header-cta desktop-nav font-sora" className="header-cta desktop-nav font-sora"
@@ -299,7 +329,7 @@ export default function Header() {
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
padding: '11px 26px', padding: '11px 26px',
background: '#FFC72C', background: GOLD,
color: INK, color: INK,
border: `3px solid ${INK}`, border: `3px solid ${INK}`,
borderRadius: '50px', borderRadius: '50px',
@@ -313,7 +343,7 @@ export default function Header() {
transition: 'transform 0.12s ease, box-shadow 0.12s ease, background 0.15s', transition: 'transform 0.12s ease, box-shadow 0.12s ease, background 0.15s',
}} }}
> >
Get Started Download Now
</Link> </Link>
{/* Hamburger — mobile only */} {/* Hamburger — mobile only */}
@@ -338,23 +368,36 @@ 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: 60px !important; } .header-inner { height: 62px !important; }
.header-logo-img { height: 28px !important; } .header-logo-img { height: 28px !important; }
} }
@media (min-width: 1024px) and (max-width: 1200px) { @media (max-width: 600px) {
.header-nav-tab { padding: 7px 12px !important; font-size: 11px !important; letter-spacing: 0.03em !important; } header { left: 10px !important; right: 10px !important; top: 10px !important; }
}
@media (min-width: 1024px) and (max-width: 1280px) {
.header-nav-row { gap: 7px !important; }
.header-nav-tab { padding: 7px 11px !important; font-size: 11px !important; letter-spacing: 0.03em !important; gap: 5px !important; }
.header-nav-dot { width: 6px !important; height: 6px !important; }
.header-cta { padding: 9px 18px !important; font-size: 12px !important; } .header-cta { padding: 9px 18px !important; font-size: 12px !important; }
.header-logo-img { height: 30px !important; } .header-logo-img { height: 32px !important; }
}
@media (min-width: 1281px) and (max-width: 1440px) {
.header-nav-tab { padding: 8px 14px !important; font-size: 12px !important; }
.header-cta { padding: 10px 22px !important; font-size: 13px !important; }
.header-logo-img { height: 36px !important; }
} }
.header-nav-tab:hover { .header-nav-tab:hover {
background: var(--hov) !important; transform: translateY(-3px) rotate(0deg) !important;
transform: translateY(-3px) rotate(var(--tilt));
box-shadow: 5px 5px 0 0 ${INK} !important; box-shadow: 5px 5px 0 0 ${INK} !important;
} }
.header-nav-tab:active { .header-nav-tab:active {
transform: translateY(1px) rotate(var(--tilt)); transform: translateY(1px) rotate(var(--tilt)) !important;
box-shadow: 1px 1px 0 0 ${INK} !important; box-shadow: 1px 1px 0 0 ${INK} !important;
} }
.header-nav-tab.is-active {
transform: rotate(0deg) !important;
box-shadow: 3px 3px 0 0 var(--dot) !important;
}
.header-cta:hover { .header-cta:hover {
background: #FFD95A !important; background: #FFD95A !important;
transform: translate(-2px, -2px); transform: translate(-2px, -2px);

View File

@@ -1,133 +1,231 @@
"use client";
import { useRef } from "react";
import { import {
Section, Section,
Pill, Pill,
Card, Card,
Chip, Chip,
Heading, Heading,
Group,
Reveal, Reveal,
SplitWords,
Magnetic, Magnetic,
Parallax, Parallax,
} from "./ui"; } from "./ui";
import { C } from "./palette"; import { C } from "./palette";
import {
gsap,
ScrollTrigger,
useGSAP,
syncLenisScrollTrigger,
usePointerParallax,
MaskWords,
} from "./gsapkit";
/* 01 — INTRO / HERO */ /* 01 — INTRO / HERO
GSAP: masked line-reveal on load · scroll-scrubbed depth parallax on the
ambient layers · pointer parallax on the floating reward cluster. */
export function Intro() { export function Intro() {
const clusterRef = useRef<HTMLDivElement>(null);
useGSAP(() => {
syncLenisScrollTrigger();
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* ── load timeline ─────────────────────────────────────── */
if (reduce) {
gsap.set("#intro .hero-line-in, #intro .hero-fade, #intro .hero-object", { opacity: 1, y: 0, yPercent: 0, scale: 1 });
} else {
const tl = gsap.timeline({ defaults: { ease: "power4.out" } });
tl.from("#intro .hero-pill", { y: 18, opacity: 0, duration: 0.6 })
.from("#intro .hero-eyebrow p", { y: 14, opacity: 0, stagger: 0.08, duration: 0.6 }, "-=0.3")
.from("#intro .hero-line-in", { yPercent: 118, rotationX: -55, opacity: 0, stagger: 0.12, duration: 1.0, transformOrigin: "0% 100%" }, "-=0.2")
.from("#intro .hero-underline", { scaleX: 0, transformOrigin: "0% 50%", duration: 0.5, ease: "power2.out" }, "-=0.35")
.from("#intro .hero-chip", { y: 14, opacity: 0, duration: 0.55 }, "-=0.35")
.from("#intro .hero-cta", { y: 14, opacity: 0, stagger: 0.08, duration: 0.5 }, "-=0.3")
.from("#intro .hero-strip", { y: 14, opacity: 0, duration: 0.5 }, "-=0.3")
.from("#intro .hero-object", { scale: 0, opacity: 0, rotation: "+=18", transformOrigin: "50% 50%", stagger: 0.1, ease: "back.out(1.7)", duration: 0.7 }, "-=0.95");
/* ── scroll-scrubbed parallax as the hero leaves ───────── */
const st = { trigger: "#intro", start: "top top", end: "bottom top", scrub: 0.6 } as const;
gsap.to("#intro .hero-watermark", { yPercent: -22, ease: "none", scrollTrigger: st });
gsap.to("#intro .hero-glow", { yPercent: (i) => -14 - i * 8, ease: "none", scrollTrigger: st });
gsap.to("#intro .hero-copy", { yPercent: -8, opacity: 0.35, ease: "none", scrollTrigger: st });
gsap.utils.toArray<HTMLElement>("#intro .hero-object").forEach((el) => {
gsap.to(el, { yPercent: -30 * parseFloat(el.dataset.speed || "1"), ease: "none", scrollTrigger: st });
});
}
}, []);
usePointerParallax(clusterRef, ".hero-object");
return ( return (
<Section id="intro" variant="deep" size="epic" width="wide" align="start"> <Section id="intro" variant="deep" size="epic" width="wide" align="start">
{/* animated ambient glows — soft washes, heavily blurred so no hard edge shows */} {/* animated ambient glows — soft washes, heavily blurred so no hard edge shows */}
<span aria-hidden className="animate-pulse-glow pointer-events-none absolute -left-40 -top-16 -z-0 h-[30rem] w-[30rem] rounded-full bg-[#FF2E88]/15 blur-[150px]" /> <span aria-hidden className="hero-glow animate-pulse-glow pointer-events-none absolute -left-40 -top-16 -z-0 h-[30rem] w-[30rem] rounded-full bg-[#FF2E88]/15 blur-[150px]" />
<span aria-hidden className="animate-float pointer-events-none absolute -bottom-16 left-1/4 -z-0 h-[28rem] w-[28rem] rounded-full bg-[#8B3DFF]/20 blur-[150px]" /> <span aria-hidden className="hero-glow animate-float pointer-events-none absolute -bottom-16 left-1/4 -z-0 h-[28rem] w-[28rem] rounded-full bg-[#8B3DFF]/20 blur-[150px]" />
<span aria-hidden className="animate-float pointer-events-none absolute -right-16 top-1/3 -z-0 h-[26rem] w-[26rem] rounded-full bg-[#C8FF37]/10 blur-[140px]" style={{ animationDelay: "1.5s" }} /> <span aria-hidden className="hero-glow animate-float pointer-events-none absolute -right-16 top-1/3 -z-0 h-[26rem] w-[26rem] rounded-full bg-[#C8FF37]/10 blur-[140px]" style={{ animationDelay: "1.5s" }} />
{/* giant watermark — drifts against the scroll for depth */} {/* giant watermark — drifts against the scroll for depth */}
<Parallax <div
speed={120} aria-hidden
className="pointer-events-none absolute right-0 top-1/2 -z-0 -translate-y-1/2 select-none" className="hero-watermark pointer-events-none absolute left-1/2 top-[58%] -z-0 -translate-x-1/2 -translate-y-1/2 select-none"
> >
<span className="font-sora text-[28vw] font-extrabold leading-none text-white/[0.06]"> <span className="font-sora text-[30vw] font-extrabold leading-none text-white/[0.05]">
LYTS LYTS
</span> </span>
</Parallax> </div>
<div className="relative z-10 pt-24 sm:pt-28 lg:pt-32"> {/* dotted grid — top right accent */}
{/* eyebrow badge */} <div
<Reveal> aria-hidden
<span className="nb inline-flex items-center gap-2.5 rounded-full bg-[#C8FF37] px-5 py-2.5 font-sora text-sm font-extrabold uppercase tracking-wide text-[#0a0a0a]"> className="pointer-events-none absolute right-10 top-10 hidden -z-0 h-24 w-40 opacity-60 lg:block"
<span className="relative flex h-2.5 w-2.5"> style={{
backgroundImage: "radial-gradient(#C8FF37 1.6px, transparent 1.6px)",
backgroundSize: "18px 18px",
}}
/>
<div className="relative z-10 grid items-center gap-14 pt-20 sm:pt-24 lg:grid-cols-[1.05fr_0.95fr] lg:pt-12">
{/* ── LEFT: message column ─────────────────────────────── */}
<div className="hero-copy">
{/* status pill */}
<span className="hero-pill nb-sm inline-flex items-center gap-2 rounded-full bg-[#C8FF37] px-4 py-2 font-sora text-xs font-extrabold uppercase tracking-wider text-[#0a0a0a]">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#FF2E88] opacity-75" /> <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#FF2E88] opacity-75" />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-[#FF2E88]" /> <span className="relative inline-flex h-2 w-2 rounded-full bg-[#FF2E88]" />
</span> </span>
social currency, reimagined now in early access
</span> </span>
</Reveal>
{/* eyebrow copy */} {/* eyebrow copy */}
<Reveal delay={0.08}> <div
<div className="mt-6 space-y-1 font-bold text-[#FFD21E] sm:text-lg"> className="hero-eyebrow mt-6 space-y-1 bg-clip-text font-bold text-transparent sm:text-lg"
<p>we built this for you.</p> style={{ backgroundImage: "linear-gradient(180deg, #FF4D9E 0%, #FF7A59 100%)" }}
<p> >
social currency is value earned for <em className="italic">participating</em> in life. <p>we built this for you </p>
</p> <p>the one who keeps showing up</p>
<p>a LYTIAN believes every meaningful interaction has value.</p> <p>and never gets a thank-you.</p>
</div> </div>
</Reveal>
{/* headline — line-by-line reveal, shimmering last line */} {/* headline — masked line reveal, "real life." shimmers, "real" underlined */}
<h1 className="mt-8 font-sora text-6xl font-extrabold leading-[0.88] tracking-tight text-white sm:text-7xl lg:text-8xl"> <h1 className="mt-7 font-sora text-6xl font-extrabold leading-[0.86] tracking-tight text-white sm:text-7xl lg:text-8xl [perspective:800px]">
<span className="block"> <span className="block overflow-hidden pb-[0.06em]"><span className="hero-line-in inline-block">social</span></span>
<SplitWords text="social" delay={0.14} />{" "} <span className="block overflow-hidden pb-[0.06em]"><span className="hero-line-in inline-block">currency</span></span>
</span> <span className="block overflow-hidden pb-[0.06em]">
<span className="block"> <span className="hero-line-in inline-block">
<SplitWords text="currency" delay={0.24} />{" "} for{" "}
</span> <span className="animate-shimmer bg-gradient-to-r from-[#FFC72C] via-[#FF2E88] to-[#8B3DFF] bg-[length:200%_auto] bg-clip-text text-transparent">
<span className="block"> <span className="relative inline-block">
<SplitWords text="for" delay={0.34} />{" "} real
<span className="animate-shimmer bg-gradient-to-r from-[#FFC72C] via-[#FF2E88] to-[#8B3DFF] bg-[length:200%_auto] bg-clip-text text-transparent"> <svg
<SplitWords text="real life." delay={0.42} /> aria-hidden
viewBox="0 0 120 12"
preserveAspectRatio="none"
className="hero-underline absolute -bottom-1 left-0 h-2.5 w-full text-[#C8FF37]"
fill="none"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
>
<path d="M2 8 C 20 2, 40 12, 60 6 S 100 2, 118 7" />
</svg>
</span>{" "}
life.
</span>
</span>
</span> </span>
</span> </h1>
</h1>
{/* CTA buttons */} {/* tagline chip */}
<Reveal delay={0.38}> <div className="hero-chip mt-8">
<div className="mt-9 flex flex-col items-start gap-4 sm:flex-row sm:items-center"> <span className="nb-sm inline-flex flex-wrap items-center gap-x-2 rounded-xl bg-[#0a0a0a] px-4 py-2.5 font-sora text-sm font-extrabold">
<Magnetic> <span className="text-[#C8FF37]">&quot;earn. spend. live more.&quot;</span>
<span className="font-semibold text-white/90">
a quiet promise, not a promise.
</span>
</span>
</div>
{/* CTAs */}
<div className="mt-9 flex flex-wrap items-center gap-4">
<Magnetic className="hero-cta">
<a <a
href="#for-business" href="#for-you"
className="nb nb-hover inline-flex items-center gap-2 rounded-full bg-[#FFC72C] px-8 py-4 font-sora text-base font-extrabold uppercase tracking-wide text-[#0a0a0a] sm:text-lg" 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
<span aria-hidden></span> <span aria-hidden></span>
</a> </a>
</Magnetic> </Magnetic>
<Magnetic> <a
<a href="#difference"
href="#how-it-works" className="hero-cta group inline-flex items-center gap-2 font-sora text-base font-bold text-white/90 transition-colors hover:text-[#C8FF37]"
className="nb nb-hover inline-flex items-center gap-2 rounded-full bg-white px-8 py-4 font-sora text-base font-extrabold uppercase tracking-wide text-[#0a0a0a] sm:text-lg" >
> see how it works
How it works <span aria-hidden className="transition-transform group-hover:translate-x-1"></span>
</a> </a>
</Magnetic>
</div> </div>
</Reveal>
{/* tagline chip */} {/* credibility strip */}
<Reveal delay={0.46}> <div className="hero-strip mt-10 flex flex-wrap items-center gap-x-8 gap-y-3 font-sora text-sm font-bold text-white/55">
<div className="mt-8"> <span><span className="text-white">12k+</span> LYTIANS earning</span>
<Chip bg="#0a0a0a" text="#fff"> <span className="hidden h-4 w-px bg-white/20 sm:block" />
<span className="text-[#C8FF37]">&quot;earn. spend. live more.&quot;</span> <span><span className="text-white">240</span> partner spots</span>
<span className="ml-2 font-semibold text-white/80"> <span className="hidden h-4 w-px bg-white/20 sm:block" />
a quiet belief that everyday moments matter. <span><span className="text-white">1</span> currency for real life</span>
</span>
</Chip>
</div> </div>
</Reveal> </div>
</div>
{/* floating reward stickers (desktop) */} {/* ── RIGHT: floating brand-object cluster (desktop) ────── */}
<div aria-hidden className="pointer-events-none absolute inset-0 -z-0 hidden lg:block"> <div ref={clusterRef} aria-hidden className="relative hidden h-[30rem] lg:block">
<span className="animate-float nb-sm absolute right-[16%] top-[20%] -rotate-6 rounded-xl bg-[#FF2E88] px-4 py-2 font-sora text-lg font-extrabold text-white"> {/* rotating "make everyday count" badge */}
+50 LYTs <div className="hero-object absolute right-6 top-4" data-depth="0.08" data-speed="0.6">
</span> <div className="animate-float nb flex h-44 w-44 rotate-6 items-center justify-center rounded-full bg-[#FFD21E] text-center font-sora text-lg font-extrabold text-[#0a0a0a]">
<span className="animate-float nb-sm absolute right-[30%] top-[52%] rotate-6 rounded-xl bg-white px-4 py-2 font-sora text-lg font-extrabold text-[#0a0a0a]" style={{ animationDelay: "0.8s" }}> <div>
🔥 7-day streak <div className="text-xs font-bold uppercase tracking-widest text-[#FF2E88]">psst</div>
</span> make everyday
<span className="animate-float nb-sm absolute bottom-[16%] right-[20%] -rotate-3 rounded-xl bg-[#C8FF37] px-4 py-2 font-sora text-lg font-extrabold text-[#0a0a0a]" style={{ animationDelay: "1.6s" }}> <br />
🎁 rewards unlocked count. <span className="text-[#FF2E88]"></span>
</span> </div>
</div> </div>
</div>
{/* rotating "make everyday count" badge — pinned to the right, vertically centered */} {/* reward stickers */}
<div className="absolute right-8 top-1/2 hidden -translate-y-1/2 lg:block"> <div className="hero-object absolute left-4 top-24" data-depth="0.16" data-speed="1.4">
<div className="animate-float nb flex h-40 w-40 rotate-6 items-center justify-center rounded-full bg-[#FFD21E] text-center font-sora text-lg font-extrabold text-[#0a0a0a]"> <span className="animate-float nb-sm inline-block -rotate-6 rounded-xl bg-[#FF2E88] px-4 py-2 font-sora text-lg font-extrabold text-white">
<div> +50 LYTS
<div className="text-xs font-bold uppercase tracking-widest text-[#FF2E88]">psst</div> </span>
make everyday count. </div>
<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" }}>
🔥 7-day streak
</span>
</div>
<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" }}>
🎁 reward unlocked
</span>
</div>
{/* hand-drawn arrow linking sticker to badge */}
<div className="hero-object pointer-events-none absolute left-1/2 top-16" data-depth="0.1" data-speed="0.8">
<svg
viewBox="0 0 80 80"
className="animate-float h-14 w-14 text-[#C8FF37]"
fill="none"
stroke="currentColor"
strokeWidth="4"
strokeLinecap="round"
strokeLinejoin="round"
style={{ animationDelay: "0.4s" }}
>
<path d="M12 60 C 30 40, 46 26, 66 14" />
<path d="M46 12 L 68 12 L 68 34" />
</svg>
</div> </div>
</div> </div>
</div> </div>
</Section> </Section>
); );
} }
@@ -182,8 +280,43 @@ export function TheWhy() {
); );
} }
/* 03 — THE PROBLEM */ /* 03 — THE PROBLEM
GSAP: the three cards deal in like cards flipped onto a table — a 3D
rotationY/lift entrance scrubbed to scroll, with each number badge
popping as its card lands. */
export function TheProblem() { export function TheProblem() {
useGSAP(() => {
syncLenisScrollTrigger();
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return;
/* Play-once on enter: the cards deal in with a gentle 3D lift, then settle
perfectly flat and aligned (transforms cleared) so nothing lingers
skewed or overlaps the note below. */
const tl = gsap.timeline({
scrollTrigger: { trigger: "#the-problem", start: "top 72%", once: true },
defaults: { ease: "power3.out" },
onComplete: () => gsap.set("#the-problem .prob-card", { clearProps: "transform" }),
});
tl.from("#the-problem .prob-card", {
y: 60,
rotationY: -18,
rotationX: 8,
opacity: 0,
transformOrigin: "50% 100%",
stagger: 0.14,
duration: 0.9,
})
.from("#the-problem .prob-num", {
scale: 0,
rotation: -20,
transformOrigin: "50% 50%",
stagger: 0.14,
duration: 0.5,
ease: "back.out(2.2)",
}, 0.2);
}, []);
const cards = [ const cards = [
{ n: "01", t: "starts too late.", bg: "#fff", tc: "#0a0a0a", items: ["Visits a store", "Discovers a brand", "Refers a friend", "Attends an event", "Joins a community"], foot: "= $0 value today" }, { n: "01", t: "starts too late.", bg: "#fff", tc: "#0a0a0a", items: ["Visits a store", "Discovers a brand", "Refers a friend", "Attends an event", "Joins a community"], foot: "= $0 value today" },
{ n: "02", t: "points = useless.", bg: C.pink, tc: "#fff", items: ["Expiring numbers", "Transactional incentives", "Isolated brand currencies", "Complex redemptions", "Zero emotional bond"], foot: "no flex · no vibe." }, { n: "02", t: "points = useless.", bg: C.pink, tc: "#fff", items: ["Expiring numbers", "Transactional incentives", "Isolated brand currencies", "Complex redemptions", "Zero emotional bond"], foot: "no flex · no vibe." },
@@ -197,10 +330,14 @@ export function TheProblem() {
<br /> <br />
<span className="text-[#FFD21E]">here&apos;s why.</span> <span className="text-[#FFD21E]">here&apos;s why.</span>
</Heading> </Heading>
<Group className="mt-10 grid gap-5 md:grid-cols-3"> <div className="mt-10 grid gap-5 [perspective:1200px] md:grid-cols-3">
{cards.map((c) => ( {cards.map((c) => (
<Card key={c.n} group bg={c.bg} text={c.tc} className="flex flex-col p-6"> <div
<div className="mb-2 w-fit rounded-md bg-[#0a0a0a] px-2 py-0.5 text-xs font-extrabold text-white">{c.n}</div> key={c.n}
className="prob-card nb nb-hover flex flex-col rounded-2xl p-6"
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="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) => (
@@ -208,9 +345,9 @@ export function TheProblem() {
))} ))}
</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>
</Card> </div>
))} ))}
</Group> </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}> customers create value before spending. then traditional loyalty assigns zero credit.</Chip>
</div> </div>
@@ -261,29 +398,75 @@ export function TheShift() {
); );
} }
/* 05 — MEET LYTs */ /* 05 — MEET LYTs
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
drifts on a scroll-scrubbed parallax for depth. */
export function MeetLyts() { export function MeetLyts() {
const chars = ["L", "Y", "T", "s", "."];
useGSAP(() => {
syncLenisScrollTrigger();
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return;
const tl = gsap.timeline({
scrollTrigger: { trigger: "#lyts", start: "top 72%" },
defaults: { ease: "power3.out" },
});
tl.from("#lyts .lyts-pill", { y: 16, opacity: 0, duration: 0.5 })
.from("#lyts .lyts-char", {
yPercent: 130,
rotationX: -90,
opacity: 0,
transformOrigin: "50% 100%",
stagger: 0.09,
duration: 1.0,
ease: "back.out(1.5)",
}, "-=0.2")
.from("#lyts .lyts-card", { y: 34, opacity: 0, duration: 0.7 }, "-=0.4")
.from("#lyts .lyts-chip", { scale: 0, opacity: 0, stagger: 0.05, duration: 0.5, ease: "back.out(2)" }, "-=0.3");
/* subtle scroll-scrubbed lift on the giant wordmark */
gsap.to("#lyts .lyts-word", {
yPercent: -12,
ease: "none",
scrollTrigger: { trigger: "#lyts", start: "top bottom", end: "bottom top", scrub: 0.6 },
});
}, []);
return ( return (
<Section id="lyts" variant="deep" size="normal" width="narrow"> <Section id="lyts" variant="deep" size="normal" width="narrow">
<Pill bg={C.yellow} text="#0a0a0a"> introducing</Pill> <span className="lyts-pill inline-block">
<h2 className="mt-6 font-sora text-8xl font-extrabold tracking-tight text-white">LYTs.</h2> <Pill bg={C.yellow} text="#0a0a0a"> introducing</Pill>
<Card bg="#fff" className="mt-6 max-w-3xl p-6"> </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">
{chars.map((ch, i) => (
<span
key={i}
className={`lyts-char inline-block ${ch === "." ? "text-[#C8FF37]" : ""}`}
>
{ch}
</span>
))}
</h2>
<div className="lyts-card nb nb-hover mt-6 max-w-3xl rounded-2xl bg-white p-6 text-[#0a0a0a]">
<p className="text-lg font-bold"> <p className="text-lg font-bold">
a LYT is <span className="text-[#FF2E88]">a unit of digital social currency</span> you earn by participating in real life. one LYT = one moment of showing up. a LYT is <span className="text-[#FF2E88]">a unit of digital social currency</span> you earn by participating in real life. one LYT = one moment of showing up.
</p> </p>
</Card> </div>
<div className="mt-8 flex flex-wrap items-center gap-2"> <div className="mt-12 flex flex-wrap items-center gap-2">
<Chip>walk 🚶</Chip> <span className="lyts-chip inline-block"><Chip>walk 🚶</Chip></span>
<Chip bg={C.yellow}>visit 📍</Chip> <span className="lyts-chip inline-block"><Chip bg={C.yellow}>visit 📍</Chip></span>
<Chip bg={C.lime}>friend 🤝</Chip> <span className="lyts-chip inline-block"><Chip bg={C.lime}>friend 🤝</Chip></span>
<Chip bg={C.orange} text="#fff">shop 🛍</Chip> <span className="lyts-chip inline-block"><Chip bg={C.orange} text="#fff">shop 🛍</Chip></span>
<Chip bg={C.purple} text="#fff">event 🎉</Chip> <span className="lyts-chip inline-block"><Chip bg={C.purple} text="#fff">event 🎉</Chip></span>
<span></span> <span className="lyts-chip"></span>
<Chip bg="#0a0a0a" text={C.lime}>your LYTs balance</Chip> <span className="lyts-chip inline-block"><Chip bg="#0a0a0a" text={C.lime}>your LYTs balance</Chip></span>
<span></span> <span className="lyts-chip"></span>
<Chip bg={C.orange} text="#fff">spend</Chip> <span className="lyts-chip inline-block"><Chip bg={C.orange} text="#fff">spend</Chip></span>
<span></span> <span className="lyts-chip"></span>
<Chip bg={C.purple} text="#fff">live more</Chip> <span className="lyts-chip inline-block"><Chip bg={C.purple} text="#fff">live more</Chip></span>
</div> </div>
</Section> </Section>
); );
@@ -323,7 +506,9 @@ export function OneDay() {
); );
} }
/* 07 — FEELINGS */ /* 07 — FEELINGS
GSAP: the headline reveals word-by-word out of masks, then the five cards
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"],
@@ -332,60 +517,131 @@ export function Feelings() {
["🪞", "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", "#0a0a0a", C.lime],
] as const; ] as const;
useGSAP(() => {
syncLenisScrollTrigger();
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return;
/* hide immediately (before paint) so nothing flashes before its trigger */
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.to("#feelings .feel-w-in", {
scrollTrigger: { trigger: "#feelings", start: "top 78%", once: true },
yPercent: 0,
rotationZ: 0,
autoAlpha: 1,
stagger: 0.045,
duration: 0.8,
ease: "power4.out",
});
gsap.to("#feelings .feel-card", {
scrollTrigger: { trigger: "#feelings .feel-grid", start: "top 82%", once: true },
clipPath: "inset(0% 0% 0% 0%)",
yPercent: 0,
scale: 1,
autoAlpha: 1,
filter: "blur(0px)",
stagger: 0.1,
duration: 0.95,
ease: "power3.out",
onComplete: () => gsap.set("#feelings .feel-card", { clearProps: "filter,clipPath,transform" }),
});
gsap.to("#feelings .feel-sum", {
scrollTrigger: { trigger: "#feelings .feel-sum", start: "top 88%", once: true },
y: 0,
autoAlpha: 1,
duration: 0.8,
ease: "power3.out",
});
}, []);
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">💗 why social currency</Pill>
<Heading className="mt-6"> <h2 className="mt-6 font-sora text-4xl font-extrabold leading-[1.02] tracking-tight [perspective:800px] sm:text-5xl lg:text-6xl">
people don&apos;t want points. they want <span className="text-[#FF2E88]">feelings.</span> <MaskWords text="people don't want points. they want" wordClass="feel-w-in" />
</Heading> <MaskWords text="feelings." wordClass="feel-w-in" inner="text-[#FF2E88]" />
<Group className="mt-10 grid gap-4 sm:grid-cols-3 lg:grid-cols-5"> </h2>
<div className="feel-grid mt-10 grid gap-4 sm:grid-cols-3 lg:grid-cols-5">
{cards.map(([e, t, s, bg, tc]) => ( {cards.map(([e, t, s, bg, tc]) => (
<Card key={t} group bg={bg} text={tc} className="p-5"> <div
key={t}
className="feel-card nb nb-hover rounded-2xl p-5 [will-change:transform,filter]"
style={{ background: bg, color: tc }}
>
<div className="text-2xl">{e}</div> <div className="text-2xl">{e}</div>
<div className="mt-6 font-sora text-lg font-extrabold">{t}</div> <div className="mt-6 font-sora text-lg font-extrabold">{t}</div>
<div className="text-xs font-bold opacity-80">{s}</div> <div className="text-xs font-bold opacity-80">{s}</div>
</Card> </div>
))} ))}
</Group> </div>
<Card bg={C.purple} text="#fff" className="mt-6 flex flex-col items-start gap-2 p-6 sm:flex-row sm:items-center sm:gap-6"> <div className="feel-sum nb mt-6 flex flex-col items-start gap-2 rounded-2xl p-6 sm:flex-row sm:items-center sm:gap-6" style={{ background: C.purple, color: "#fff" }}>
<div className="font-sora text-6xl font-extrabold text-[#FFD21E]">0/5</div> <div className="font-sora text-6xl font-extrabold text-[#FFD21E]">0/5</div>
<div> <div>
<div className="font-sora font-extrabold tracking-wide">TRADITIONAL LOYALTY SCORES</div> <div className="font-sora font-extrabold tracking-wide">TRADITIONAL LOYALTY SCORES</div>
<div className="text-sm font-semibold text-white/85">On every emotional need above, old-school points-based loyalty = 0%. LYTs is designed to reward human participation itself.</div> <div className="text-sm font-semibold text-white/85">On every emotional need above, old-school points-based loyalty = 0%. LYTs is designed to reward human participation itself.</div>
</div> </div>
</Card> </div>
</Section> </Section>
); );
} }
/* 08 — THE DIFFERENCE */ /* 08 — THE DIFFERENCE
GSAP: the headline drops in word by word, the two worlds slide in from
opposite edges, then each step of the two flows cascades downward like
value moving through the funnel. */
export function Difference() { export function Difference() {
useGSAP(() => {
syncLenisScrollTrigger();
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return;
gsap.set("#difference .diff-w-in", { yPercent: 120, autoAlpha: 0 });
gsap.set("#difference .diff-left", { xPercent: -9, autoAlpha: 0 });
gsap.set("#difference .diff-right", { xPercent: 9, autoAlpha: 0 });
gsap.set("#difference .diff-step", { y: -18, autoAlpha: 0 });
const tl = gsap.timeline({
scrollTrigger: { trigger: "#difference", start: "top 70%", once: true },
defaults: { ease: "power3.out" },
});
tl.to("#difference .diff-w-in", { yPercent: 0, autoAlpha: 1, stagger: 0.045, duration: 0.75, ease: "power4.out" })
.to("#difference .diff-left", { xPercent: 0, autoAlpha: 1, duration: 0.85 }, "-=0.25")
.to("#difference .diff-right", { xPercent: 0, autoAlpha: 1, duration: 0.85 }, "<")
.to("#difference .diff-left .diff-step", { y: 0, autoAlpha: 1, stagger: 0.08, duration: 0.45 }, "-=0.3")
.to("#difference .diff-right .diff-step", { y: 0, autoAlpha: 1, stagger: 0.08, duration: 0.45 }, "<");
}, []);
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> the fundamental difference</Pill>
<Heading className="mt-6 text-[#FFD21E]"> <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">
loyalty starts at spend. <span className="block"><MaskWords text="loyalty starts at spend." wordClass="diff-w-in" /></span>
<br /> <span className="block"><MaskWords text="LYTs starts at engage." wordClass="diff-w-in" /></span>
LYTs starts at engage. </h2>
</Heading>
<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> <div className="diff-left">
<Pill bg={C.yellow} text="#0a0a0a">💻 OLD WORLD</Pill> <Pill bg={C.yellow} text="#0a0a0a">💻 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="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="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="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="font-extrabold text-[#C8FF37]"> POINTS EXPIRE</div> <div className="diff-step font-extrabold text-[#C8FF37]"> 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}>🔥 not a better coupon. a whole new currency. 🌱</Chip></div>
</div> </div>
<div className="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"> 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="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 }}>{t} {t !== "💞 LYTIAN" && "↓"}</div>
))} ))}
</div> </div>
</div> </div>
@@ -394,7 +650,9 @@ export function Difference() {
); );
} }
/* 09 — THE LOOP */ /* 09 — THE LOOP
GSAP: the headline flips up line by line, then the six steps flip open
left-to-right like a chain of dominoes — the loop building itself. */
export function TheLoop() { export function TheLoop() {
const cards = [ const cards = [
["01", "DISCOVER", "brands, places, people worth meeting.", C.pink, "#fff"], ["01", "DISCOVER", "brands, places, people worth meeting.", C.pink, "#fff"],
@@ -404,23 +662,60 @@ export function TheLoop() {
["05", "LIVE MORE", "explore, connect, feel alive.", C.purple, "#fff"], ["05", "LIVE MORE", "explore, connect, feel alive.", C.purple, "#fff"],
["06", "BECOME A LYTIAN", "the community you always wanted. 💞", "#0a0a0a", C.lime], ["06", "BECOME A LYTIAN", "the community you always wanted. 💞", "#0a0a0a", C.lime],
] as const; ] as const;
useGSAP(() => {
syncLenisScrollTrigger();
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return;
gsap.set("#the-loop .loop-w-in", { yPercent: 118, rotationX: -85, autoAlpha: 0, transformOrigin: "50% 100%" });
gsap.set("#the-loop .loop-card", { rotationY: -92, xPercent: -6, autoAlpha: 0, transformOrigin: "left center" });
gsap.to("#the-loop .loop-w-in", {
scrollTrigger: { trigger: "#the-loop", start: "top 78%", once: true },
yPercent: 0,
rotationX: 0,
autoAlpha: 1,
stagger: 0.05,
duration: 0.8,
ease: "power4.out",
});
gsap.to("#the-loop .loop-card", {
scrollTrigger: { trigger: "#the-loop .loop-grid", start: "top 84%", once: true },
rotationY: 0,
xPercent: 0,
autoAlpha: 1,
stagger: 0.09,
duration: 0.7,
ease: "power3.out",
onComplete: () => gsap.set("#the-loop .loop-card", { clearProps: "transform" }),
});
}, []);
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">🚀 your 6-step loop</Pill>
<Heading className="mt-6"> <h2 className="mt-6 font-sora text-4xl font-extrabold leading-[1.02] tracking-tight [perspective:900px] sm:text-5xl lg:text-6xl">
six small moments. <span className="block"><MaskWords text="six small moments." wordClass="loop-w-in" /></span>
<br /> <span className="block">
one big life that <span className="text-[#FFD21E]">keeps going.</span> <MaskWords text="one big life that" wordClass="loop-w-in" />
</Heading> <MaskWords text="keeps going." wordClass="loop-w-in" inner="text-[#FFD21E]" />
<Group className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-6"> </span>
</h2>
<div className="loop-grid mt-10 grid gap-4 [perspective:1400px] sm:grid-cols-2 lg:grid-cols-6">
{cards.map(([n, t, s, bg, tc]) => ( {cards.map(([n, t, s, bg, tc]) => (
<Card key={n} group bg={bg} text={tc} className="p-4"> <div
key={n}
className="loop-card nb nb-hover rounded-2xl p-4 [will-change:transform]"
style={{ background: bg, color: tc }}
>
<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">{s}</div>
</Card> </div>
))} ))}
</Group> </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"> every interaction creates value. every LYTIAN invites another.</Chip></div>
</Section> </Section>
); );

View File

@@ -1,3 +1,5 @@
"use client";
import { import {
Section, Section,
Pill, Pill,
@@ -6,11 +8,11 @@ import {
Heading, Heading,
Logo, Logo,
Group, Group,
Reveal,
Marquee, Marquee,
ScrollLitText, ScrollLitText,
} from "./ui"; } from "./ui";
import { C } from "./palette"; import { C } from "./palette";
import { gsap, useGSAP, syncLenisScrollTrigger, MaskWords } from "./gsapkit";
/* 10 — FOR YOU */ /* 10 — FOR YOU */
export function ForYou() { export function ForYou() {
@@ -188,45 +190,76 @@ function ActionTile({
}) { }) {
return ( return (
<div <div
className="nb relative w-[248px] shrink-0 rounded-2xl p-5" className="nb relative flex h-[150px] w-[248px] shrink-0 flex-col justify-center rounded-2xl p-5"
style={{ background: bg, color: text }} style={{ background: bg, color: text }}
> >
<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="text-2xl">{emoji}</div>
<div className="mt-4 font-sora text-lg font-extrabold">{title}</div> <div className="mt-2 font-sora text-lg font-extrabold leading-tight">{title}</div>
<div className="text-xs font-bold opacity-80">{sub}</div> <div className="mt-0.5 text-xs font-bold opacity-80">{sub}</div>
</div> </div>
); );
} }
/* 14 — OLD vs NEW */ /* 14 — OLD vs NEW
GSAP: a face-off. The headline reveals word by word, the two worlds charge
in from opposite edges, and the VS badge slams in with an elastic pop and a
quick shake — the clash — before each card's rule lines stagger up. */
export function OldVsNew() { export function OldVsNew() {
useGSAP(() => {
syncLenisScrollTrigger();
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return;
gsap.set("#old-vs-new .ovn-w-in", { yPercent: 120, autoAlpha: 0 });
gsap.set("#old-vs-new .ovn-left", { xPercent: -18, rotationZ: -4, autoAlpha: 0 });
gsap.set("#old-vs-new .ovn-right", { xPercent: 18, rotationZ: 4, autoAlpha: 0 });
gsap.set("#old-vs-new .ovn-vs", { scale: 0, autoAlpha: 0 });
gsap.set("#old-vs-new .ovn-rule", { y: 18, autoAlpha: 0 });
const tl = gsap.timeline({
scrollTrigger: { trigger: "#old-vs-new", start: "top 70%", once: true },
defaults: { ease: "power3.out" },
});
tl.to("#old-vs-new .ovn-w-in", { yPercent: 0, autoAlpha: 1, stagger: 0.05, duration: 0.7, ease: "power4.out" })
.to("#old-vs-new .ovn-left", { xPercent: 0, rotationZ: 0, autoAlpha: 1, duration: 0.7 }, "-=0.2")
.to("#old-vs-new .ovn-right", { xPercent: 0, rotationZ: 0, autoAlpha: 1, duration: 0.7 }, "<")
.to("#old-vs-new .ovn-vs", { scale: 1, autoAlpha: 1, duration: 0.6, ease: "elastic.out(1, 0.5)" }, "-=0.15")
.to("#old-vs-new .ovn-vs", { rotation: "+=8", duration: 0.08, yoyo: true, repeat: 3, ease: "power1.inOut" }, "<0.1")
.to("#old-vs-new .ovn-rule", { y: 0, autoAlpha: 1, stagger: 0.08, duration: 0.5 }, "-=0.3");
}, []);
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">💞 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>
<Heading className="mt-1"> <h2 className="mt-1 font-sora text-4xl font-extrabold leading-[0.95] tracking-tight [perspective:800px] sm:text-5xl lg:text-6xl">
<span className="text-[#FFD21E]">LYTs says</span> another. <MaskWords text="LYTs says" wordClass="ovn-w-in" inner="text-[#FFD21E]" />
</Heading> <MaskWords text="another." wordClass="ovn-w-in" />
</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]">
<Card bg="#0a0a0a" text="#fff" className="p-6"> <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">💻 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]">
the old rule<br /><span className="text-white">spend first.</span><br /><span className="text-white">earn later.</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">earn later.</span>
</div> </div>
<div className="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. 🫥</div>
</Card>
<div className="flex items-center justify-center">
<span className="nb flex h-16 w-16 items-center justify-center rounded-full bg-[#FFD21E] font-sora text-xl font-extrabold text-[#0a0a0a]">VS</span>
</div> </div>
<div className="nb-lg rounded-2xl bg-gradient-to-br from-[#FF2E88] to-[#A020F0] p-6"> <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>
</div>
<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"> 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">
the new rule<br />live first.<br />earn everywhere. <span className="ovn-rule block">the new rule</span>
<span className="ovn-rule block">live first.</span>
<span className="ovn-rule block">earn everywhere.</span>
</div> </div>
<div className="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. 🎉</div>
</div> </div>
</div> </div>
<div className="mt-6"> <div className="mt-6">
@@ -238,7 +271,10 @@ export function OldVsNew() {
); );
} }
/* 15 — THE VISION */ /* 15 — THE VISION
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
right and its ✓ 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"],
@@ -247,23 +283,52 @@ export function TheVision() {
["every community has value", C.orange, "#fff"], ["every community has value", C.orange, "#fff"],
["every person can live more", C.purple, "#fff"], ["every person can live more", C.purple, "#fff"],
] as const; ] as const;
useGSAP(() => {
syncLenisScrollTrigger();
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return;
gsap.set("#the-vision .vis-w-in", { yPercent: 118, autoAlpha: 0 });
gsap.set("#the-vision .vis-social", { scale: 1.6, autoAlpha: 0, transformOrigin: "0% 50%" });
gsap.set("#the-vision .vis-row", { xPercent: 26, autoAlpha: 0 });
gsap.set("#the-vision .vis-check", { scale: 0, rotation: -40, transformOrigin: "50% 50%" });
const tl = gsap.timeline({
scrollTrigger: { trigger: "#the-vision", start: "top 72%", once: true },
defaults: { ease: "power3.out" },
});
tl.to("#the-vision .vis-w-in", { yPercent: 0, autoAlpha: 1, stagger: 0.05, duration: 0.7, ease: "power4.out" })
.to("#the-vision .vis-social", { scale: 1, autoAlpha: 1, duration: 0.6, ease: "back.out(2.4)" }, "-=0.35")
.to("#the-vision .vis-row", { xPercent: 0, autoAlpha: 1, stagger: 0.11, duration: 0.6 }, "-=0.3")
.to("#the-vision .vis-check", { scale: 1, rotation: 0, stagger: 0.11, duration: 0.5, ease: "back.out(3)" }, "<0.15");
}, []);
return ( return (
<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">🌈 the vision</Pill>
<h2 className="mt-6 font-sora text-4xl sm:text-6xl md:text-7xl font-extrabold leading-[0.95] tracking-tight"> <h2 className="mt-6 font-sora text-4xl font-extrabold leading-[0.95] tracking-tight [perspective:800px] sm:text-6xl md:text-7xl">
world&apos;s largest <span className="text-[#A020F0]">social</span> currency ecosystem. <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="vis-w-in inline-block">
<span className="vis-social inline-block text-[#A020F0]">social</span>
</span>
</span>
<MaskWords text="currency ecosystem." wordClass="vis-w-in" />
</h2> </h2>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
{items.map(([t, bg, tc], i) => ( {items.map(([t, bg, tc]) => (
<Reveal key={t} delay={i * 0.09} y={16}> <div
<div className="nb flex items-center gap-3 rounded-xl px-4 py-3 font-extrabold" style={{ background: bg, color: tc }}> key={t}
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[#C8FF37] text-sm text-[#0a0a0a]"></span> className="vis-row nb flex items-center gap-3 rounded-xl px-4 py-3 font-extrabold [will-change:transform]"
{t} style={{ background: bg, color: tc }}
</div> >
</Reveal> <span className="vis-check flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[#C8FF37] text-sm text-[#0a0a0a]"></span>
{t}
</div>
))} ))}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,105 @@
"use client";
/* Shared GSAP wiring for the story sections.
- Registers ScrollTrigger + the useGSAP helper once.
- Syncs Lenis smooth-scroll (set up in Providers) with ScrollTrigger so
scrubbed timelines stay glued to the smoothed scroll position.
- Exposes a pointer-parallax hook for depth-reactive floating objects. */
import { useEffect, type RefObject } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useGSAP } from "@gsap/react";
if (typeof window !== "undefined") {
gsap.registerPlugin(ScrollTrigger, useGSAP);
}
/* Wire Lenis → ScrollTrigger.update exactly once. Lenis is created in a
Providers effect, so it may not exist yet on first call — retry on a few
frames until it appears, then stop. */
let lenisSynced = false;
export function syncLenisScrollTrigger() {
if (lenisSynced || typeof window === "undefined") return;
let tries = 0;
const attach = () => {
const lenis = (window as unknown as { lenis?: { on?: (e: string, cb: () => void) => void } }).lenis;
if (lenis?.on) {
lenis.on("scroll", ScrollTrigger.update);
lenisSynced = true;
ScrollTrigger.refresh();
return;
}
if (tries++ < 60) requestAnimationFrame(attach);
};
attach();
}
/* Depth-reactive pointer parallax: every element matching `selector` inside
`scope` drifts toward the cursor by its `data-depth` factor. Uses quickTo
for a buttery, GPU-friendly follow. Disabled under reduced-motion and on
touch (no hover). */
export function usePointerParallax(
scope: RefObject<HTMLElement | null>,
selector: string,
) {
useEffect(() => {
const root = scope.current;
if (!root) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
if (!window.matchMedia("(hover: hover)").matches) return;
const targets = Array.from(root.querySelectorAll<HTMLElement>(selector));
const movers = targets.map((el) => ({
x: gsap.quickTo(el, "x", { duration: 0.9, ease: "power3.out" }),
y: gsap.quickTo(el, "y", { duration: 0.9, ease: "power3.out" }),
depth: parseFloat(el.dataset.depth || "0.05"),
}));
const onMove = (e: PointerEvent) => {
const r = root.getBoundingClientRect();
const dx = (e.clientX - (r.left + r.width / 2));
const dy = (e.clientY - (r.top + r.height / 2));
movers.forEach((m) => {
m.x(dx * m.depth);
m.y(dy * m.depth);
});
};
const onLeave = () => movers.forEach((m) => { m.x(0); m.y(0); });
root.addEventListener("pointermove", onMove);
root.addEventListener("pointerleave", onLeave);
return () => {
root.removeEventListener("pointermove", onMove);
root.removeEventListener("pointerleave", onLeave);
};
}, [scope, selector]);
}
/* Splits a phrase into per-word masks: each word sits in an overflow-hidden
box so a GSAP tween on `.${wordClass}` can slide/flip it out of the mask.
`inner` lets a segment carry its own colour. */
export function MaskWords({
text,
wordClass,
inner = "",
}: {
text: string;
wordClass: string;
inner?: string;
}) {
return (
<>
{text.split(" ").map((w, i) => (
<span
key={i}
className="mr-[0.24em] inline-block overflow-hidden align-bottom pb-[0.08em]"
>
<span className={`${wordClass} inline-block ${inner}`}>{w}</span>
</span>
))}
</>
);
}
export { gsap, ScrollTrigger, useGSAP };

View File

@@ -450,7 +450,7 @@ export function Pill({
}) { }) {
return ( return (
<motion.span <motion.span
className="nb inline-flex items-center gap-2.5 rounded-full px-6 py-3 text-lg font-extrabold uppercase tracking-wide sm:px-8 sm:py-4 sm:text-2xl" className="story-pill nb inline-flex items-center gap-2.5 rounded-full px-6 py-3 text-lg font-extrabold uppercase tracking-wide sm:px-8 sm:py-4 sm:text-2xl"
style={{ background: bg, color: text }} style={{ background: bg, color: text }}
initial={{ opacity: 0, scale: 0.8, y: 12 }} initial={{ opacity: 0, scale: 0.8, y: 12 }}
whileInView={{ opacity: 1, scale: 1, y: 0 }} whileInView={{ opacity: 1, scale: 1, y: 0 }}