diff --git a/.gitignore b/.gitignore index 28a8d9e..10f1453 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,9 @@ next-env.d.ts # editors .claude/ +# agents & analysis +.agents/ +skills-lock.json +Loyaly_Website_Content_Analysis.* + + diff --git a/public/ai_agents/agent-1-c.png b/public/ai_agents/agent-1-c.png new file mode 100644 index 0000000..41fb470 Binary files /dev/null and b/public/ai_agents/agent-1-c.png differ diff --git a/public/ai_agents/agent-1.png b/public/ai_agents/agent-1.png new file mode 100644 index 0000000..21d14dd Binary files /dev/null and b/public/ai_agents/agent-1.png differ diff --git a/public/ai_agents/agent-2-c.png b/public/ai_agents/agent-2-c.png new file mode 100644 index 0000000..274e9c7 Binary files /dev/null and b/public/ai_agents/agent-2-c.png differ diff --git a/public/ai_agents/agent-2.png b/public/ai_agents/agent-2.png new file mode 100644 index 0000000..d5b4de2 Binary files /dev/null and b/public/ai_agents/agent-2.png differ diff --git a/public/ai_agents/agent-3-c.png b/public/ai_agents/agent-3-c.png new file mode 100644 index 0000000..31dda49 Binary files /dev/null and b/public/ai_agents/agent-3-c.png differ diff --git a/public/ai_agents/agent-3.png b/public/ai_agents/agent-3.png new file mode 100644 index 0000000..763b4cb Binary files /dev/null and b/public/ai_agents/agent-3.png differ diff --git a/public/ai_agents/agent-4-c.png b/public/ai_agents/agent-4-c.png new file mode 100644 index 0000000..7f65c19 Binary files /dev/null and b/public/ai_agents/agent-4-c.png differ diff --git a/public/ai_agents/agent-4.png b/public/ai_agents/agent-4.png new file mode 100644 index 0000000..3c09209 Binary files /dev/null and b/public/ai_agents/agent-4.png differ diff --git a/public/images/bg-01.png b/public/images/bg-01.png new file mode 100644 index 0000000..f9d72af Binary files /dev/null and b/public/images/bg-01.png differ diff --git a/public/images/meet_the_agents/meet_the_agents_01.png b/public/images/meet_the_agents/meet_the_agents_01.png new file mode 100644 index 0000000..92f96c7 Binary files /dev/null and b/public/images/meet_the_agents/meet_the_agents_01.png differ diff --git a/public/images/meet_the_agents/meet_the_agents_02.png b/public/images/meet_the_agents/meet_the_agents_02.png new file mode 100644 index 0000000..2451ae2 Binary files /dev/null and b/public/images/meet_the_agents/meet_the_agents_02.png differ diff --git a/public/images/meet_the_agents/meet_the_agents_03.png b/public/images/meet_the_agents/meet_the_agents_03.png new file mode 100644 index 0000000..c47569e Binary files /dev/null and b/public/images/meet_the_agents/meet_the_agents_03.png differ diff --git a/public/images/meet_the_agents/meet_the_agents_04.png b/public/images/meet_the_agents/meet_the_agents_04.png new file mode 100644 index 0000000..fc9b802 Binary files /dev/null and b/public/images/meet_the_agents/meet_the_agents_04.png differ diff --git a/public/images/stock/discover.zip b/public/images/stock/discover.zip new file mode 100644 index 0000000..9246786 Binary files /dev/null and b/public/images/stock/discover.zip differ diff --git a/public/team/1.png b/public/team/1.png new file mode 100644 index 0000000..3089f76 Binary files /dev/null and b/public/team/1.png differ diff --git a/public/team/2.png b/public/team/2.png new file mode 100644 index 0000000..2f8146c Binary files /dev/null and b/public/team/2.png differ diff --git a/public/team/3.png b/public/team/3.png new file mode 100644 index 0000000..0464aca Binary files /dev/null and b/public/team/3.png differ diff --git a/public/team/4.jpg b/public/team/4.jpg new file mode 100644 index 0000000..21b88b8 Binary files /dev/null and b/public/team/4.jpg differ diff --git a/public/who_its_for-business/01.png b/public/who_its_for-business/01.png new file mode 100644 index 0000000..a7facd9 Binary files /dev/null and b/public/who_its_for-business/01.png differ diff --git a/public/who_its_for-business/02.png b/public/who_its_for-business/02.png new file mode 100644 index 0000000..3be2714 Binary files /dev/null and b/public/who_its_for-business/02.png differ diff --git a/public/who_its_for-business/03.png b/public/who_its_for-business/03.png new file mode 100644 index 0000000..b6cd38b Binary files /dev/null and b/public/who_its_for-business/03.png differ diff --git a/public/who_its_for-business/04.png b/public/who_its_for-business/04.png new file mode 100644 index 0000000..4d0c426 Binary files /dev/null and b/public/who_its_for-business/04.png differ diff --git a/src/app/about_us/page.tsx b/src/app/about_us/page.tsx index dbece88..40ba68c 100644 --- a/src/app/about_us/page.tsx +++ b/src/app/about_us/page.tsx @@ -5,6 +5,7 @@ import Link from 'next/link'; import Image from 'next/image'; import { useReveal } from '@/hooks/useReveal'; import GSAPAnimator from '@/components/GSAPAnimator'; +import SectionEyebrow from '@/components/SectionEyebrow'; function Reveal({ children, delay = 0 }: { children: React.ReactNode; delay?: number }) { const { ref, visible } = useReveal(); @@ -70,7 +71,7 @@ export default function AboutUsPage() {
-
Our Story
+ Our Story

We started with
a simple idea.

@@ -158,7 +159,7 @@ export default function AboutUsPage() {
-
Market Opportunity
+ Market Opportunity

13 Million Stores.
One Opportunity.

@@ -291,7 +292,7 @@ function TeamCarousel() { {/* Header */}
-
The Team
+ The Team

Builders. Thinkers.
Retail obsessives.

diff --git a/src/app/solutions_page/page.tsx b/src/app/solutions_page/page.tsx index 9051e84..c1da28b 100644 --- a/src/app/solutions_page/page.tsx +++ b/src/app/solutions_page/page.tsx @@ -1,11 +1,12 @@ 'use client'; -import React from 'react'; +import React, { useState, useEffect } from 'react'; import Link from 'next/link'; import Image from 'next/image'; import { useReveal } from '@/hooks/useReveal'; import GSAPAnimator from '@/components/GSAPAnimator'; import ScrollStack, { ScrollStackItem } from '@/components/ScrollStack'; +import SectionEyebrow from '@/components/SectionEyebrow'; function Reveal({ children, delay = 0 }: { children: React.ReactNode; delay?: number }) { const { ref, visible } = useReveal(); @@ -28,6 +29,7 @@ const VALUES = [ const PRODUCTS = [ { n: '01', title: 'Behavision', subtitle: 'AI Powered Footfall Analytics', color: '#f4be28', + image: '/images/meet_the_agents/meet_the_agents_01.png', desc: 'AI powered footfall analytics and behavioral intelligence for physical retail. Behavision turns every camera and sensor you already have into a live map of how customers move, where they linger, and what makes them walk away, so store layout and staffing decisions are based on evidence, not guesswork.', capabilities: [ 'Real-time footfall counting, accurate to 98% across every entry point', @@ -39,6 +41,7 @@ const PRODUCTS = [ }, { n: '02', title: 'SpendSense', subtitle: 'Customer Spending Intelligence', color: '#10B981', + image: '/images/meet_the_agents/meet_the_agents_02.png', desc: 'Deep, continuously updating intelligence on how your customers spend. SpendSense studies basket size, purchase frequency, and category preferences across every visit, surfacing the high-value segments and the early warning signs of churn well before a customer stops coming back.', capabilities: [ 'Automatic customer segmentation by spend, frequency, and loyalty tier', @@ -50,6 +53,7 @@ const PRODUCTS = [ }, { n: '03', title: 'Dyscount', subtitle: 'Smart Discount Engine', color: '#EF4444', + image: '/images/meet_the_agents/meet_the_agents_03.png', desc: 'A discount engine that treats margin as a budget, not an afterthought. Dyscount decides who gets an offer, how deep it goes, and when it fires, targeting the customers most likely to convert while capping every discount so promotions never quietly eat into profit.', capabilities: [ 'Dynamic offer targeting based on real-time purchase intent signals', @@ -61,6 +65,7 @@ const PRODUCTS = [ }, { n: '04', title: 'Identiq', subtitle: 'Consumer Loyalty Platform', color: '#f4be28', + image: '/images/meet_the_agents/meet_the_agents_04.png', desc: 'The consumer-facing app that closes the loop. Identiq turns daily logins, walking challenges, and quick mini-games into LYTs, Loyaly’s reward tokens, redeemable instantly at any partner store, so every idle moment on a phone becomes a reason to walk into a real one.', capabilities: [ 'Daily login and step-challenge Lyts that build a lasting habit loop', @@ -73,6 +78,17 @@ const PRODUCTS = [ ]; export default function MeetTheCrewPage() { + // Below 860px the product grid stacks vertically, making each card taller than + // the viewport — so we drop the sticky scroll-stack and let them flow normally. + const [isMobile, setIsMobile] = useState(false); + useEffect(() => { + const mq = window.matchMedia('(max-width: 860px)'); + const update = () => setIsMobile(mq.matches); + update(); + mq.addEventListener('change', update); + return () => mq.removeEventListener('change', update); + }, []); + return (
@@ -124,33 +140,75 @@ export default function MeetTheCrewPage() { {/* ── VALUES ── */}
-
How We Work
+ How We Work

What we believe.

-
- {VALUES.map((v, i) => ( +
+ {VALUES.map((v, i) => { + const themes = [ + { g: 'linear-gradient(135deg, #6366f1 0%, #4338ca 100%)', glow: 'rgba(99,102,241,0.5)' }, + { g: 'linear-gradient(135deg, #10b981 0%, #047857 100%)', glow: 'rgba(16,185,129,0.5)' }, + { g: 'linear-gradient(135deg, #fb7185 0%, #be123c 100%)', glow: 'rgba(244,63,94,0.5)' }, + { g: 'linear-gradient(135deg, #f59e0b 0%, #c9960f 100%)', glow: 'rgba(245,158,11,0.5)' }, + ]; + const t = themes[i % themes.length]; + return ( -
- {v.n} -

{v.title}

-

{v.desc}

+ {/* Floating light blob */} + + {/* Glossy sheen + hover shine sweep */} + + + {/* Faint watermark number */} + {v.n} + {/* Number badge */} + {v.n} +

{v.title}

+

{v.desc}

- ))} + ); + })}
{/* ── OUR PRODUCTS ── */}
+ {/* Background photo — confined to the hero band at the top so it isn't + stretched/zoomed across the tall product-stack section below */} +
+ + {/* Darker on the left where the copy sits */} +
+ {/* Fade the band into the section background at the bottom */} +
+
-
Meet the Agents
+ Meet the Agents

Four agents.
One AI.

@@ -160,7 +218,7 @@ export default function MeetTheCrewPage() {
- + {PRODUCTS.map((p, i) => (
- {/* Accent glow blob */} -
+ {/* Background photo */} + + {/* Left-anchored scrim — copy sits on near-solid dark, photo breathes to the right */} +
+ {/* Accent tint from the top-right + bottom vignette for depth */} +
{/* Left — identity + description */}
-
+
{p.n} {p.subtitle}
-

{p.title}

-

{p.desc}

+

{p.title}

+

{p.desc}

{p.tags.map((tag) => (
- {/* Right — metric hero + capabilities */} -
-
-
{p.metric.val}
-
{p.metric.lbl}
-
+ {/* Right — one frosted-glass panel floating on the photo */} +
+ {/* Metric hero */} +
{p.metric.val}
+
{p.metric.lbl}
-
+ {/* Divider */} +
+ + {/* Capabilities */} +
{p.capabilities.map((cap) => (
- {cap} + {cap}
))}
@@ -253,8 +319,18 @@ export default function MeetTheCrewPage() {
diff --git a/src/app/use_cases/page.tsx b/src/app/use_cases/page.tsx index 7c42986..522265f 100644 --- a/src/app/use_cases/page.tsx +++ b/src/app/use_cases/page.tsx @@ -6,6 +6,7 @@ import Image from 'next/image'; import GSAPAnimator from '@/components/GSAPAnimator'; import { useReveal } from '@/hooks/useReveal'; import ProductsScrollSection from '@/components/use-cases/ProductsScrollSection'; +import SectionEyebrow from '@/components/SectionEyebrow'; const OUTCOMES = [ { stat: '3.2×', label: 'Conversion rate increase' }, @@ -16,24 +17,44 @@ const OUTCOMES = [ const CAPS = [ { - n: '01', title: 'Merchant Campaign Platform', color: '#f4be28', + title: 'Merchant Campaign Platform', accent: '#7dd957', + bg: 'linear-gradient(140deg, #1d3f2b 0%, #0f2a1c 100%)', image: '/ai_agents/agent-1-c.png', + icon: (<>), desc: 'Register your store, upload products and inventory, then launch interactive promotional campaigns in minutes. Define maximum discounts per product and track performance in real time.', - metrics: [{ val: '5+', lbl: 'Game formats' }, { val: '1-click', lbl: 'Campaign setup' }], + metrics: [ + { val: '5+', lbl: 'Game formats', icon: (<>) }, + { val: '1-click', lbl: 'Campaign setup', icon: (<>) }, + ], }, { - n: '02', title: 'Game Based Discount Engine', color: '#a78bfa', + title: 'Game Based Discount Engine', accent: '#a78bfa', + bg: 'linear-gradient(140deg, #2a2352 0%, #1a1838 100%)', image: '/ai_agents/agent-2-c.png', + icon: (<>), desc: 'Select a product, choose a game (Maze, Spin Wheel, Scratch Card, or Match Master) and set a maximum discount. Customers play once and discover their reward only after completing the game.', - metrics: [{ val: '4×', lbl: 'Engagement lift' }, { val: '100%', lbl: 'Margin controlled' }], + metrics: [ + { val: '4×', lbl: 'Engagement lift', icon: (<>) }, + { val: '100%', lbl: 'Margin controlled', icon: () }, + ], }, { - n: '03', title: 'Smart Coupon System', color: '#34d399', + title: 'Smart Coupon System', accent: '#34d399', + bg: 'linear-gradient(140deg, #16704a 0%, #0d4d33 100%)', image: '/ai_agents/agent-4-c.png', + icon: (), desc: 'Generate daily surprise coupons through the Battery Challenge and other activities. Offer flat discounts, BOGO, percentage-off deals, festival specials, and brand promotions, fresh every day.', - metrics: [{ val: '6', lbl: 'Coupon types' }, { val: 'Daily', lbl: 'Fresh surprises' }], + metrics: [ + { val: '6', lbl: 'Coupon types', icon: (<>) }, + { val: 'Daily', lbl: 'Fresh surprises', icon: (<>) }, + ], }, { - n: '04', title: 'Footfall & Campaign Analytics', color: '#f97316', + title: 'Footfall & Campaign Analytics', accent: '#ffd8a8', + bg: 'linear-gradient(140deg, #ee7420 0%, #d1560a 100%)', image: '/ai_agents/agent-3-c.png', + icon: (<>), desc: 'Track which campaigns drove store visits, measure conversion rates, monitor repeat footfall, and optimize your promotional budget with a real-time analytics dashboard.', - metrics: [{ val: '98%', lbl: 'Count accuracy' }, { val: '<9ms', lbl: 'Real-time latency' }], + metrics: [ + { val: '98%', lbl: 'Count accuracy', icon: (<>) }, + { val: '<9ms', lbl: 'Real-time latency', icon: () }, + ], }, ]; @@ -58,29 +79,34 @@ const PILLARS = [ const WHO = [ { label: 'Retail Stores', + image: '/who_its_for-business/01.png', icon: , }, { label: 'Shopping Malls', + image: '/who_its_for-business/02.png', icon: , }, { label: 'QSR Chains', + image: '/who_its_for-business/03.png', icon: , }, { label: 'Franchise Businesses', + image: '/who_its_for-business/04.png', icon: , }, ]; -function RevealBlock({ children, delay = 0, className = '' }: { children: React.ReactNode; delay?: number; className?: string }) { +function RevealBlock({ children, delay = 0, className = '', from = 'translateY(48px)', duration = 0.9 }: { children: React.ReactNode; delay?: number; className?: string; from?: string; duration?: number }) { const { ref, visible } = useReveal(); return (
{children}
@@ -89,7 +115,6 @@ function RevealBlock({ children, delay = 0, className = '' }: { children: React. export default function ForBusinessPage() { const [countVisible, setCountVisible] = useState(false); - const [hovered, setHovered] = useState(null); const statsRef = useRef(null); useEffect(() => { @@ -180,27 +205,78 @@ export default function ForBusinessPage() {
-
The Challenge
+ The Challenge

You're Getting
Visitors. But
Missing Revenue.

-
+
{[ - "You don't know who walks into your store.", - "You don't know what they're looking for.", - "You don't know why they leave.", - "And your discounts? Just guesswork.", - ].map((line, i) => ( - -
+ + + ), + }, + { + title: "You don't know what they're looking for.", + bg: 'linear-gradient(135deg, #10b981 0%, #047857 100%)', glow: 'rgba(16,185,129,0.45)', + icon: ( + + + + ), + }, + { + title: "You don't know why they leave.", + bg: 'linear-gradient(135deg, #fb7185 0%, #be123c 100%)', glow: 'rgba(244,63,94,0.45)', + icon: ( + + + + ), + }, + { + title: "And your discounts? Just guesswork.", + bg: 'linear-gradient(135deg, #f59e0b 0%, #c9960f 100%)', glow: 'rgba(245,158,11,0.45)', + icon: ( + + + + ), + }, + ].map((item, i) => ( + +
- 0{i + 1} - {line} + {/* Glossy sheen overlay */} + + {/* Icon tile */} +
{item.icon}
+ {/* Copy */} +
+ {item.title} +
))} @@ -216,7 +292,7 @@ export default function ForBusinessPage() {
-
The AI Agents
+ The AI Agents

Four agents.
One AI.

@@ -227,32 +303,54 @@ export default function ForBusinessPage() {
-
+
{CAPS.map((cap, i) => ( - -
setHovered(i)} - onMouseLeave={() => setHovered(null)} - className="uc-cap-row" - style={{ - display: 'grid', gridTemplateColumns: '80px 1fr 1fr auto', - alignItems: 'center', gap: '4vw', - padding: '40px 0', - borderBottom: '1px solid rgba(0,0,0,0.07)', - cursor: 'default', - transition: 'all 0.3s ease', - }} - > - {cap.n} -

{cap.title}

-

{cap.desc}

-
- {cap.metrics.map((m, j) => ( -
-
{m.val}
-
{m.lbl}
+ +
+ {/* Dotted decoration, top-right */} +
+ + {/* Product image — cropped artwork, centered in the right area with breathing room */} +
+ {cap.title} +
+ + {/* Content — left column; right side left empty for a product image */} +
+ {/* Icon + title + accent underline */} +
+ + {cap.icon} + +
+

{cap.title}

+
- ))} +
+ +

{cap.desc}

+ + {/* Metric chips with icon badges */} +
+ {cap.metrics.map((m, j) => ( +
+ + {m.icon} + +
+
{m.val}
+
{m.lbl}
+
+
+ ))} +
@@ -264,7 +362,7 @@ export default function ForBusinessPage() {
-
How It Works
+ How It Works

Three ways we grow your store.

@@ -416,7 +514,7 @@ export default function ForBusinessPage() {

Loyaly's proximity engine sends instant notifications to shoppers browsing within your store's radius. No ad spend. No guesswork. Just timely, relevant offers delivered at exactly the right moment.

-
+
{[ { v: 'Instant', l: 'Notification speed', icon: }, { v: '0', l: 'Ad spend required', icon: <> }, @@ -446,7 +544,7 @@ export default function ForBusinessPage() { {/* ── OUTCOMES ── */}
-
Outcomes
+ Outcomes

Real Results.
Not Assumptions.

@@ -468,14 +566,17 @@ export default function ForBusinessPage() {
{/* ── WHO IT'S FOR ── */} -
+
{/* Background photo */} - -
+ + {/* Minimal black vignette — keeps the center bright, softly shades only the edges */} +
+ {/* Light lift for the heading, top-left */} +
-
Who It's For
+ Who It's For

Built for every format of physical retail.

@@ -483,14 +584,22 @@ export default function ForBusinessPage() {
{WHO.map((w, i) => ( -
-
{w.icon}
-
{w.label}
+ {/* Background photo */} + + {/* Legibility overlay — heavier at the bottom where the copy sits */} +
+
+
{w.icon}
+
{w.label}
+
))} @@ -521,6 +630,7 @@ export default function ForBusinessPage() { 100% { transform: scale(1.7); opacity: 0; } } .uc-pillar-card:hover { transform: translateY(-8px) scale(1.015); filter: brightness(1.12) saturate(1.08); } + .uc-cap-card:hover { transform: translateY(-6px); box-shadow: 0 34px 64px -32px rgba(23,21,18,0.38); } .uc-radar-dot { animation: ucDotPulse 2s ease-in-out infinite; } @keyframes ucDotPulse { 0%, 100% { transform: scale(1); opacity: 0.85; } 50% { transform: scale(1.25); opacity: 1; } } .uc-pin-halo { animation: ucHaloPulse 2.6s ease-in-out infinite; } @@ -535,15 +645,20 @@ export default function ForBusinessPage() { @keyframes ucMapPing { 0%, 100% { r: 6px; opacity: 0.5; } 50% { r: 13px; opacity: 0.12; } } .uc-map-livedot { animation: ucMapLive 1.5s ease-in-out infinite; } @keyframes ucMapLive { 0% { box-shadow: 0 0 0 0 rgba(22,163,74,0.5); } 70% { box-shadow: 0 0 0 6px rgba(22,163,74,0); } 100% { box-shadow: 0 0 0 0 rgba(22,163,74,0); } } + .uc-who-card:hover { transform: translateY(-6px); border-color: rgba(244,190,40,0.55); } + .uc-challenge-card:hover { transform: translateY(-6px) scale(1.015); box-shadow: 0 30px 55px -20px var(--cc-glow); } + .uc-challenge-card:hover .uc-cc-icon { background: #ffffff; color: #0a0a0f; } @media (max-width: 860px) { .uc-challenge-grid { grid-template-columns: 1fr !important; gap: 40px !important; } .uc-outcomes-grid { grid-template-columns: repeat(2,1fr) !important; } .uc-outcomes-grid > div > div { border-radius: 16px !important; } .uc-who-grid { grid-template-columns: repeat(2,1fr) !important; } - .uc-cap-row { grid-template-columns: 1fr !important; gap: 14px !important; padding: 28px 0 !important; } - .uc-cap-row > div:last-child { justify-content: flex-start !important; } + .uc-cap-grid { grid-template-columns: 1fr !important; } + .uc-cap-content { width: 62% !important; } + .uc-cap-art { width: 40% !important; } .uc-pillar-grid { grid-template-columns: 1fr !important; } .uc-proximity-grid { grid-template-columns: 1fr !important; gap: 40px !important; } + .uc-prox-stats { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; } .uc-timeline-row { grid-template-columns: 44px 1fr !important; gap: 18px !important; } .uc-timeline-row > div:first-child { width: 44px !important; height: 44px !important; } .uc-timeline-line { left: 21px !important; } @@ -551,6 +666,8 @@ export default function ForBusinessPage() { @media (max-width: 520px) { .uc-outcomes-grid { grid-template-columns: 1fr !important; } .uc-who-grid { grid-template-columns: 1fr !important; } + .uc-cap-content { width: 100% !important; } + .uc-cap-art { display: none !important; } } @media (prefers-reduced-motion: reduce) { .uc-radar-ring { animation: none !important; opacity: 0.4 !important; } diff --git a/src/components/ScrollStack.css b/src/components/ScrollStack.css index a8f812f..11d7c8b 100644 --- a/src/components/ScrollStack.css +++ b/src/components/ScrollStack.css @@ -13,7 +13,7 @@ } .scroll-stack-inner { - padding: 20vh 0 20rem; + padding: 8vh 0 20rem; min-height: 100vh; } diff --git a/src/components/ScrollStack.tsx b/src/components/ScrollStack.tsx index 485f99e..dbbfb78 100644 --- a/src/components/ScrollStack.tsx +++ b/src/components/ScrollStack.tsx @@ -29,6 +29,7 @@ interface ScrollStackProps { blurAmount?: number; hideBehind?: boolean; useWindowScroll?: boolean; + disabled?: boolean; onStackComplete?: () => void; } @@ -45,6 +46,7 @@ const ScrollStack = ({ blurAmount = 0, hideBehind = false, useWindowScroll = false, + disabled = false, onStackComplete, }: ScrollStackProps) => { const scrollerRef = useRef(null); @@ -288,6 +290,20 @@ const ScrollStack = ({ cardsRef.current = cards; const transformsCache = lastTransformsRef.current; + // Disabled (e.g. on mobile): clear any pinning transforms so the cards + // flow as a normal scrollable list, and skip the whole sticky-stack setup. + if (disabled) { + cards.forEach((card) => { + card.style.transform = ''; + card.style.filter = ''; + card.style.opacity = ''; + card.style.marginBottom = ''; + card.style.willChange = ''; + }); + transformsCache.clear(); + return; + } + cards.forEach((card, i) => { if (i < cards.length - 1) { card.style.marginBottom = `${itemDistance}px`; @@ -323,7 +339,7 @@ const ScrollStack = ({ isUpdatingRef.current = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [itemDistance, itemScale, itemStackDistance, stackPosition, scaleEndPosition, baseScale, rotationAmount, blurAmount, useWindowScroll, onStackComplete, setupLenis, updateCardTransforms, measurePositions]); + }, [itemDistance, itemScale, itemStackDistance, stackPosition, scaleEndPosition, baseScale, rotationAmount, blurAmount, useWindowScroll, disabled, onStackComplete, setupLenis, updateCardTransforms, measurePositions]); return (
-
+
{children} {/* Spacer so the last pin can release cleanly */}
diff --git a/src/components/SectionEyebrow.tsx b/src/components/SectionEyebrow.tsx new file mode 100644 index 0000000..f99f4d0 --- /dev/null +++ b/src/components/SectionEyebrow.tsx @@ -0,0 +1,124 @@ +'use client'; + +import React from 'react'; + +/* ── Section eyebrow — the small uppercase label that sits above a section + heading. One component, several distinct motifs, so each section on a page + can carry a visually unique kicker while staying consistent in quality. + + variant: + 'line' — a gold rule that fades out, then the label + 'bar' — a short vertical gradient bar + label + 'pill' — label inside a soft bordered gold pill + 'bracket' — label framed by two L-shaped corner brackets + 'dot' — a pulsing gold dot + label + 'number' — a large index figure + hairline divider + label + + `index` is used by the 'number' variant (e.g. "01"). + Colours are gold-accented and read on both light and dark backgrounds. ── */ + +const ACCENT = '#f4be28'; + +export type EyebrowVariant = 'line' | 'bar' | 'pill' | 'bracket' | 'dot' | 'number'; + +export default function SectionEyebrow({ + children, + variant = 'line', + index, + style, +}: { + children: React.ReactNode; + variant?: EyebrowVariant; + index?: string; + style?: React.CSSProperties; +}) { + const label: React.CSSProperties = { + fontFamily: 'Sora, sans-serif', fontWeight: 700, textTransform: 'uppercase', + fontSize: 13, letterSpacing: '0.22em', color: ACCENT, lineHeight: 1, + whiteSpace: 'nowrap', + }; + const row: React.CSSProperties = { + display: 'inline-flex', alignItems: 'center', gap: 14, ...style, + }; + + if (variant === 'pill') { + return ( + + {children} + + ); + } + + if (variant === 'bar') { + return ( + + + {children} + + ); + } + + if (variant === 'bracket') { + return ( + + + + {children} + + ); + } + + if (variant === 'dot') { + return ( + + + {children} + + + ); + } + + if (variant === 'number') { + return ( + + {index} + + {children} + + ); + } + + // 'line' (default) + return ( + + + {children} + + ); +} diff --git a/src/components/for-people/EarnRewardsSection.tsx b/src/components/for-people/EarnRewardsSection.tsx index fc7d1ed..dc08f78 100644 --- a/src/components/for-people/EarnRewardsSection.tsx +++ b/src/components/for-people/EarnRewardsSection.tsx @@ -3,7 +3,6 @@ import React, { useEffect, useLayoutEffect, useRef } from 'react'; import { gsap } from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; -import { PhoneMockupImage } from './PhoneMockup'; import { FpTagCard, FpStatCard } from './FpCard'; import { useCountUp, useStaggerReveal } from './fpMotion'; @@ -77,14 +76,66 @@ export default function EarnRewardsSection() {
- {/* Left — phone mockup, matches the in-app Earn / Home experience */} + {/* Left — live LytsUp Earn preview embedded in a real phone frame */}
- +
+ {/* Screen — locked to a real phone's aspect ratio (~9:19.5) */} +
+ {/* Dynamic island */} +
+ {/* Safe area — reserves the notch strip above the app's top bar */} +
+ {/* The embed is a Flutter web app built for a real phone's logical + width (390px). Rendering it at 390 and scaling to fit keeps the + UI proportioned as designed instead of cramped at ~280px. The + iframe is overscanned ~16px past the right clip so Flutter's + desktop-style list scrollbar falls outside the visible screen. */} +
+