feat: enhance landing page, for-people sections, blog main, and components including TheLoop and PhoneMockup

This commit is contained in:
R-Bharathraj
2026-07-06 13:21:09 +05:30
parent 727a6bcb62
commit 57e64f86b6
18 changed files with 1037 additions and 386 deletions

View File

@@ -29,15 +29,37 @@ export default function Header() {
const pathname = usePathname();
const [mobileOpen, setMobileOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const [hidden, setHidden] = useState(false);
const mobileMenuRef = useRef<HTMLDivElement>(null);
const lastScrollYRef = useRef(0);
// Detect scroll to show/hide shadow on header
// Detect scroll to show/hide shadow on header, and hide the header on
// scroll-down / reveal it again on scroll-up.
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 50);
lastScrollYRef.current = window.scrollY;
const onScroll = () => {
const y = window.scrollY;
setScrolled(y > 50);
const lastY = lastScrollYRef.current;
if (y <= 80) {
setHidden(false);
} else if (y > lastY + 4) {
setHidden(true);
} else if (y < lastY - 4) {
setHidden(false);
}
lastScrollYRef.current = y;
};
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
// Never hide the header while the mobile nav panel is open
useEffect(() => {
if (mobileOpen) setHidden(false);
}, [mobileOpen]);
// Close mobile menu on route change
useEffect(() => {
setMobileOpen(false);
@@ -210,17 +232,19 @@ export default function Header() {
className={transparentHeader ? 'header-transparent' : ''}
style={{
position: 'fixed',
top: 0,
top: isMobileViewport ? '14px' : 0,
left: '20px',
right: '20px',
width: 'auto',
zIndex: 99999999,
background: transparentHeader ? 'transparent' : '#ffffff',
borderTopLeftRadius: isMobileViewport ? '24px' : '0px',
borderTopRightRadius: isMobileViewport ? '24px' : '0px',
borderBottomLeftRadius: '24px',
borderBottomRightRadius: '24px',
boxShadow: transparentHeader ? 'none' : (scrolled ? '0 4px 24px rgba(0,0,0,0.12)' : '0 2px 12px rgba(0,0,0,0.07)'),
transition: 'background 0.3s ease, box-shadow 0.3s ease',
animation: scrolled ? 'slideDown 0.3s ease' : 'none',
boxShadow: transparentHeader ? 'none' : (scrolled ? '0 4px 24px rgba(0,0,0,0.12)' : '0 8px 28px rgba(23,21,18,0.12)'),
transform: hidden ? 'translateY(-130%)' : 'translateY(0)',
transition: 'transform 0.35s cubic-bezier(0.4,0,0.2,1), background 0.3s ease, box-shadow 0.3s ease',
}}
>
<div style={{
@@ -333,10 +357,6 @@ export default function Header() {
</header>
<style>{`
@keyframes slideDown {
from { transform: translateY(-100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@media (max-width: 1023px) {
.desktop-nav { display: none !important; }
.menu-trigger { display: flex !important; }

View File

@@ -101,7 +101,7 @@ export default function PhoneWidget() {
<div style={{ fontSize: 16, color: '#fff', fontWeight: 800, fontFamily: 'Sora, sans-serif', marginTop: 2 }}>Loyaly </div>
</div>
{/* LYT balance card */}
{/* LYTs balance card */}
<div style={{
margin: '0 14px', borderRadius: 22,
background: 'linear-gradient(135deg, #f4be28 0%, #e09820 100%)',
@@ -109,7 +109,7 @@ export default function PhoneWidget() {
position: 'relative', overflow: 'hidden',
}}>
<div style={{ position: 'absolute', top: '-30%', right: '-10%', width: 110, height: 110, borderRadius: '50%', background: 'rgba(255,255,255,0.12)' }} />
<div style={{ fontSize: 10, color: 'rgba(23,21,18,0.65)', fontWeight: 700, fontFamily: 'Sora, sans-serif', letterSpacing: '0.1em', textTransform: 'uppercase' }}>Total LYT</div>
<div style={{ fontSize: 10, color: 'rgba(23,21,18,0.65)', fontWeight: 700, fontFamily: 'Sora, sans-serif', letterSpacing: '0.1em', textTransform: 'uppercase' }}>Total LYTs</div>
<div style={{ fontSize: 34, color: '#171512', fontWeight: 900, fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em', marginTop: 2 }}>284</div>
<div style={{ fontSize: 10, color: 'rgba(23,21,18,0.6)', fontFamily: 'Sora, sans-serif', marginTop: 4 }}> 284 redeemable</div>
</div>
@@ -151,7 +151,7 @@ export default function PhoneWidget() {
}}>
<div style={{ width: 30, height: 30, borderRadius: 9, background: 'rgba(74,222,128,0.16)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#4ade80', fontSize: 14, fontWeight: 800 }}></div>
<div>
<div style={{ fontSize: 11, fontWeight: 700, color: '#fff', fontFamily: 'Sora, sans-serif' }}>+120 LYT</div>
<div style={{ fontSize: 11, fontWeight: 700, color: '#fff', fontFamily: 'Sora, sans-serif' }}>+120 LYTs</div>
<div style={{ fontSize: 9, color: 'rgba(255,255,255,0.4)', fontFamily: 'Sora, sans-serif' }}>just now</div>
</div>
</div>

View File

@@ -44,7 +44,7 @@ const SOLUTIONS = [
title: 'Loyalty Management',
subtitle: 'Build Lasting Relationships',
excerpt:
'Reward customer loyalty with smart programmes that adapt in real time — from LYT and tiers to personalised perks that feel genuinely earned.',
'Reward customer loyalty with smart programmes that adapt in real time — from LYTs and tiers to personalised perks that feel genuinely earned.',
image: '/loyaly_php/assets/images/cognivision-transformative-ai-for-intelligent-fi-640x640.jpg',
imageAlt: 'Loyalty management module showing customer tier performance',
href: '/solutions/loyalty-management',

View File

@@ -9,7 +9,7 @@ if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger);
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
const recs = [
{ rank: '01', label: 'Earn 2× LYT at Starbucks', match: 98, tag: 'Near you · 0.3km' },
{ rank: '01', label: 'Earn 2× LYTs at Starbucks', match: 98, tag: 'Near you · 0.3km' },
{ rank: '02', label: 'Nike Air sale ends tonight', match: 94, tag: 'Trending · Fashion' },
{ rank: '03', label: 'Decathlon weekend special', match: 91, tag: 'Sports · 1.4km' },
{ rank: '04', label: 'Zara new arrivals, 15% off', match: 87, tag: 'Your style' },
@@ -76,7 +76,7 @@ export default function AIRecommendationsSection() {
{/* Left — editorial */}
<div>
<div style={{ marginBottom: 28 }}><span className="fp-kicker fp-rise">AI Intelligence<span className="fp-kicker-num">05</span></span></div>
<div style={{ marginBottom: 28 }}><span className="fp-kicker fp-rise">AI Intelligence<span className="fp-kicker-num">06</span></span></div>
<h2 style={{
fontSize: 'clamp(40px, 5vw, 72px)', fontWeight: 800, letterSpacing: '-0.05em', lineHeight: 0.94,

View File

@@ -2,13 +2,14 @@
import React, { useState } from 'react';
import Image from 'next/image';
import { PhoneFrame, PhoneBottomNav, BellIcon, PinIcon, ChevronIcon, SearchIcon, MicIcon, CameraIcon, MapIcon, HeartLogoIcon } from './PhoneMockup';
const deals = [
{ store: 'Starbucks', category: 'Coffee & Drinks', offer: '2× LYT', savings: '₹120 saved', color: '#00704A', distance: '0.3 km', rating: 4.8 },
{ store: 'Nike', category: 'Footwear', offer: '30% OFF', savings: '₹890 saved', color: '#ffffff', distance: '0.7 km', rating: 4.9 },
{ store: 'H&M', category: 'Fashion', offer: '₹200 Back', savings: '₹200 saved', color: '#E50010', distance: '1.1 km', rating: 4.6 },
{ store: 'Decathlon', category: 'Sports', offer: 'Free Gift', savings: '₹350 saved', color: '#0082C8', distance: '1.4 km', rating: 4.7 },
{ store: 'Zara', category: 'Fashion', offer: '15% OFF', savings: '₹450 saved', color: '#9ca3af', distance: '0.9 km', rating: 4.5 },
{ store: 'Starbucks', category: 'Coffee & Drinks', offer: '2× LYTs', savings: '₹120 saved', color: '#00704A', dot: '#22c55e', distance: '0.3 km', rating: 4.8 },
{ store: 'Nike', category: 'Footwear', offer: '30% OFF', savings: '₹890 saved', color: '#171512', dot: '#d1d5db', distance: '0.7 km', rating: 4.9 },
{ store: 'H&M', category: 'Fashion', offer: '₹200 Back', savings: '₹200 saved', color: '#E50010', dot: '#ef4444', distance: '1.1 km', rating: 4.6 },
{ store: 'Decathlon', category: 'Sports', offer: 'Free Gift', savings: '₹350 saved', color: '#0082C8', dot: '#3b82f6', distance: '1.4 km', rating: 4.7 },
{ store: 'Zara', category: 'Fashion', offer: '15% OFF', savings: '₹450 saved', color: '#171512', dot: '#d1d5db', distance: '0.9 km', rating: 4.5 },
];
export default function DiscoverDealsSection() {
@@ -28,11 +29,11 @@ export default function DiscoverDealsSection() {
}} />
<div className="fp-shell">
<div className="fp-split" style={{ gridTemplateColumns: '0.85fr 1.15fr', alignItems: 'center' }}>
<div className="fp-split" style={{ gridTemplateColumns: '0.9fr 1.1fr', alignItems: 'center' }}>
{/* Headline column */}
<div>
<div style={{ marginBottom: 28 }}><span className="fp-kicker fp-rise">Discover<span className="fp-kicker-num">02</span></span></div>
<div style={{ marginBottom: 28 }}><span className="fp-kicker fp-rise">Discover<span className="fp-kicker-num">03</span></span></div>
<h2 style={{
fontSize: 'clamp(44px, 5.6vw, 88px)', fontWeight: 800, lineHeight: 0.94,
@@ -46,7 +47,7 @@ export default function DiscoverDealsSection() {
<p className="fp-rise" style={{
fontSize: 16, lineHeight: 1.8, color: 'rgba(255,255,255,0.45)', fontFamily: 'Sora, sans-serif', margin: '0 0 36px', maxWidth: 380,
}}>
Find participating stores nearby and see their active game based discount campaigns, live coupons, and LYT earning opportunities, all surfaced in real time so you never miss a deal.
Find participating stores nearby and see their active game based discount campaigns, live coupons, and LYTs earning opportunities, all surfaced in real time so you never miss a deal.
</p>
<div className="fp-rise" style={{
@@ -61,63 +62,97 @@ export default function DiscoverDealsSection() {
</div>
</div>
{/* Deal list */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{deals.map((deal, i) => (
<div
key={i}
onMouseEnter={() => setHovered(i)}
onMouseLeave={() => setHovered(null)}
className="fp-rise"
style={{
position: 'relative', overflow: 'hidden',
display: 'grid', gridTemplateColumns: '46px 1fr auto', alignItems: 'center', gap: 16,
padding: '16px 20px', borderRadius: 16,
background: hovered === i ? 'rgba(244,190,40,0.06)' : 'rgba(255,255,255,0.03)',
border: `1px solid ${hovered === i ? 'rgba(244,190,40,0.28)' : 'rgba(255,255,255,0.07)'}`,
backdropFilter: 'blur(20px)', cursor: 'pointer',
transitionDelay: `${0.1 + i * 0.07}s`,
boxShadow: hovered === i ? '0 14px 40px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.06)' : 'none',
transform: `${hovered === i ? 'translateX(8px)' : 'translateX(0)'}`,
}}
>
{/* Bottom border sweep, left to right on hover */}
<div style={{
position: 'absolute', left: 0, right: 0, bottom: 0, height: 2,
background: 'linear-gradient(90deg, #f4be28, #ff9500)',
transformOrigin: 'left center',
transform: hovered === i ? 'scaleX(1)' : 'scaleX(0)',
transition: 'transform 0.45s cubic-bezier(0.22,1,0.36,1)',
transitionDelay: '0s',
boxShadow: '0 0 10px rgba(244,190,40,0.5)',
pointerEvents: 'none',
}} />
<div style={{
width: 46, height: 46, borderRadius: 13, background: `${deal.color}22`, border: `1px solid ${deal.color}44`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<div style={{ width: 15, height: 15, borderRadius: '50%', background: deal.color }} />
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
<span style={{ fontSize: 16, fontWeight: 700, color: '#fff', fontFamily: 'Sora, sans-serif' }}>{deal.store}</span>
<span style={{ fontSize: 11, color: 'rgba(255,255,255,0.32)', fontFamily: 'Sora, sans-serif' }}>{deal.distance}</span>
<span style={{ fontSize: 11, color: '#f4be28', fontFamily: 'Sora, sans-serif' }}> {deal.rating}</span>
{/* Phone mockup — white app screen, matches the in-app Discover experience */}
<div className="fp-rise" style={{ transitionDelay: '0.1s', display: 'flex', justifyContent: 'center' }}>
<PhoneFrame floatClassName="ddm-phone-shell">
{/* Amber header */}
<div style={{ background: 'linear-gradient(180deg, #fcd34d 0%, #f4be28 100%)', padding: '34px 16px 14px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<Image src="/images/loyalyai.png" alt="Loyaly.AI" width={100} height={26} style={{ objectFit: 'contain', height: 16, width: 'auto' }} />
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 5, background: '#fff', borderRadius: 100, padding: '5px 10px', boxShadow: '0 2px 6px rgba(0,0,0,0.12)' }}>
<HeartLogoIcon />
<span style={{ fontWeight: 800, fontSize: 11.5, color: '#171512', fontFamily: 'Sora, sans-serif' }}>1,250</span>
</div>
<div style={{ width: 26, height: 26, borderRadius: '50%', background: 'rgba(255,255,255,0.55)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<BellIcon />
</div>
</div>
<span style={{ fontSize: 12, color: 'rgba(255,255,255,0.38)', fontFamily: 'Sora, sans-serif' }}>{deal.category}</span>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: 15, fontWeight: 800, color: '#f4be28', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.02em' }}>{deal.offer}</div>
<div style={{ fontSize: 11, color: 'rgba(255,255,255,0.32)', fontFamily: 'Sora, sans-serif', marginTop: 2 }}>{deal.savings}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<PinIcon />
<span style={{ fontSize: 11.5, fontWeight: 600, color: '#171512', fontFamily: 'Sora, sans-serif' }}>R.S. Puram, Coimbatore</span>
<ChevronIcon color="#171512" />
</div>
</div>
))}
<div className="fp-rise" style={{
textAlign: 'right', paddingRight: 4, marginTop: 4, transitionDelay: '0.5s',
fontSize: 12, color: 'rgba(255,255,255,0.25)', fontFamily: 'Sora, sans-serif', letterSpacing: '0.06em',
}}>
+ 2,400 more deals near you
</div>
{/* Search bar */}
<div style={{ padding: '12px 14px 4px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 9, background: '#f5f5f6', borderRadius: 100, padding: '9px 14px' }}>
<SearchIcon />
<span style={{ flex: 1, color: '#9ca3af', fontSize: 12, fontFamily: 'Sora, sans-serif' }}>Search</span>
<MicIcon />
<CameraIcon />
</div>
</div>
{/* Deal rows */}
<div style={{ padding: '8px 12px 2px', display: 'flex', flexDirection: 'column', gap: 6 }}>
{deals.map((deal, i) => (
<div
key={deal.store}
onMouseEnter={() => setHovered(i)}
onMouseLeave={() => setHovered(null)}
style={{
display: 'grid', gridTemplateColumns: '34px minmax(0,1fr) 62px 12px', alignItems: 'center', gap: 8,
padding: '10px 8px', borderRadius: 14,
background: hovered === i ? '#fff7e0' : 'transparent',
transition: 'background 0.25s ease',
cursor: 'default',
}}
>
<div style={{ position: 'relative', width: 34, height: 34, borderRadius: 10, background: deal.color, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ fontSize: 12, fontWeight: 800, color: '#fff', fontFamily: 'Sora, sans-serif' }}>{deal.store[0]}</span>
<span style={{ position: 'absolute', top: -2, left: -2, width: 9, height: 9, borderRadius: '50%', background: deal.dot, border: '2px solid #fff' }} />
</div>
<div style={{ minWidth: 0 }}>
<div style={{
fontSize: 12.5, fontWeight: 700, color: '#171512', fontFamily: 'Sora, sans-serif',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{deal.store}</div>
<div style={{
display: 'flex', alignItems: 'center', gap: 5, marginTop: 2,
fontSize: 9.5, color: '#9ca3af', fontFamily: 'Sora, sans-serif', whiteSpace: 'nowrap',
}}>
<span>{deal.distance}</span>
<span style={{ color: '#f4be28' }}> {deal.rating}</span>
</div>
</div>
<div style={{ textAlign: 'right', minWidth: 0 }}>
<div style={{
fontSize: 11.5, fontWeight: 800, color: '#f4be28', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.01em',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{deal.offer}</div>
<div style={{
fontSize: 8.5, color: '#9ca3af', fontFamily: 'Sora, sans-serif', marginTop: 1,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{deal.savings}</div>
</div>
<ChevronIcon />
</div>
))}
</div>
{/* More deals row */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 18px', margin: '2px 10px 10px', borderRadius: 14, background: '#faf7ee' }}>
<MapIcon />
<span style={{ flex: 1, fontSize: 11.5, fontWeight: 600, color: '#171512', fontFamily: 'Sora, sans-serif' }}>+ 2,400 more deals near you</span>
<ChevronIcon />
</div>
<PhoneBottomNav active="heart" />
</PhoneFrame>
</div>
</div>
</div>

View File

@@ -1,8 +1,10 @@
'use client';
import React, { useEffect, useRef, useState } from 'react';
import Image from 'next/image';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { PhoneFrame, PhoneBottomNav, BellIcon, FlameIcon } from './PhoneMockup';
if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger);
@@ -11,9 +13,6 @@ const feed = [
{ store: 'Walking 5,000 steps', pts: +80, time: '1h ago', type: 'earn' },
{ store: 'Spin Wheel Game', pts: +120, time: '3h ago', type: 'earn' },
{ store: 'Selfie Booth', pts: +200, time: 'Yesterday', type: 'earn' },
{ store: 'Scratch Card', pts: +40, time: 'Yesterday', type: 'earn' },
{ store: 'Match Master', pts: +75, time: '2 days ago', type: 'earn' },
{ store: 'Maze Challenge', pts: +95, time: '3 days ago', type: 'earn' },
];
export default function EarnRewardsSection() {
@@ -35,27 +34,20 @@ export default function EarnRewardsSection() {
rowRefs.current.forEach((row, i) => {
if (!row) return;
const active = i === idx;
row.style.background = active ? '#16a34a' : 'rgba(23,21,18,0.55)';
row.style.borderColor = active ? '#16a34a' : 'rgba(255,255,255,0.14)';
row.style.transform = active ? 'translateX(10px) scale(1.06)' : 'translateX(0) scale(1)';
row.style.boxShadow = active ? '0 16px 40px -12px rgba(22,163,74,0.5)' : 'none';
row.style.zIndex = active ? '2' : '1';
row.style.background = active ? '#f0fdf4' : 'transparent';
row.style.borderColor = active ? '#16a34a' : 'transparent';
row.style.transform = active ? 'translateX(4px) scale(1.02)' : 'translateX(0) scale(1)';
row.style.boxShadow = active ? '0 10px 24px -12px rgba(22,163,74,0.35)' : 'none';
// Flip inner text/icon colors for contrast per background
const title = row.querySelector<HTMLElement>('.feed-title');
if (title) title.style.color = '#ffffff';
const time = row.querySelector<HTMLElement>('.feed-time');
if (time) time.style.color = active ? 'rgba(255,255,255,0.75)' : 'rgba(255,255,255,0.55)';
const icon = row.querySelector<HTMLElement>('.feed-icon');
if (icon) {
icon.style.background = active ? 'rgba(255,255,255,0.15)' : 'rgba(74,222,128,0.1)';
icon.style.borderColor = active ? 'rgba(255,255,255,0.5)' : 'rgba(74,222,128,0.45)';
icon.style.color = active ? '#ffffff' : '#4ade80';
icon.style.background = active ? 'rgba(22,163,74,0.14)' : 'rgba(23,21,18,0.04)';
icon.style.borderColor = active ? '#16a34a' : 'rgba(23,21,18,0.08)';
icon.style.color = active ? '#16a34a' : '#9ca3af';
}
const points = row.querySelector<HTMLElement>('.feed-pts');
if (points) {
points.style.color = active ? '#ffffff' : '#4ade80';
points.style.background = active ? 'rgba(255,255,255,0.15)' : 'rgba(74,222,128,0.12)';
points.style.background = active ? 'rgba(22,163,74,0.14)' : 'rgba(22,163,74,0.08)';
}
});
};
@@ -103,7 +95,7 @@ export default function EarnRewardsSection() {
background: '#f0fdf4', border: '1.5px solid #16a34a', borderRadius: 100, padding: '7px 16px',
}}>
<span style={{ width: 7, height: 7, borderRadius: '50%', background: '#16a34a', flexShrink: 0, animation: 'fpPulse 1.5s infinite' }} />
<span style={{ fontSize: 11, fontWeight: 700, color: '#16a34a', fontFamily: 'Sora, sans-serif', letterSpacing: '0.1em', textTransform: 'uppercase' }}>Live LYT balance</span>
<span style={{ fontSize: 11, fontWeight: 700, color: '#16a34a', fontFamily: 'Sora, sans-serif', letterSpacing: '0.1em', textTransform: 'uppercase' }}>Live LYTs balance</span>
</div>
{/* Giant gold number */}
@@ -125,7 +117,7 @@ export default function EarnRewardsSection() {
<span style={{ fontSize: 13, color: '#4f463a', fontFamily: 'Sora, sans-serif' }}>redeemable value</span>
</div>
<div style={{ marginBottom: 16 }}><span className="fp-kicker fp-rise">Earn<span className="fp-kicker-num">03</span></span></div>
<div style={{ marginBottom: 16 }}><span className="fp-kicker fp-rise">Earn<span className="fp-kicker-num">04</span></span></div>
<h2 style={{
fontSize: 'clamp(28px, 3.4vw, 48px)', fontWeight: 800, lineHeight: 1.08,
@@ -139,83 +131,107 @@ export default function EarnRewardsSection() {
<p className="fp-rise" style={{
fontSize: 15, lineHeight: 1.8, color: '#4f463a', fontFamily: 'Sora, sans-serif', maxWidth: 380, margin: 0,
}}>
Earn LYT (Loyaly Tokens) through daily app login, completing 5,000-step walking challenges, playing Maze, Spin Wheel, Scratch Card, and Match Master games, or visiting Selfie Booths at partner malls. Every activity adds more LYT to your balance.
Earn LYTs (Loyaly Tokens) through daily app login, completing 5,000-step walking challenges, playing Maze, Spin Wheel, Scratch Card, and Match Master games, or visiting Selfie Booths at partner malls. Every activity adds more LYTs to your balance.
</p>
</div>
{/* Right — live feed */}
<div className="fp-rise" style={{ transitionDelay: '0.15s' }}>
{/* Feed header */}
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
marginBottom: 14, paddingBottom: 14, borderBottom: '2px solid #f4be28',
}}>
<span style={{ fontSize: 12, fontWeight: 700, letterSpacing: '0.14em', textTransform: 'uppercase', color: '#171512', fontFamily: 'Sora, sans-serif' }}>Activity feed</span>
<span style={{ fontSize: 11, color: '#16a34a', fontWeight: 700, fontFamily: 'Sora, sans-serif', display: 'flex', alignItems: 'center', gap: 5,
background: '#f0fdf4', border: '1px solid #16a34a', borderRadius: 100, padding: '4px 10px',
}}>
<span style={{ width: 5, height: 5, borderRadius: '50%', background: '#16a34a', display: 'block', animation: 'fpPulse 1.5s infinite' }} />
LIVE
</span>
</div>
{/* Right — phone mockup, matches the in-app Earn / Home experience */}
<div className="fp-rise" style={{ transitionDelay: '0.15s', display: 'flex', justifyContent: 'center' }}>
<PhoneFrame floatClassName="erm-phone-shell">
{/* Feed rows */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{feed.map((tx, i) => (
<div key={i} ref={el => { rowRefs.current[i] = el; }} style={{
position: 'relative', zIndex: i === 0 ? 2 : 1,
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '12px 14px', borderRadius: 12,
background: i === 0 ? '#16a34a' : 'rgba(23,21,18,0.55)',
border: `1.5px solid ${i === 0 ? '#16a34a' : 'rgba(255,255,255,0.14)'}`,
backdropFilter: 'blur(16px)',
WebkitBackdropFilter: 'blur(16px)',
transform: i === 0 ? 'translateX(10px) scale(1.06)' : 'translateX(0) scale(1)',
boxShadow: i === 0 ? '0 16px 40px -12px rgba(22,163,74,0.5)' : 'none',
transition: 'background 0.35s ease, border-color 0.35s ease, transform 0.35s cubic-bezier(0.34,1.56,0.64,1), box-shadow 0.35s ease',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div className="feed-icon" style={{
width: 36, height: 36, borderRadius: 10, flexShrink: 0,
background: i === 0 ? 'rgba(255,255,255,0.15)' : 'rgba(74,222,128,0.1)',
border: `1.5px solid ${i === 0 ? 'rgba(255,255,255,0.5)' : 'rgba(74,222,128,0.45)'}`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 15, fontWeight: 900,
color: i === 0 ? '#ffffff' : '#4ade80',
transition: 'background 0.35s ease, border-color 0.35s ease, color 0.35s ease',
}}>{tx.type === 'earn' ? '↑' : '↓'}</div>
<div>
<div className="feed-title" style={{ fontSize: 13, fontWeight: 700, color: '#ffffff', fontFamily: 'Sora, sans-serif', transition: 'color 0.35s ease' }}>{tx.store}</div>
<div className="feed-time" style={{ fontSize: 11, color: i === 0 ? 'rgba(255,255,255,0.75)' : 'rgba(255,255,255,0.55)', fontFamily: 'Sora, sans-serif', marginTop: 1, transition: 'color 0.35s ease' }}>{tx.time}</div>
</div>
{/* Amber header */}
<div style={{ background: 'linear-gradient(180deg, #fcd34d 0%, #f4be28 100%)', padding: '34px 16px 14px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<Image src="/images/loyalyai.png" alt="Loyaly.AI" width={100} height={26} style={{ objectFit: 'contain', height: 16, width: 'auto' }} />
<div style={{ width: 26, height: 26, borderRadius: '50%', background: 'rgba(255,255,255,0.55)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<BellIcon />
</div>
<span className="feed-pts" style={{
fontSize: 15, fontWeight: 900, fontFamily: 'Sora, sans-serif',
color: i === 0 ? '#ffffff' : '#4ade80', letterSpacing: '-0.02em',
background: i === 0 ? 'rgba(255,255,255,0.15)' : 'rgba(74,222,128,0.12)',
padding: '4px 10px', borderRadius: 8,
transition: 'color 0.35s ease, background 0.35s ease',
}}>
{tx.pts > 0 ? '+' : ''}{tx.pts}
</span>
</div>
))}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<FlameIcon />
<span style={{ fontSize: 11.5, fontWeight: 600, color: '#171512', fontFamily: 'Sora, sans-serif' }}>6-day streak · keep it going!</span>
</div>
</div>
{/* Summary strip */}
<div style={{
marginTop: 16, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10,
}}>
{[
{ label: 'Earned this month', val: '660 LYT', color: '#16a34a', bg: '#f0fdf4', border: '#16a34a' },
{ label: 'Redeemable value', val: '₹660', color: '#f4be28', bg: '#fff7e0', border: '#f4be28' },
].map((s, i) => (
<div key={i} style={{ padding: '14px 16px', borderRadius: 14, background: s.bg, border: `1.5px solid ${s.border}` }}>
<div style={{ fontSize: 11, color: 'rgba(23,21,18,0.5)', fontFamily: 'Sora, sans-serif', marginBottom: 4 }}>{s.label}</div>
<div style={{ fontSize: 18, fontWeight: 900, color: s.color, fontFamily: 'Sora, sans-serif' }}>{s.val}</div>
</div>
))}
</div>
{/* Balance card */}
<div style={{ margin: '14px 14px 0', borderRadius: 18, padding: '16px 18px', background: 'linear-gradient(135deg, #f4be28 0%, #e09820 100%)', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: '-30%', right: '-8%', width: 90, height: 90, borderRadius: '50%', background: 'rgba(255,255,255,0.14)' }} />
<div style={{ fontSize: 10, color: 'rgba(23,21,18,0.65)', fontWeight: 700, fontFamily: 'Sora, sans-serif', letterSpacing: '0.1em', textTransform: 'uppercase' }}>Total LYTs</div>
<div style={{ fontSize: 30, color: '#171512', fontWeight: 900, fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em', marginTop: 2 }}>284</div>
<div style={{ fontSize: 10.5, color: 'rgba(23,21,18,0.6)', fontFamily: 'Sora, sans-serif', marginTop: 3 }}> 284 redeemable</div>
</div>
{/* Feed header */}
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
margin: '16px 14px 6px',
}}>
<span style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: '#171512', fontFamily: 'Sora, sans-serif' }}>Activity feed</span>
<span style={{ fontSize: 9.5, color: '#16a34a', fontWeight: 700, fontFamily: 'Sora, sans-serif', display: 'flex', alignItems: 'center', gap: 4,
background: '#f0fdf4', border: '1px solid #16a34a', borderRadius: 100, padding: '3px 8px',
}}>
<span style={{ width: 4.5, height: 4.5, borderRadius: '50%', background: '#16a34a', display: 'block', animation: 'fpPulse 1.5s infinite' }} />
LIVE
</span>
</div>
{/* Feed rows */}
<div style={{ padding: '2px 10px', display: 'flex', flexDirection: 'column', gap: 3 }}>
{feed.map((tx, i) => (
<div key={i} ref={el => { rowRefs.current[i] = el; }} style={{
display: 'grid', gridTemplateColumns: '32px minmax(0,1fr) auto', alignItems: 'center', gap: 10,
padding: '8px 8px', borderRadius: 12,
background: i === 0 ? '#f0fdf4' : 'transparent',
border: `1px solid ${i === 0 ? '#16a34a' : 'transparent'}`,
transform: i === 0 ? 'translateX(4px) scale(1.02)' : 'translateX(0) scale(1)',
boxShadow: i === 0 ? '0 10px 24px -12px rgba(22,163,74,0.35)' : 'none',
transition: 'background 0.35s ease, border-color 0.35s ease, transform 0.35s cubic-bezier(0.34,1.56,0.64,1), box-shadow 0.35s ease',
}}>
<div className="feed-icon" style={{
width: 32, height: 32, borderRadius: 9, flexShrink: 0,
background: i === 0 ? 'rgba(22,163,74,0.14)' : 'rgba(23,21,18,0.04)',
border: `1.5px solid ${i === 0 ? '#16a34a' : 'rgba(23,21,18,0.08)'}`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: i === 0 ? '#16a34a' : '#9ca3af',
transition: 'background 0.35s ease, border-color 0.35s ease, color 0.35s ease',
}}>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round"><path d="M12 19V5" /><path d="M5 12l7-7 7 7" /></svg>
</div>
<div style={{ minWidth: 0 }}>
<div style={{
fontSize: 11.5, fontWeight: 700, color: '#171512', fontFamily: 'Sora, sans-serif',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{tx.store}</div>
<div style={{ fontSize: 9, color: '#9ca3af', fontFamily: 'Sora, sans-serif', marginTop: 1 }}>{tx.time}</div>
</div>
<span className="feed-pts" style={{
fontSize: 12, fontWeight: 900, fontFamily: 'Sora, sans-serif',
color: '#16a34a', letterSpacing: '-0.01em',
background: i === 0 ? 'rgba(22,163,74,0.14)' : 'rgba(22,163,74,0.08)',
padding: '3px 8px', borderRadius: 8, whiteSpace: 'nowrap',
transition: 'background 0.35s ease',
}}>
+{tx.pts}
</span>
</div>
))}
</div>
{/* Summary strip */}
<div style={{ margin: '10px 14px 12px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
{[
{ label: 'Earned this month', val: '660 LYTs', color: '#16a34a', bg: '#f0fdf4', border: '#16a34a' },
{ label: 'Redeemable value', val: '₹660', color: '#bc7122', bg: '#fff7e0', border: '#f4be28' },
].map((s, i) => (
<div key={i} style={{ padding: '10px 12px', borderRadius: 12, background: s.bg, border: `1.5px solid ${s.border}` }}>
<div style={{ fontSize: 8.5, color: 'rgba(23,21,18,0.5)', fontFamily: 'Sora, sans-serif', marginBottom: 3 }}>{s.label}</div>
<div style={{ fontSize: 14, fontWeight: 900, color: s.color, fontFamily: 'Sora, sans-serif' }}>{s.val}</div>
</div>
))}
</div>
<PhoneBottomNav active="home" />
</PhoneFrame>
</div>
</div>
</div>

View File

@@ -19,7 +19,7 @@ export default function ExperienceSection() {
<div className="fp-shell">
<div style={{ marginBottom: 'clamp(36px, 5vh, 60px)' }}>
<span className="fp-kicker fp-rise">The Experience<span className="fp-kicker-num">06</span></span>
<span className="fp-kicker fp-rise">The Experience<span className="fp-kicker-num">07</span></span>
</div>
<div className="fp-split" style={{ gridTemplateColumns: '1.1fr 0.9fr', alignItems: 'center' }}>

View File

@@ -1,7 +1,6 @@
'use client';
import React, { useRef, useEffect } from 'react';
import Link from 'next/link';
import Image from 'next/image';
function useMagnetic<T extends HTMLElement>(strength = 12) {
@@ -27,7 +26,6 @@ function useMagnetic<T extends HTMLElement>(strength = 12) {
}
export default function HeroSection() {
const magPrimary = useMagnetic<HTMLAnchorElement>(10);
const magSecondary = useMagnetic<HTMLButtonElement>(8);
return (
@@ -71,19 +69,12 @@ export default function HeroSection() {
<p className="fp-content fp-subtext" style={{
fontSize: 'clamp(16px, 1.4vw, 19px)', lineHeight: 1.7, fontWeight: 400,
color: 'rgba(255,255,255,0.58)', maxWidth: 560, margin: '0 auto 40px', fontFamily: 'Sora, sans-serif',
color: 'rgba(255,255,255,0.58)', maxWidth: 640, margin: '0 auto 40px', fontFamily: 'Sora, sans-serif',
}}>
Earn LYT through daily activities, interactive games, and walking challenges, then redeem them at participating stores near you. Earn Online. Spend Offline.
Meet <strong>LYT (Loyaly Token)</strong>: the single currency behind every reward in the app. Earn them through daily tasks, walking challenges, and games, then spend them instantly at partner stores near you with zero expiry.
</p>
<div className="fp-content fp-cta" style={{ display: 'flex', gap: 16, flexWrap: 'wrap', alignItems: 'center', justifyContent: 'center' }}>
<Link
href="/contacts"
ref={magPrimary}
className="fp-btn fp-btn-primary fp-magnetic"
>
Download Free
</Link>
<button
ref={magSecondary}
className="fp-btn fp-btn-ghost fp-magnetic"
@@ -92,8 +83,41 @@ export default function HeroSection() {
</button>
</div>
{/* LYT Quick Facts */}
<div className="fp-content fp-cta" style={{
display: 'flex',
gap: 16,
justifyContent: 'center',
flexWrap: 'wrap',
margin: '36px auto 0',
maxWidth: 600,
}}>
{[
{ val: '1 LYT = ₹1', label: 'Fixed value' },
{ val: 'Zero Expiry', label: 'Ever' },
{ val: '10,000+', label: 'Partner stores' }
].map((item, idx) => (
<div key={idx} style={{
display: 'flex',
alignItems: 'center',
gap: 8,
fontSize: 13,
fontFamily: 'Sora, sans-serif',
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid rgba(244, 190, 40, 0.22)',
padding: '8px 16px',
borderRadius: '100px',
backdropFilter: 'blur(8px)',
}}>
<span style={{ color: '#f4be28', fontWeight: 800 }}>{item.val}</span>
<span style={{ width: 4, height: 4, borderRadius: '50%', background: 'rgba(255,255,255,0.25)' }} />
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: 11.5 }}>{item.label}</span>
</div>
))}
</div>
{/* Trust row */}
<div className="fp-content fp-cta" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 14, marginTop: 40 }}>
<div className="fp-content fp-cta" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 14, marginTop: 32 }}>
<div style={{ display: 'flex' }}>
{['#f4be28', '#4ade80', '#a855f7', '#3b82f6'].map((c, i) => (
<div key={i} style={{ width: 30, height: 30, borderRadius: '50%', background: c, border: '2px solid #0a0a12', marginLeft: i > 0 ? -9 : 0, opacity: 0.9 }} />

View File

@@ -0,0 +1,145 @@
'use client';
import React from 'react';
import Image from 'next/image';
/* ── Shared icon set — used across the for-people phone mockups ── */
export function BellIcon() {
return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#171512" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9" />
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
</svg>
);
}
export function PinIcon() {
return (
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#171512" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
<path d="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-7z" />
<circle cx="12" cy="9" r="2.4" />
</svg>
);
}
export function ChevronIcon({ color = '#9ca3af' }: { color?: string }) {
return (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 6l6 6-6 6" />
</svg>
);
}
export function SearchIcon() {
return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#9ca3af" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="7" /><path d="M21 21l-4.3-4.3" />
</svg>
);
}
export function MicIcon() {
return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#171512" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<rect x="9" y="2" width="6" height="12" rx="3" /><path d="M5 10a7 7 0 0 0 14 0" /><path d="M12 19v3" />
</svg>
);
}
export function CameraIcon() {
return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#171512" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
<circle cx="12" cy="13" r="4" />
</svg>
);
}
export function MapIcon() {
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f4be28" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<path d="M1 6v16l7-4 8 4 7-4V2l-7 4-8-4z" /><path d="M8 2v16" /><path d="M16 6v16" />
</svg>
);
}
export function HeartLogoIcon({ size = 16 }: { size?: number }) {
return (
<Image src="/images/loyalyai-logo.png" alt="" width={32} height={32} style={{ width: size, height: size, objectFit: 'contain' }} />
);
}
export function UpArrowIcon({ color = '#16a34a' }: { color?: string }) {
return (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 19V5" /><path d="M5 12l7-7 7 7" />
</svg>
);
}
export function DownArrowIcon({ color = '#ef4444' }: { color?: string }) {
return (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 5v14" /><path d="M5 12l7 7 7-7" />
</svg>
);
}
export function FlameIcon({ color = '#f4be28' }: { color?: string }) {
return (
<svg width="14" height="14" viewBox="0 0 24 24" fill={color} stroke="none">
<path d="M12 2c1 3-2 4-2 7a3 3 0 0 0 6 0c1.5 1.5 2 3.5 2 5a6 6 0 0 1-12 0c0-4 3-6 3-9 0-1 1-2 3-3z" />
</svg>
);
}
/* ── Bottom nav — pass the active tab key to highlight it ── */
export function PhoneBottomNav({ active = 'home' }: { active?: string }) {
const items: { key: string; icon: React.ReactNode }[] = [
{ key: 'home', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 10.5L12 3l9 7.5" /><path d="M5 9.5V21h14V9.5" /></svg> },
{ key: 'tag', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M20.59 13.41 12 22l-9-9V4h9z" /><circle cx="7.5" cy="8.5" r="1.2" /></svg> },
{ key: 'heart', icon: <HeartLogoIcon size={18} /> },
{ key: 'shop', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9l1.5-5h15L21 9" /><path d="M3 9h18v11H3z" /><path d="M9 20v-6h6v6" /></svg> },
{ key: 'user', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="8" r="4" /><path d="M4 21c1.5-4 5-6 8-6s6.5 2 8 6" /></svg> },
];
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-around', padding: '10px 8px', borderTop: '1px solid #f0f0f0' }}>
{items.map((n) => {
const isActive = n.key === active;
return (
<div key={n.key} style={{
width: 34, height: 34, borderRadius: 12, display: 'flex', alignItems: 'center', justifyContent: 'center',
color: isActive && n.key !== 'heart' ? '#171512' : '#9ca3af',
background: isActive ? '#fff7e0' : 'transparent',
border: isActive ? '1.5px solid #f4be28' : 'none',
}}>
{n.icon}
</div>
);
})}
</div>
);
}
/* ── Phone frame — dark bezel + dynamic island + gentle float, white screen inside ── */
export function PhoneFrame({ children, floatClassName = 'pm-phone-float', width = 300 }: { children: React.ReactNode; floatClassName?: string; width?: number }) {
return (
<div
className={floatClassName}
style={{
width, borderRadius: 42, padding: 10,
background: 'linear-gradient(160deg, #2a2a2e 0%, #0a0a0c 100%)',
boxShadow: '0 60px 120px -24px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.06)',
position: 'relative',
}}
>
<div style={{ borderRadius: 32, overflow: 'hidden', background: '#ffffff', position: 'relative' }}>
{/* Dynamic island */}
<div style={{
position: 'absolute', top: 8, left: '50%', transform: 'translateX(-50%)',
width: 82, height: 22, borderRadius: 14, background: '#000', zIndex: 5,
}} />
{children}
</div>
<style>{`
@keyframes pmPhoneFloat { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
.pm-phone-float { animation: pmPhoneFloat 7s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) { .pm-phone-float { animation: none !important; } }
@media (max-width: 900px) {
.pm-phone-float { width: 280px !important; }
}
`}</style>
</div>
);
}

View File

@@ -80,7 +80,7 @@ export default function SpendAnywhereSection() {
{/* Header row */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', flexWrap: 'wrap', gap: 24, marginBottom: 'clamp(40px, 6vh, 64px)' }}>
<div>
<div style={{ marginBottom: 22 }}><span className="fp-kicker fp-rise">Spend<span className="fp-kicker-num">04</span></span></div>
<div style={{ marginBottom: 22 }}><span className="fp-kicker fp-rise">Spend<span className="fp-kicker-num">05</span></span></div>
<h2 style={{
fontSize: 'clamp(46px, 7vw, 110px)', fontWeight: 800, letterSpacing: '-0.06em', lineHeight: 0.9,
color: '#fff', margin: 0, fontFamily: 'Sora, sans-serif',
@@ -92,7 +92,7 @@ export default function SpendAnywhereSection() {
<p className="fp-rise" style={{
fontSize: 15, lineHeight: 1.75, color: 'rgba(255,255,255,0.45)', fontFamily: 'Sora, sans-serif', margin: 0, maxWidth: 320,
}}>
One balance. Every store. Your LYT is universal: no restrictions, no expiry, no fine print.
One balance. Every store. Your LYTs are universal: no restrictions, no expiry, no fine print.
</p>
</div>
@@ -184,7 +184,7 @@ export default function SpendAnywhereSection() {
}}>
{[
{ val: '10,000+', lbl: 'Partner stores' },
{ val: 'Zero', lbl: 'Expiry on LYT' },
{ val: 'Zero', lbl: 'Expiry on LYTs' },
{ val: 'Universal', lbl: 'One balance, all brands' },
{ val: 'Instant', lbl: 'Redeem at checkout' },
].map((s, i) => (

View File

@@ -178,7 +178,7 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
minHeight: '180px',
minHeight: '230px',
boxShadow: '0 10px 30px -12px rgba(225, 45, 45, 0.4)',
position: 'relative',
overflow: 'hidden',
@@ -196,8 +196,8 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
position: 'absolute',
right: '-18px',
bottom: '-18px',
width: '210px',
height: '210px',
width: '250px',
height: '250px',
pointerEvents: 'none',
zIndex: 1,
}}>
@@ -210,7 +210,7 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
objectFit: 'contain',
width: '100%',
height: '100%',
transform: 'scale(2.0)',
transform: 'scale(1.85)',
transformOrigin: 'center center',
}}
/>
@@ -233,7 +233,7 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
minHeight: '180px',
minHeight: '230px',
boxShadow: '0 10px 30px -12px rgba(225, 45, 45, 0.4)',
position: 'relative',
overflow: 'hidden',
@@ -251,8 +251,8 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
position: 'absolute',
right: '-18px',
bottom: '-18px',
width: '210px',
height: '210px',
width: '250px',
height: '250px',
pointerEvents: 'none',
zIndex: 1,
}}>
@@ -265,7 +265,7 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
objectFit: 'contain',
width: '100%',
height: '100%',
transform: 'scale(2.0)',
transform: 'scale(1.85)',
transformOrigin: 'center center',
}}
/>
@@ -288,7 +288,7 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
minHeight: '180px',
minHeight: '230px',
boxShadow: '0 10px 30px -12px rgba(225, 45, 45, 0.4)',
position: 'relative',
overflow: 'hidden',
@@ -306,8 +306,8 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
position: 'absolute',
right: '-18px',
bottom: '-18px',
width: '210px',
height: '210px',
width: '250px',
height: '250px',
pointerEvents: 'none',
zIndex: 1,
}}>
@@ -320,7 +320,7 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
objectFit: 'contain',
width: '100%',
height: '100%',
transform: 'scale(2.0)',
transform: 'scale(1.85)',
transformOrigin: 'center center',
}}
/>
@@ -347,12 +347,12 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
minHeight: '190px',
minHeight: '250px',
position: 'relative',
overflow: 'hidden',
}}
>
<div style={{ width: card.wide ? '46%' : '54%', paddingRight: '12px', position: 'relative', zIndex: 2 }}>
<div style={{ width: card.wide ? '42%' : '52%', paddingRight: '12px', position: 'relative', zIndex: 2 }}>
{card.icon}
<h3 style={{ fontSize: '1.1rem', fontWeight: 800, color: '#ffffff', fontFamily: 'Sora, sans-serif', margin: '0 0 6px' }}>
{card.title}
@@ -366,16 +366,16 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
position: 'absolute',
right: '-6px',
bottom: '-6px',
width: '300px',
height: '180px',
width: '380px',
height: '230px',
pointerEvents: 'none',
zIndex: 1,
}}>
<Image
src={card.image}
alt={card.title}
width={400}
height={400}
width={480}
height={480}
style={{
objectFit: 'cover',
objectPosition: '52% 34%',
@@ -387,23 +387,23 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
) : (
<div style={{
position: 'absolute',
right: '-24px',
bottom: '-24px',
width: '230px',
height: '230px',
right: '-22px',
bottom: '-22px',
width: '280px',
height: '280px',
pointerEvents: 'none',
zIndex: 1,
}}>
<Image
src={card.image}
alt={card.title}
width={400}
height={400}
width={440}
height={440}
style={{
objectFit: 'contain',
width: '100%',
height: '100%',
transform: 'scale(2.1)',
transform: 'scale(2.0)',
transformOrigin: 'center center',
}}
/>

View File

@@ -63,7 +63,7 @@ const PRODUCTS: Product[] = [
title: 'Identiq',
subtitle: 'Consumer Loyalty Platform',
description:
'Earn LYT through daily logins and games, then redeem rewards at real offline stores.',
'Earn LYTs through daily logins and games, then redeem rewards at real offline stores.',
accent: '#f4be28',
accentRgb: '244, 190, 40',
tags: ['Daily Rewards', 'Game Based Offers', 'Offline Redemption'],
@@ -283,12 +283,12 @@ function ProductVisual({ product }: { product: Product }) {
</span>
</div>
{/* LYT balance card with progress to next tier */}
{/* LYTs balance card with progress to next tier */}
<div style={{ borderRadius: 18, padding: '18px 20px', background: `linear-gradient(135deg, ${accent} 0%, #e09820 100%)`, position: 'relative', overflow: 'hidden' }}>
<div className="pv-sheen" />
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(0,0,0,0.6)', fontFamily: 'Sora, sans-serif' }}>Reward LYT</div>
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(0,0,0,0.6)', fontFamily: 'Sora, sans-serif' }}>Reward LYTs</div>
<div style={{ fontSize: 32, fontWeight: 900, color: '#111', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em', marginTop: 2 }}>284</div>
</div>
<span style={{ width: 34, height: 34, borderRadius: 10, background: 'rgba(255,255,255,0.35)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 15, fontWeight: 900, color: '#111', fontFamily: 'Sora, sans-serif' }}></span>
@@ -297,7 +297,7 @@ function ProductVisual({ product }: { product: Product }) {
<div style={{ height: 5, borderRadius: 3, background: 'rgba(0,0,0,0.15)', overflow: 'hidden' }}>
<div className="pv-progress-fill" style={{ height: '100%', width: '68%', borderRadius: 3, background: '#111' }} />
</div>
<div style={{ fontSize: 10, color: 'rgba(0,0,0,0.55)', fontFamily: 'Sora, sans-serif', marginTop: 6, fontWeight: 600 }}>116 LYT to Platinum</div>
<div style={{ fontSize: 10, color: 'rgba(0,0,0,0.55)', fontFamily: 'Sora, sans-serif', marginTop: 6, fontWeight: 600 }}>116 LYTs to Platinum</div>
</div>
</div>
@@ -667,10 +667,16 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro
}
.products-track::-webkit-scrollbar { display: none; }
.products-track { scrollbar-width: none; }
.products-card { height: 560px; width: min(86vw, 460px); }
.products-card { height: auto; width: min(86vw, 460px); }
.products-card { flex-direction: column !important; }
.products-card-content { flex: 1 1 auto !important; }
.products-card-visual { flex: 0 0 200px !important; border-left: none !important; border-top: 1px solid rgba(255,255,255,0.08); }
.products-card-content { flex: 0 0 auto !important; }
.products-card-visual {
flex: 0 0 auto !important;
min-height: 300px;
padding: 32px 0;
border-left: none !important;
border-top: 1px solid rgba(255,255,255,0.08);
}
.products-progress-wrap { position: relative; bottom: auto; transform: none; left: auto; margin: 20px auto 0; }
}
`}</style>

View File

@@ -37,7 +37,7 @@ const GREEN_CARDS = [
const BLACK_CARDS = [
{
title: 'Earn Daily',
description: 'Customers earn LYT through daily app activities',
description: 'Customers earn LYTs through daily app activities',
icon: (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
@@ -226,23 +226,23 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
{/* Papercraft Image */}
<div style={{
position: 'absolute',
right: i === 2 ? '-18px' : '-22px',
bottom: i === 2 ? '-18px' : '-22px',
width: i === 2 ? '62%' : '58%',
height: '100%',
right: i === 2 ? '-20px' : '-24px',
bottom: i === 2 ? '-20px' : '-24px',
width: i === 2 ? '72%' : '68%',
height: '112%',
pointerEvents: 'none',
zIndex: 1,
}}>
<Image
src={card.image}
alt={card.label}
width={340}
height={340}
width={400}
height={400}
style={{
objectFit: 'contain',
width: '100%',
height: '100%',
transform: i === 2 ? 'scale(1.32)' : 'scale(1.28)',
transform: i === 2 ? 'scale(1.4)' : 'scale(1.36)',
transformOrigin: 'bottom right',
}}
/>
@@ -263,7 +263,7 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
clickEffect={true}
particleCount={6}
>
<div style={{ position: 'relative', zIndex: 2, width: cardNumber === 7 ? '40%' : (cardNumber === 8 ? '46%' : '52%'), display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ position: 'relative', zIndex: 2, width: cardNumber === 7 ? '36%' : (cardNumber === 8 ? '42%' : '48%'), display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ color: iconAccent, marginBottom: 14, display: 'flex', alignItems: 'center' }}>
{card.icon}
</div>
@@ -295,7 +295,7 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
position: 'absolute',
right: 0,
bottom: 0,
width: cardNumber === 7 ? '58%' : '54%',
width: cardNumber === 7 ? '66%' : '62%',
height: '100%',
pointerEvents: 'none',
zIndex: 1,
@@ -303,8 +303,8 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
<Image
src={card.image}
alt={card.title}
width={cardNumber === 7 ? 480 : 400}
height={cardNumber === 7 ? 480 : 400}
width={cardNumber === 7 ? 520 : 440}
height={cardNumber === 7 ? 520 : 440}
style={{
objectFit: 'cover',
objectPosition: cardNumber === 7 ? '62% 88%' : '68% 82%',
@@ -316,23 +316,23 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
) : (
<div style={{
position: 'absolute',
right: '-14px',
bottom: '-16px',
width: '56%',
height: '96%',
right: '-16px',
bottom: '-20px',
width: '66%',
height: '112%',
pointerEvents: 'none',
zIndex: 1,
}}>
<Image
src={card.image}
alt={card.title}
width={340}
height={340}
width={400}
height={400}
style={{
objectFit: 'contain',
width: '100%',
height: '100%',
transform: 'scale(1.22)',
transform: 'scale(1.3)',
transformOrigin: 'bottom right',
}}
/>
@@ -366,7 +366,7 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
margin: 0 auto;
}
.shift-bento-card {
min-height: 220px;
min-height: 270px;
border-radius: 24px;
padding: clamp(20px, 2.4vw, 28px);
display: flex;
@@ -401,7 +401,7 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
.shift-card-4 { grid-column: span 1; }
.shift-card-5 { grid-column: span 1; }
.shift-card-6 { grid-column: span 1; }
.shift-card-7 { grid-column: span 2; min-height: 240px; }
.shift-card-7 { grid-column: span 2; min-height: 300px; }
.shift-card-8 { grid-column: span 1; }
@media (max-width: 1023px) {

View File

@@ -1,6 +1,6 @@
'use client';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import React, { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import AnimatedDarkBg from './AnimatedDarkBg';
@@ -12,6 +12,17 @@ if (typeof window !== 'undefined') {
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
const MOBILE_QUERY = '(max-width: 768px)';
const subscribeMobileQuery = (callback: () => void) => {
const mql = window.matchMedia(MOBILE_QUERY);
mql.addEventListener('change', callback);
return () => mql.removeEventListener('change', callback);
};
const getMobileSnapshot = () => window.matchMedia(MOBILE_QUERY).matches;
// Server has no viewport, so it always renders the desktop layout; the client's
// first render must match that to avoid a hydration mismatch.
const getMobileServerSnapshot = () => false;
interface Step {
step: string;
id: 'earn' | 'engage' | 'spend' | 'repeat';
@@ -20,10 +31,10 @@ interface Step {
}
const STEPS: Step[] = [
{ step: '01', id: 'earn', title: 'Earn Online', desc: 'Daily login, 5,000-step walking challenges, and interactive games like Maze and Spin Wheel earn LYT and unlock surprise coupons.' },
{ step: '01', id: 'earn', title: 'Earn Online', desc: 'Daily login, 5,000-step walking challenges, and interactive games like Maze and Spin Wheel earn LYTs and unlock surprise coupons.' },
{ step: '02', id: 'engage', title: 'Unlock Deals', desc: 'Merchants create game based discount campaigns: customers play once to reveal their exclusive reward on a specific product.' },
{ step: '03', id: 'spend', title: 'Discover & Visit', desc: 'Browse participating stores nearby, view active game campaigns and LYT-earning offers, then walk in and shop in person.' },
{ step: '04', id: 'repeat', title: 'Redeem & Repeat', desc: 'Use LYT and coupons at checkout to save money, then come back tomorrow to earn more and keep the loop going.' },
{ step: '03', id: 'spend', title: 'Discover & Visit', desc: 'Browse participating stores nearby, view active game campaigns and LYTs-earning offers, then walk in and shop in person.' },
{ step: '04', id: 'repeat', title: 'Redeem & Repeat', desc: 'Use LYTs and coupons at checkout to save money, then come back tomorrow to earn more and keep the loop going.' },
];
const IMPACTS = [
@@ -34,15 +45,16 @@ const IMPACTS = [
];
const IMAGE_PATHS = [
'/retail_cycle/Earn_online.png',
'/retail_cycle/unlock_deals.png',
'/retail_cycle/discover+visits.png',
'/retail_cycle/redeem+repeat.png',
'/retail_cycle/01_Earn_online.png',
'/retail_cycle/02_unlock_deals.png',
'/retail_cycle/03_discover+visits.png',
'/retail_cycle/04_redeem+repeat.png',
];
export default function TheLoop() {
// Only counts uses state — everything scroll-driven is ref-mutated
const [counts, setCounts] = useState<number[]>(IMPACTS.map(() => 0));
const isMobile = useSyncExternalStore(subscribeMobileQuery, getMobileSnapshot, getMobileServerSnapshot);
const sectionRef = useRef<HTMLDivElement>(null);
const pinRef = useRef<HTMLDivElement>(null);
@@ -51,7 +63,7 @@ export default function TheLoop() {
const tipRef = useRef<HTMLDivElement>(null);
const ghostRef = useRef<HTMLDivElement>(null);
const scrollHintRef = useRef<HTMLDivElement>(null);
// Per-step refs
// Per-step refs (desktop)
const labelRefs = useRef<(HTMLDivElement | null)[]>([]);
const titleRefs = useRef<(HTMLHeadingElement | null)[]>([]);
const descRefs = useRef<(HTMLDivElement | null)[]>([]);
@@ -60,10 +72,19 @@ export default function TheLoop() {
const trackDotGlowRefs = useRef<(HTMLDivElement | null)[]>([]);
const visualRefs = useRef<(HTMLDivElement | null)[]>([]);
// Mobile-only refs — same scroll-scrub mechanic, vertical single-column layout
const mobileSectionRef = useRef<HTMLDivElement>(null);
const mobilePinRef = useRef<HTMLDivElement>(null);
const mobileLineRef = useRef<HTMLDivElement>(null);
const mobileVisualRefs = useRef<(HTMLDivElement | null)[]>([]);
const mobileTextRefs = useRef<(HTMLDivElement | null)[]>([]);
const mobileDotRefs = useRef<(HTMLDivElement | null)[]>([]);
const mobileDotTextRefs = useRef<(HTMLSpanElement | null)[]>([]);
useIsomorphicLayoutEffect(() => {
if (typeof window === 'undefined') return;
// Set initial active state directly in DOM
// Set initial active state directly in DOM (desktop)
const applyActive = (idx: number, lineFillPct: number) => {
// Growing line
if (lineRef.current)
@@ -131,26 +152,84 @@ export default function TheLoop() {
});
};
// Seed with step 0
// Same progression logic, driving the vertical mobile layout
const applyActiveMobile = (idx: number, lineFillPct: number) => {
if (mobileLineRef.current)
mobileLineRef.current.style.width = `calc((100% - 20px) * ${lineFillPct / 100})`;
STEPS.forEach((_, i) => {
const isActive = i === idx;
const isPassed = i < idx;
if (mobileTextRefs.current[i])
mobileTextRefs.current[i]!.style.opacity = isActive ? '1' : '0';
if (mobileVisualRefs.current[i])
mobileVisualRefs.current[i]!.style.opacity = isActive ? '1' : '0';
const dot = mobileDotRefs.current[i];
const txt = mobileDotTextRefs.current[i];
if (dot) {
if (isActive) {
dot.style.background = '#f4be28';
dot.style.borderColor = '#f4be28';
dot.style.transform = 'scale(1.15)';
dot.style.boxShadow = '0 0 14px rgba(244,190,40,0.7)';
} else if (isPassed) {
dot.style.background = 'rgba(244,190,40,0.25)';
dot.style.borderColor = 'rgba(244,190,40,0.5)';
dot.style.transform = 'scale(1)';
dot.style.boxShadow = 'none';
} else {
dot.style.background = 'transparent';
dot.style.borderColor = 'rgba(244,190,40,0.2)';
dot.style.transform = 'scale(1)';
dot.style.boxShadow = 'none';
}
}
if (txt) {
txt.style.color = isActive ? '#1a1200' : isPassed ? 'rgba(244,190,40,0.7)' : 'rgba(244,190,40,0.22)';
}
});
};
// Seed initial state for whichever layout is active
applyActive(0, 0);
applyActiveMobile(0, 0);
const ctx = gsap.context(() => {
const totalSteps = STEPS.length;
ScrollTrigger.create({
trigger: sectionRef.current,
start: 'top top',
end: () => `+=${window.innerHeight * (totalSteps - 1) * 1.8}`,
pin: pinRef.current,
scrub: true, // No smoothing lag — direct 1:1 with scroll position
onUpdate: (self) => {
const p = self.progress;
const idx = Math.min(Math.floor(p * totalSteps), totalSteps - 1);
// Fill goes from 0 to 100% as progress goes 0→(n-1)/n
const lineFillPct = Math.min(p * totalSteps, totalSteps - 1) / (totalSteps - 1) * 100;
applyActive(idx, lineFillPct);
},
});
if (isMobile) {
ScrollTrigger.create({
trigger: mobileSectionRef.current,
start: 'top top',
end: () => `+=${window.innerHeight * (totalSteps - 1) * 1.5}`,
pin: mobilePinRef.current,
scrub: true,
onUpdate: (self) => {
const p = self.progress;
const idx = Math.min(Math.floor(p * totalSteps), totalSteps - 1);
const lineFillPct = Math.min(p * totalSteps, totalSteps - 1) / (totalSteps - 1) * 100;
applyActiveMobile(idx, lineFillPct);
},
});
} else {
ScrollTrigger.create({
trigger: sectionRef.current,
start: 'top top',
end: () => `+=${window.innerHeight * (totalSteps - 1) * 1.8}`,
pin: pinRef.current,
scrub: true, // No smoothing lag — direct 1:1 with scroll position
onUpdate: (self) => {
const p = self.progress;
const idx = Math.min(Math.floor(p * totalSteps), totalSteps - 1);
const lineFillPct = Math.min(p * totalSteps, totalSteps - 1) / (totalSteps - 1) * 100;
applyActive(idx, lineFillPct);
},
});
}
// Count-up metrics
ScrollTrigger.create({
@@ -179,10 +258,13 @@ export default function TheLoop() {
}, sectionRef);
return () => ctx.revert();
}, []);
}, [isMobile]);
return (
<>
{/* ══════════════════════════════════════════
DESKTOP / LAPTOP — pinned two-column layout
══════════════════════════════════════════ */}
<section
ref={sectionRef}
id="the-loop"
@@ -194,6 +276,7 @@ export default function TheLoop() {
overflow: 'hidden',
zIndex: 2,
height: `${(STEPS.length - 1) * 180 + 100}vh`,
display: isMobile ? 'none' : 'block',
}}
>
<AnimatedDarkBg variant="amber" />
@@ -233,7 +316,7 @@ export default function TheLoop() {
color: 'rgba(255,255,255,0.38)', lineHeight: 1.75,
fontFamily: 'Sora, sans-serif',
}}>
Earn LYT online, unlock game based deals, discover nearby stores, and redeem offline.
Earn LYTs online, unlock game based deals, discover nearby stores, and redeem offline.
</p>
</div>
@@ -372,23 +455,19 @@ export default function TheLoop() {
</div>
</div>
{/* RIGHT: visual panel */}
{/* RIGHT: visual panel — borderless, image floats directly on the section bg */}
<div style={{
flex: '0 0 clamp(300px,44%,500px)', minHeight: 360, position: 'relative', borderRadius: 28,
background: 'rgba(244,190,40,0.04)',
border: '1px solid rgba(244,190,40,0.12)',
backdropFilter: 'blur(24px)', WebkitBackdropFilter: 'blur(24px)',
overflow: 'hidden',
flex: '0 0 clamp(340px,50%,560px)', minHeight: 420, position: 'relative',
}}>
<div style={{
position: 'absolute', inset: 0,
position: 'absolute', inset: '-10%',
background: 'radial-gradient(ellipse 60% 50% at 50% 50%, rgba(244,190,40,0.08) 0%, transparent 70%)',
pointerEvents: 'none',
}} />
{/* Ghost step number — text content set via ref */}
<div ref={ghostRef} style={{
position: 'absolute', bottom: -10, right: 24,
position: 'absolute', bottom: -10, right: 0,
fontSize: 'clamp(7rem,14vw,12rem)', fontWeight: 900, lineHeight: 1,
color: 'transparent',
WebkitTextStroke: '1px rgba(244,190,40,0.07)',
@@ -410,7 +489,6 @@ export default function TheLoop() {
justifyContent: 'center',
opacity: i === 0 ? 1 : 0,
transition: 'opacity 0.5s ease',
padding: '24px',
}}
>
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
@@ -419,7 +497,7 @@ export default function TheLoop() {
alt={s.title}
fill
style={{ objectFit: 'contain' }}
sizes="(max-width: 768px) 100vw, 44vw"
sizes="(max-width: 768px) 100vw, 50vw"
priority={i === 0}
/>
</div>
@@ -446,6 +524,189 @@ export default function TheLoop() {
<div style={{ width: 1.5, height: 28, background: 'linear-gradient(180deg,rgba(244,190,40,0.6),transparent)', animation: 'loopScrollHint 1.4s ease-in-out infinite' }} />
</div>
</div>
</section>
{/* ══════════════════════════════════════════
MOBILE — pinned single-column layout: big
image on top, one legible step at a time,
horizontal progress tracker below
══════════════════════════════════════════ */}
<section
ref={mobileSectionRef}
id="the-loop-mobile"
style={{
position: 'relative',
width: 'calc(100% - 40px)',
margin: '0 20px 0',
borderRadius: '32px 32px 0 0',
overflow: 'hidden',
zIndex: 2,
height: `${(STEPS.length - 1) * 130 + 100}vh`,
display: isMobile ? 'block' : 'none',
}}
>
<AnimatedDarkBg variant="amber" />
<div
ref={mobilePinRef}
style={{
position: 'relative',
height: '100vh',
display: 'flex',
flexDirection: 'column',
padding: '96px 24px 28px',
boxSizing: 'border-box',
zIndex: 10,
}}
>
{/* Header */}
<div style={{ textAlign: 'center', marginBottom: 12, flexShrink: 0 }}>
<h2 style={{
margin: '0 0 8px',
fontSize: 'clamp(1.5rem,6.8vw,2rem)',
fontWeight: 800, color: '#fff',
lineHeight: 1.05, letterSpacing: '-0.04em',
fontFamily: 'Sora, sans-serif',
}}>
A smarter{' '}
<span style={{
background: 'linear-gradient(105deg,#f4be28 0%,#fff8e0 48%,#f4be28 62%,#ff9500 100%)',
backgroundSize: '200% auto', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
animation: 'loopShimmer 3s linear infinite',
}}>retail cycle.</span>
</h2>
<p style={{
margin: '0 auto', maxWidth: 300,
fontSize: '0.78rem',
color: 'rgba(255,255,255,0.4)', lineHeight: 1.5,
fontFamily: 'Sora, sans-serif',
}}>
Earn LYTs online, unlock game based deals, discover nearby stores, and redeem offline.
</p>
</div>
{/* Visual panel — borderless, image floats directly on the section bg */}
<div style={{
flex: '0 0 auto',
height: 'clamp(230px,32vh,300px)',
position: 'relative',
marginBottom: 14,
}}>
<div style={{
position: 'absolute', inset: '-10%',
background: 'radial-gradient(ellipse 60% 50% at 50% 50%, rgba(244,190,40,0.1) 0%, transparent 70%)',
pointerEvents: 'none',
}} />
{STEPS.map((s, i) => (
<div
key={s.id}
ref={el => { mobileVisualRefs.current[i] = el; }}
style={{
position: 'absolute', inset: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
opacity: i === 0 ? 1 : 0,
transition: 'opacity 0.5s ease',
}}
>
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
<Image
src={IMAGE_PATHS[i]}
alt={s.title}
fill
style={{ objectFit: 'contain' }}
sizes="100vw"
priority={i === 0}
/>
</div>
</div>
))}
</div>
{/* Step text — one clear step at a time, cross-faded */}
<div style={{ flex: 1, position: 'relative', minHeight: 150 }}>
{STEPS.map((s, i) => (
<div
key={s.id}
ref={el => { mobileTextRefs.current[i] = el; }}
style={{
position: 'absolute', inset: 0,
opacity: i === 0 ? 1 : 0,
transition: 'opacity 0.4s ease',
textAlign: 'center',
}}
>
<div style={{
fontSize: 10, fontWeight: 700, letterSpacing: '0.18em',
textTransform: 'uppercase', color: 'rgba(244,190,40,0.85)',
fontFamily: 'Sora, sans-serif', marginBottom: 6,
}}>
Step {s.step}
</div>
<h3 style={{
margin: '0 0 8px',
fontSize: 'clamp(1.25rem,6.4vw,1.55rem)',
fontWeight: 800, color: '#fff',
letterSpacing: '-0.03em', lineHeight: 1.15,
fontFamily: 'Sora, sans-serif',
}}>
{s.title}
</h3>
<p style={{
margin: '0 auto', maxWidth: 340,
fontSize: '0.85rem', lineHeight: 1.55,
color: 'rgba(255,255,255,0.6)',
fontFamily: 'Sora, sans-serif',
}}>
{s.desc}
</p>
</div>
))}
</div>
{/* Horizontal progress tracker */}
<div style={{ position: 'relative', flexShrink: 0, padding: '0 12px', marginTop: 10 }}>
<div style={{
position: 'absolute', left: 22, right: 22, top: 11,
height: 2, background: 'rgba(244,190,40,0.12)', borderRadius: 2,
}} />
<div ref={mobileLineRef} style={{
position: 'absolute', left: 22, top: 11,
width: '0%', height: 2,
background: 'linear-gradient(90deg,#f4be28 0%,rgba(244,190,40,0.55) 100%)',
borderRadius: 2,
boxShadow: '0 0 10px rgba(244,190,40,0.5)',
}} />
<div style={{ display: 'flex', justifyContent: 'space-between', position: 'relative', zIndex: 2 }}>
{STEPS.map((s, i) => (
<div
key={s.id}
ref={el => { mobileDotRefs.current[i] = el; }}
style={{
width: 24, height: 24, borderRadius: '50%',
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: i === 0 ? '#f4be28' : 'transparent',
border: `1.5px solid ${i === 0 ? '#f4be28' : 'rgba(244,190,40,0.2)'}`,
transition: 'background 0.3s ease, border-color 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease',
}}
>
<span
ref={el => { mobileDotTextRefs.current[i] = el; }}
style={{
fontSize: 9, fontWeight: 800, lineHeight: 1,
fontFamily: 'Sora, sans-serif', letterSpacing: '0.04em',
color: i === 0 ? '#1a1200' : 'rgba(244,190,40,0.22)',
transition: 'color 0.3s ease',
userSelect: 'none',
}}
>
{String(i + 1).padStart(2, '0')}
</span>
</div>
))}
</div>
</div>
</div>
</section>
<style>{`
@keyframes loopShimmer { 0%{background-position:200% center} 100%{background-position:-200% center} }
@@ -456,10 +717,14 @@ export default function TheLoop() {
@keyframes loopFloat1 { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-6px)} }
@keyframes loopFloat2 { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-10px)} }
@keyframes loopFloat3 { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-7px)} }
@media (max-width: 768px) { #the-loop { height: auto !important; } }
@media (prefers-reduced-motion: reduce) { #the-loop [style*="animation"] { animation: none !important; } }
@media (prefers-reduced-motion: reduce) { #the-loop [style*="animation"], #the-loop-mobile [style*="animation"] { animation: none !important; } }
/* Metrics grid collapses to a single column on narrow screens — the
right-hand column divider no longer applies, so drop it. */
@media (max-width: 480px) {
.loop-metric { border-right: none !important; }
}
`}</style>
</section>
{/* Impact metrics — outside section to avoid overflow:hidden clipping */}
<div style={{