diff --git a/public/_fp-icon-check.html b/public/_fp-icon-check.html new file mode 100644 index 0000000..b162bae --- /dev/null +++ b/public/_fp-icon-check.html @@ -0,0 +1,46 @@ + + +icon check + + + +
+ + + diff --git a/public/assets/images/about_bg.png b/public/assets/images/about_bg.png deleted file mode 100644 index 5cdfe3d..0000000 Binary files a/public/assets/images/about_bg.png and /dev/null differ diff --git a/public/assets/images/aboutus_bg.png b/public/assets/images/aboutus_bg.png new file mode 100644 index 0000000..c6f767f Binary files /dev/null and b/public/assets/images/aboutus_bg.png differ diff --git a/src/app/about_us/page.tsx b/src/app/about_us/page.tsx index a73aea6..b952fed 100644 --- a/src/app/about_us/page.tsx +++ b/src/app/about_us/page.tsx @@ -31,10 +31,14 @@ export default function AboutUsPage() { margin: 'var(--hero-top) var(--sec-gutter) var(--sec-gap) var(--sec-gutter)', borderRadius: '32px', position: 'relative', overflow: 'hidden', }}> - {/* Background photo */} - -
-
+ {/* Background photo — the team shot's subjects sit centre-frame, which is + exactly where the headline lands. The scrims below are tuned to keep + the faces readable rather than blacking them out: a bottom-weighted + gradient carries most of the text contrast, and the centre ellipse is + kept light. The headline earns the rest back with a text shadow. */} + +
+
{/* Accent blobs */}
@@ -45,11 +49,13 @@ export default function AboutUsPage() { fontSize: 'clamp(52px, 9vw, 140px)', fontWeight: 800, letterSpacing: '-0.06em', lineHeight: 0.88, color: '#ffffff', margin: '0 0 36px', fontFamily: 'Sora, sans-serif', + // Carries legibility over the photo so the scrim doesn't have to. + textShadow: '0 2px 30px rgba(10,7,20,0.7)', }}> The People
Behind Loyaly. -

+

We're not just building a product. We're redefining retail, one store at a time.

@@ -77,7 +83,7 @@ export default function AboutUsPage() { {/* ── MISSION & VISION ── */} -
+
{/* Ambient top glow */}
@@ -326,6 +332,37 @@ export default function AboutUsPage() { .au-mv-grid { grid-template-columns: 1fr !important; } .au-mv-grid > div > div { padding: 48px 32px !important; min-height: 280px !important; } } + /* Phones — the icon and copy sat side by side, which left the text roughly + 90px wide at 320px (two words per line). Stack them so the copy gets the + full card width, and drop the min-height so the card sizes to content. */ + @media (max-width: 600px) { + /* 8% side padding costs ~48px of a 320px screen on top of the section + gutters and card padding — trim it so the copy gets the width back. */ + .au-mv-section { padding: 56px 14px !important; } + .au-mv-card-layout { + flex-direction: column !important; + gap: 20px !important; + } + .au-mv-grid > div > div, + .au-mv-card { + padding: 32px 22px !important; + min-height: 0 !important; + justify-content: flex-start !important; + } + /* Slightly smaller badge now that it leads the stack */ + .au-mv-card-layout > div:first-child { + width: 64px !important; + height: 64px !important; + } + .au-mv-card-layout > div:first-child svg { + width: 28px !important; + height: 28px !important; + } + } + @media (max-width: 360px) { + .au-mv-grid > div > div, + .au-mv-card { padding: 26px 18px !important; } + } `} ); @@ -374,6 +411,20 @@ function TeamCarousel() { // Dimensions matching premium layout const CARD_W = 320; const CARD_GAP = 24; + // One card height for every state. The active/inactive size difference is done + // with scale (compositor-only) instead of animating height, which used to + // relayout the whole track on every frame of the transition. + const CARD_H = 470; + const INACTIVE_SCALE = 0.88; + + // Shared motion tokens so the track, cards and details all move on one curve. + const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; + const DUR = 0.75; + + // The per-card stagger belongs to the entrance only. It used to stay applied + // forever, so every later slide change inherited a delay that grew with the + // card's index — the far cards visibly lagged the near ones. + const [entered, setEntered] = useState(false); // The card width (and gap) is responsive via CSS media queries, so the slide // distance can't be a hard-coded constant. We measure the real pixel offset @@ -402,6 +453,15 @@ function TeamCarousel() { }; }, [visible]); + // Once the staggered entrance has finished, drop the per-card delay so slide + // changes respond instantly and all cards move together. + useEffect(() => { + if (!visible) return; + const totalMs = (0.15 + (count - 1) * 0.08 + DUR) * 1000; + const t = setTimeout(() => setEntered(true), totalMs); + return () => clearTimeout(t); + }, [visible, count]); + const go = (dir: number) => setActive(a => Math.min(count - 1, Math.max(0, a + dir))); const handleTouchStart = (e: React.TouchEvent) => { @@ -640,8 +700,9 @@ function TeamCarousel() { gap: CARD_GAP, alignItems: 'center', height: '100%', - transform: `translateX(${-active * step}px)`, - transition: 'transform 0.7s cubic-bezier(0.2, 0.8, 0.2, 1)', + transform: `translate3d(${-active * step}px, 0, 0)`, + transition: `transform ${DUR}s ${EASE}`, + willChange: 'transform', }} > {TEAM_MEMBERS.map((member, i) => { @@ -657,21 +718,28 @@ function TeamCarousel() { flexShrink: 0, width: CARD_W, position: 'relative', - height: isActive ? 470 : 400, + height: CARD_H, borderRadius: 20, overflow: 'hidden', cursor: isActive ? 'default' : 'pointer', background: '#0b0b10', - border: isActive ? '2px solid #f4be28' : '1px solid rgba(255,255,255,0.06)', - boxShadow: isActive - ? '0 25px 50px rgba(0,0,0,0.8), 0 0 30px rgba(244,190,40,0.18)' - : '0 10px 30px rgba(0,0,0,0.4)', - transition: 'all 0.7s cubic-bezier(0.2, 0.8, 0.2, 1)', + // 2px→1px border used to shift the inner layout by a pixel + // mid-transition; keep the box identical and change colour only. + border: '1.5px solid', + borderColor: isActive ? '#f4be28' : 'rgba(255,255,255,0.06)', + boxShadow: isActive + ? '0 14px 30px -8px rgba(0,0,0,0.45), 0 0 22px rgba(244,190,40,0.14)' + : '0 8px 20px -6px rgba(0,0,0,0.28)', + // Explicit list — `all` was also transitioning height, forcing + // a full relayout of the track on every frame. + transition: `transform ${DUR}s ${EASE}, opacity ${DUR}s ${EASE}, border-color 0.45s ease, box-shadow 0.6s ease`, + transitionDelay: entered ? '0s' : `${0.15 + i * 0.08}s`, opacity: visible ? (isActive ? 1 : 0.45) : 0, - transform: visible - ? (isActive ? 'scale(1)' : 'scale(0.94)') - : 'translateY(48px) scale(0.92)', - transitionDelay: visible ? `${0.15 + i * 0.08}s` : '0s', + transform: visible + ? (isActive ? 'scale(1)' : `scale(${INACTIVE_SCALE})`) + : `translate3d(0,48px,0) scale(${INACTIVE_SCALE - 0.02})`, + willChange: 'transform, opacity', + backfaceVisibility: 'hidden', }} > {/* Top Left Number Tag */} @@ -767,28 +835,40 @@ function TeamCarousel() { marginBottom: 14 }}>{member.role}
- {/* Description */} -

{member.description}

+ transition: `grid-template-rows ${DUR}s ${EASE}, opacity 0.45s ease ${isActive ? '0.15s' : '0s'}`, + }}> +
+

{member.description}

+
+
{/* Social icons */}
+ {/* The clipping box must own no padding/margin of its own — + either would floor its height and leave a stub when the + row collapses to 0fr. Spacing goes on the child inside. */} +
+
{member.socials.linkedin && ( )} +
+
@@ -960,7 +1042,7 @@ function TeamCarousel() { background: rgba(7, 7, 13, 0.95) !important; border-color: #f4be28 !important; transform: translateY(-50%) scale(1.08) !important; - box-shadow: 0 15px 35px rgba(0,0,0,0.7), 0 0 20px rgba(244,190,40,0.3) !important; + box-shadow: 0 10px 22px -6px rgba(0,0,0,0.45), 0 0 14px rgba(244,190,40,0.22) !important; } .team-social:hover { border-color: #f4be28 !important; @@ -972,7 +1054,8 @@ function TeamCarousel() { .team-card:not(.is-active):hover { border-color: rgba(255,255,255,0.15) !important; opacity: 0.75 !important; - transform: scale(0.96) !important; + /* Sits just above the 0.88 resting scale — a lift toward the active card. */ + transform: scale(0.92) !important; } .team-card:not(.is-active):hover img { filter: grayscale(0.5) brightness(0.5) contrast(0.95) !important; @@ -1004,9 +1087,9 @@ function TeamCarousel() { rim, and a soft light spilling from the top edge. ── */ .team-card.is-active { box-shadow: - 0 34px 70px -20px rgba(0,0,0,0.9), + 0 20px 42px -18px rgba(0,0,0,0.55), 0 0 0 1px rgba(244,190,40,0.35), - 0 0 46px -6px rgba(244,190,40,0.28) !important; + 0 0 30px -8px rgba(244,190,40,0.2) !important; } .team-card.is-active::after { content: ''; @@ -1077,11 +1160,9 @@ function TeamCarousel() { .team-track { gap: 18px !important; } + /* One height for both states — the active/inactive difference is scale. */ .team-card { width: min(300px, 80vw) !important; - height: 412px !important; - } - .team-card.is-active { height: 452px !important; } .team-next-overlay { @@ -1108,14 +1189,13 @@ function TeamCarousel() { } .team-card { width: min(272px, 82vw) !important; - height: 388px !important; - } - .team-card.is-active { height: 424px !important; } } @media (prefers-reduced-motion: reduce) { .team-glow, .team-kb img, .team-sheen { animation: none !important; } + /* Cards still change state, just without the slide/scale travel. */ + .team-track, .team-card { transition-duration: 0.01ms !important; } } `} diff --git a/src/app/for_business/page.tsx b/src/app/for_business/page.tsx index 24df384..c0992d7 100644 --- a/src/app/for_business/page.tsx +++ b/src/app/for_business/page.tsx @@ -150,9 +150,12 @@ export default function ForBusinessPage() { backgroundSize: '72px 72px', }} /> -
-
-

+ {/* maxWidth:100% — this is a fit-content flex item, so without it a long + unbreakable word in the h1 stretches it past the viewport and drags + the centred paragraphs out with it. */} +
+

{/* ── PROXIMITY MARKETING ── */} -
-
+ {/* Colored gradient panel — copper bloom into gold over a warm near-black. + Deliberately warmer than the flat #07070d "How It Works" panel above it, + so two dark sections in a row still read as distinct surfaces. */} +
+
{/* Animated city map with proximity pins */} + {/* Glass panel — deliberately translucent so the animated gradient + drifts through the map instead of being blocked by an opaque box. */}
+ {/* Gold bloom pooling under the store, and a vignette to sink the edges */} +
- {/* Building blocks */} - + {/* Building blocks — faint glass slabs with a gold edge */} + @@ -431,8 +446,8 @@ export default function ForBusinessPage() { - {/* Main roads */} - + {/* Main roads — gold-tinted, not grey */} + @@ -441,7 +456,7 @@ export default function ForBusinessPage() { {/* Secondary streets */} - + @@ -451,39 +466,39 @@ export default function ForBusinessPage() { {/* Proximity radar rings from the store */} - - - - + + + + {/* Routes to nearby shoppers */} - + {[[120, 80], [300, 105], [150, 240], [312, 220]].map(([x, y], i) => ( ))} - {/* Store glow */} - - {/* Shopper pins */} + {/* Store glow — kept low; the panel bloom and pin already light this area */} + + {/* Shopper pins — gold core on a dark ring so they glow against the map */} {[[120, 80], [300, 105], [150, 240], [312, 220]].map(([x, y], i) => ( - + ))} {/* Store pin (centered, matches SVG store at 200,150) */} -
- +
+
- +
@@ -492,26 +507,28 @@ export default function ForBusinessPage() { {/* Live counter chip */}
- - 18 shoppers nearby + + 18 shoppers nearby
{/* Content */}
-
+
- Capability Spotlight + Capability Spotlight
-

- Reach customers the moment they're nearby. +

+ Reach customers the moment they're nearby.

-

+

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.

@@ -521,18 +538,21 @@ export default function ForBusinessPage() { { v: 'Real-time', l: 'Radius targeting', icon: <> }, ].map((s) => (
- {s.icon} + {s.icon}
-
{s.v}
-
{s.l}
+
{s.v}
+
{s.l}
))}
@@ -635,8 +655,22 @@ export default function ForBusinessPage() { @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; } @keyframes ucHaloPulse { 0%, 100% { transform: scale(1); opacity: 0.75; } 50% { transform: scale(1.28); opacity: 0.35; } } + /* Proximity panel — animated colored gradient, no black in the ramp. + Deep copper → amber → plum, drifting slowly across an oversized + background box. Saturated, but every stop stays dark enough that the + white headline and 55% body copy hold contrast at any frame. */ + .uc-prox-panel { + background-color: #3a1c07; + background-image: linear-gradient(120deg, #3a1c07 0%, #5c2d0c 20%, #6b3a12 38%, #5a2f22 56%, #33204a 78%, #3a1c07 100%); + background-size: 300% 300%; + animation: ucProxGradient 24s ease-in-out infinite; + } + @keyframes ucProxGradient { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + } .uc-prox-stat { transition: transform 0.3s cubic-bezier(0.22,1,0.36,1), box-shadow 0.3s ease, border-color 0.3s ease; } - .uc-prox-stat:hover { transform: translateY(-4px); box-shadow: 0 20px 42px -22px rgba(23,21,18,0.4); border-color: rgba(244,190,40,0.5); } + .uc-prox-stat:hover { transform: translateY(-4px); box-shadow: 0 24px 46px -24px rgba(244,190,40,0.45), inset 0 1px 0 rgba(255,255,255,0.12); border-color: rgba(244,190,40,0.55); } .uc-map-ring { animation: ucMapRing 3.4s ease-out infinite; } @keyframes ucMapRing { 0% { r: 18px; opacity: 0.5; } 100% { r: 118px; opacity: 0; } } .uc-map-link { stroke-dasharray: 5 7; animation: ucMapDash 1s linear infinite; } @@ -666,6 +700,15 @@ export default function ForBusinessPage() { } /* ── AI Agents — phone redesign: drop the artwork, let copy use the full width, and give each card a compact, self-sizing height ── */ + /* Hero headline — the base clamp floors at 52px, but below ~480px the 8vw + term is far smaller than that floor, so the floor won and the longest + word ("Relationship.") could not fit the line. It overflowed its box and, + because the wrapper is a fit-content flex item, dragged the centred + paragraphs out past the hero edges with it. Phones get their own ramp; + every width above 480px keeps the original sizing untouched. */ + @media (max-width: 480px) { + .uc-hero-h1 { font-size: clamp(30px, 9vw, 44px) !important; } + } @media (max-width: 640px) { .uc-cap-section { padding: 96px 6% !important; } .uc-cap-header { margin-bottom: 44px !important; } @@ -686,6 +729,8 @@ export default function ForBusinessPage() { @media (prefers-reduced-motion: reduce) { .uc-radar-ring { animation: none !important; opacity: 0.4 !important; } .uc-radar-dot, .uc-pin-halo, .uc-map-ring, .uc-map-link, .uc-map-shopper, .uc-map-livedot { animation: none !important; } + /* Hold the gradient mid-drift rather than snapping it back to 0% */ + .uc-prox-panel { animation: none !important; background-position: 50% 50% !important; } } `} diff --git a/src/app/globals.css b/src/app/globals.css index b79b73a..6921b91 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -10,21 +10,21 @@ --loyaly-copper: #bc7122; /* Section gutters. --sec-gutter = left/right side margin (+ width calc); --sec-gap = vertical gap between stacked sections; --hdr-gutter = header side padding. - On phones the side gutter goes to 0 (full-bleed sections) while the vertical - gap is preserved. Laptop/desktop/tablet keep the original values. */ + On phones the side gutter narrows to 10px while the vertical gap is + preserved. Laptop/desktop/tablet keep the original values. */ --sec-gutter: 20px; --sec-gap: 20px; --hero-top: 20px; --hdr-gutter: 48px; } -/* Phones only — sections span the full width (no left/right margin), - and hero sections sit flush to the top (top margin 0). */ +/* Phones only — sections keep a 10px left/right margin, + and hero sections keep a 10px top margin. */ @media (max-width: 600px) { :root { - --sec-gutter: 0px; + --sec-gutter: 10px; --sec-gap: 12px; - --hero-top: 0px; + --hero-top: 10px; --hdr-gutter: 20px; } } diff --git a/src/app/meet_the_agents/page.tsx b/src/app/meet_the_agents/page.tsx index f095073..a66e26f 100644 --- a/src/app/meet_the_agents/page.tsx +++ b/src/app/meet_the_agents/page.tsx @@ -339,7 +339,7 @@ export default function MeetTheCrewPage() { } .mtc-product-card:hover { transform: translateY(-4px) scale(1.005); - box-shadow: 0 45px 95px -30px rgba(0, 0, 0, 0.9), inset 0 1px 0 rgba(255, 255, 255, 0.12); + box-shadow: 0 24px 52px -26px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.12); } .mtc-product-card:active { transform: scale(0.995); diff --git a/src/components/for-people/DiscoverDealsSection.tsx b/src/components/for-people/DiscoverDealsSection.tsx index e3080b5..559e400 100644 --- a/src/components/for-people/DiscoverDealsSection.tsx +++ b/src/components/for-people/DiscoverDealsSection.tsx @@ -5,16 +5,17 @@ import Image from 'next/image'; import { gsap } from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; import { FpTagCard } from './FpCard'; +import FpIcon, { type FpIconName } from './FpIcons'; import { useCountUp, useParallax, useStaggerReveal } from './fpMotion'; if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; -const TAGS = [ - { label: 'Earn as you live', icon: '⚡' }, - { label: 'Play to win', icon: '🎮' }, - { label: 'Spend anywhere', icon: '🛍️' }, +const TAGS: { label: string; icon: FpIconName }[] = [ + { label: 'Earn as you live', icon: 'bolt' }, + { label: 'Play to win', icon: 'gamepad' }, + { label: 'Spend anywhere', icon: 'bag' }, ]; export default function DiscoverDealsSection() { @@ -85,7 +86,7 @@ export default function DiscoverDealsSection() {
{TAGS.map((tag) => ( - {tag.label} + }>{tag.label} ))}
diff --git a/src/components/for-people/EarnRewardsSection.tsx b/src/components/for-people/EarnRewardsSection.tsx index 6170f36..9e7cab2 100644 --- a/src/components/for-people/EarnRewardsSection.tsx +++ b/src/components/for-people/EarnRewardsSection.tsx @@ -4,6 +4,7 @@ import React, { useEffect, useLayoutEffect, useRef } from 'react'; import { gsap } from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; import { FpTagCard, FpStatCard } from './FpCard'; +import FpIcon, { type FpIconName } from './FpIcons'; import { useCountUp, useStaggerReveal } from './fpMotion'; if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); @@ -44,17 +45,17 @@ export default function EarnRewardsSection() { return () => ctx.revert(); }, []); - const earnTags = [ - { label: 'Walk', icon: '🚶', bg: '#eef2ff', fg: '#4c5fd5' }, - { label: 'Visit', icon: '📍', bg: '#fdeeee', fg: '#d5514c' }, - { label: 'Selfie', icon: '🤳', bg: '#fef1e8', fg: '#d5824c' }, - { label: 'Spin', icon: '🎡', bg: '#f1eefd', fg: '#7a4cd5' }, - { label: 'Scratch', icon: '🎟️', bg: '#fdeef7', fg: '#d54ca3' }, - { label: 'Challenge', icon: '🏆', bg: '#fff8e1', fg: '#b8860b' }, - { label: 'Friend', icon: '🤝', bg: '#eef9f1', fg: '#3f9d5c' }, - { label: 'Shop', icon: '🛍️', bg: '#eef7fd', fg: '#2f8fc4' }, - { label: 'Event', icon: '🎉', bg: '#fdf1ee', fg: '#c45f2f' }, - { label: 'Brand', icon: '🏷️', bg: '#fef7ea', fg: '#c49a2f' }, + const earnTags: { label: string; icon: FpIconName; bg: string; fg: string }[] = [ + { label: 'Walk', icon: 'walk', bg: '#eef2ff', fg: '#4c5fd5' }, + { label: 'Visit', icon: 'pin', bg: '#fdeeee', fg: '#d5514c' }, + { label: 'Selfie', icon: 'camera', bg: '#fef1e8', fg: '#d5824c' }, + { label: 'Spin', icon: 'wheel', bg: '#f1eefd', fg: '#7a4cd5' }, + { label: 'Scratch', icon: 'ticket', bg: '#fdeef7', fg: '#d54ca3' }, + { label: 'Challenge', icon: 'trophy', bg: '#fff8e1', fg: '#b8860b' }, + { label: 'Friend', icon: 'users', bg: '#eef9f1', fg: '#3f9d5c' }, + { label: 'Shop', icon: 'bag', bg: '#eef7fd', fg: '#2f8fc4' }, + { label: 'Event', icon: 'sparkles', bg: '#fdf1ee', fg: '#c45f2f' }, + { label: 'Brand', icon: 'tag', bg: '#fef7ea', fg: '#c49a2f' }, ]; return ( @@ -83,7 +84,7 @@ export default function EarnRewardsSection() { style={{ width: 300, 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)', + boxShadow: '0 28px 60px -22px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.06)', position: 'relative', }} > @@ -142,7 +143,10 @@ export default function EarnRewardsSection() { } @media (prefers-reduced-motion: reduce) { .erm-phone-shell { animation: none !important; } } @media (max-width: 900px) { - .erm-phone-shell { width: 280px !important; --pm-scale: calc(260 / 390); } + /* min(...,100%): a hard 280px overflows the ~256px available at + 320px. The iframe is sized 100%/scale then scaled back, so it + still fills the screen exactly at any shell width. */ + .erm-phone-shell { width: min(280px, 100%) !important; --pm-scale: calc(260 / 390); } } `}
@@ -163,7 +167,7 @@ export default function EarnRewardsSection() {
{earnTags.map((tag) => ( - + } iconBg={tag.bg} iconColor={tag.fg}> {tag.label} ))} @@ -171,11 +175,11 @@ export default function EarnRewardsSection() { {/* Stat strip — same card system as the tags above, distinguished by a filled dark surface */}
- } value={10} label="ways to earn" /> - } value="24/7" label="always live" /> - } value="∞" label="no earn cap" />
diff --git a/src/components/for-people/FinalCTASection.tsx b/src/components/for-people/FinalCTASection.tsx index dc16ed2..e4bfdca 100644 --- a/src/components/for-people/FinalCTASection.tsx +++ b/src/components/for-people/FinalCTASection.tsx @@ -5,6 +5,7 @@ import Link from 'next/link'; import { gsap } from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; import { useMagnetic as useMagneticShared } from './fpMotion'; +import FpIcon, { type FpIconName } from './FpIcons'; if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); @@ -12,11 +13,11 @@ const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffec -const PROOF = [ - { val: '50K+', label: 'Active users', color: '#f4be28', icon: '👥', iconBg: '#fffbeb' }, - { val: '10K+', label: 'Partner stores', color: '#16a34a', icon: '🏬', iconBg: '#f0fdf4' }, - { val: '₹28L+', label: 'Lyts given', color: '#2563eb', icon: '🎁', iconBg: '#eff6ff' }, - { val: '4.9★', label: 'App rating', color: '#9333ea', icon: '⭐', iconBg: '#faf5ff' }, +const PROOF: { val: string; label: string; color: string; icon: FpIconName; iconBg: string }[] = [ + { val: '50K+', label: 'Active users', color: '#f4be28', icon: 'users', iconBg: '#fffbeb' }, + { val: '10K+', label: 'Partner stores', color: '#16a34a', icon: 'store', iconBg: '#f0fdf4' }, + { val: '₹28L+', label: 'Lyts given', color: '#2563eb', icon: 'gift', iconBg: '#eff6ff' }, + { val: '4.9★', label: 'App rating', color: '#9333ea', icon: 'star', iconBg: '#faf5ff' }, ]; export default function FinalCTASection() { @@ -262,8 +263,8 @@ export default function FinalCTASection() { {p.icon} + color: p.color, marginBottom: 12, + }}>
{p.val}
{p.label}
diff --git a/src/components/for-people/FpCard.tsx b/src/components/for-people/FpCard.tsx index 510d93c..4ca5026 100644 --- a/src/components/for-people/FpCard.tsx +++ b/src/components/for-people/FpCard.tsx @@ -23,22 +23,25 @@ const DEFAULT_ICON_BG: Record = { }; const sora = 'Sora, sans-serif'; +const GOLD = '#f4be28'; interface TagCardProps { theme?: Theme; icon: React.ReactNode; /** Icon-badge background; falls back to the theme's gold tint. */ iconBg?: string; + /** Icon tint — icons draw in `currentColor`. Falls back to the theme's gold. */ + iconColor?: string; children: React.ReactNode; className?: string; style?: React.CSSProperties; } /** Compact icon + label pill (radius 12). */ -export function FpTagCard({ theme = 'light', icon, iconBg, children, className = '', style }: TagCardProps) { +export function FpTagCard({ theme = 'light', icon, iconBg, iconColor, children, className = '', style }: TagCardProps) { return (
- {icon} + {icon} ref. */ value: React.ReactNode; label: string; @@ -65,7 +70,7 @@ interface StatCardProps { /** Larger icon + value + label card (radius 20). */ export function FpStatCard({ - theme = 'light', layout = 'column', icon, iconBg, value, label, color, className = '', style, + theme = 'light', layout = 'column', icon, iconBg, iconColor, value, label, color, className = '', style, }: StatCardProps) { const dark = theme !== 'light'; const column = layout === 'column'; @@ -74,7 +79,7 @@ export function FpStatCard({ className={`fp-card fp-card--stat ${themeClass[theme]} ${className}`.trim()} style={{ flexDirection: column ? 'column' : 'row', alignItems: 'center', ...style }} > - {icon} + {icon}
= { + walk: ( + <> + + + + + + ), + compass: ( + <> + + + + ), + users: ( + <> + + + + + + ), + store: ( + <> + + + + + + ), + tag: ( + <> + + + + ), + sparkles: ( + <> + + + + ), + bolt: , + gamepad: ( + <> + + + + + ), + bag: ( + <> + + + + ), + gift: ( + <> + + + + + + + ), + star: , + pin: ( + <> + + + + ), + camera: ( + <> + + + + ), + wheel: ( + <> + + + + + + ), + ticket: ( + <> + + + + ), + trophy: ( + <> + + + + + + + + ), + map: ( + <> + + + + ), + coffee: ( + <> + + + + + ), + sneaker: ( + <> + + + + ), + ban: ( + <> + + + + ), + check: ( + <> + + + + ), + flame: ( + + ), + rocket: ( + <> + + + + + + ), +}; + +interface FpIconProps { + name: FpIconName; + /** Rendered px size. 16 suits the 30px badge, 20 the 40px `--lg` badge. */ + size?: number; + strokeWidth?: number; + className?: string; +} + +export default function FpIcon({ name, size = 16, strokeWidth = 1.75, className }: FpIconProps) { + return ( + + ); +} diff --git a/src/components/for-people/LiveMoreSection.tsx b/src/components/for-people/LiveMoreSection.tsx index 59bedfd..0d3a103 100644 --- a/src/components/for-people/LiveMoreSection.tsx +++ b/src/components/for-people/LiveMoreSection.tsx @@ -4,6 +4,7 @@ import React, { useEffect, useLayoutEffect, useRef } from 'react'; import { gsap } from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; import { PhoneMockupImage } from './PhoneMockup'; +import FpIcon, { type FpIconName } from './FpIcons'; if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); @@ -61,16 +62,16 @@ export default function LiveMoreSection() {

- {[ - { label: 'Walk more', icon: '🚶', bg: '#eef2ff' }, - { label: 'Explore more', icon: '🧭', bg: '#fdeef7' }, - { label: 'Meet friends', icon: '🤝', bg: '#eef9f1' }, - { label: 'Support local', icon: '🏪', bg: '#fef1e8' }, - { label: 'Discover new brands', icon: '🏷️', bg: '#fef7ea' }, - { label: 'Celebrate every day', icon: '🎊', bg: '#fdeeee' }, - ].map((tag) => ( + {([ + { label: 'Walk more', icon: 'walk', bg: '#eef2ff', fg: '#4c5fd5' }, + { label: 'Explore more', icon: 'compass', bg: '#fdeef7', fg: '#d54ca3' }, + { label: 'Meet friends', icon: 'users', bg: '#eef9f1', fg: '#3f9d5c' }, + { label: 'Support local', icon: 'store', bg: '#fef1e8', fg: '#d5824c' }, + { label: 'Discover new brands', icon: 'tag', bg: '#fef7ea', fg: '#c49a2f' }, + { label: 'Celebrate every day', icon: 'sparkles', bg: '#fdeeee', fg: '#d5514c' }, + ] as { label: string; icon: FpIconName; bg: string; fg: string }[]).map((tag) => (
- {tag.icon} + {tag.label} diff --git a/src/components/for-people/LytsUniverseSection.tsx b/src/components/for-people/LytsUniverseSection.tsx index 9b1653f..18bfd1d 100644 --- a/src/components/for-people/LytsUniverseSection.tsx +++ b/src/components/for-people/LytsUniverseSection.tsx @@ -4,17 +4,18 @@ import React, { useEffect, useLayoutEffect, useRef } from 'react'; import { gsap } from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; import { PhoneMockupImage } from './PhoneMockup'; +import FpIcon, { type FpIconName } from './FpIcons'; if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; -const CHALLENGES = [ - { label: 'Walk More Challenge', icon: '🚶' }, - { label: 'Selfie Quest', icon: '🤳' }, - { label: 'Weekend Explorer', icon: '🗺️' }, - { label: 'Coffee Week', icon: '☕' }, - { label: 'Sneaker Hunt', icon: '👟' }, +const CHALLENGES: { label: string; icon: FpIconName }[] = [ + { label: 'Walk More Challenge', icon: 'walk' }, + { label: 'Selfie Quest', icon: 'camera' }, + { label: 'Weekend Explorer', icon: 'map' }, + { label: 'Coffee Week', icon: 'coffee' }, + { label: 'Sneaker Hunt', icon: 'sneaker' }, ]; export default function LytsUniverseSection() { @@ -61,7 +62,7 @@ export default function LytsUniverseSection() {
{CHALLENGES.map((tag) => (
- {tag.icon} + {tag.label} diff --git a/src/components/for-people/PhoneMockup.tsx b/src/components/for-people/PhoneMockup.tsx index 479c472..7cf4d8d 100644 --- a/src/components/for-people/PhoneMockup.tsx +++ b/src/components/for-people/PhoneMockup.tsx @@ -161,7 +161,9 @@ export function PhoneMockupImage({ .pm-phone-shell { animation: pmPhoneFloat 7s ease-in-out infinite; } @media (prefers-reduced-motion: reduce) { .pm-phone-shell { animation: none !important; } } @media (max-width: 900px) { - .pm-phone-shell { width: ${MOBILE_WIDGET_WIDTH}px !important; } + /* min(...,100%) — a hard 280px is wider than the ~256px available at + 320px, so the widget overflowed its column instead of centring. */ + .pm-phone-shell { width: min(${MOBILE_WIDGET_WIDTH}px, 100%) !important; } } `}
@@ -188,7 +190,7 @@ export function PhoneFrame({ children, floatClassName = 'pm-phone-float', width 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)', + boxShadow: '0 28px 60px -22px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.06)', position: 'relative', }} > @@ -215,7 +217,9 @@ export function PhoneFrame({ children, floatClassName = 'pm-phone-float', width .pm-phone-shell { animation: pmPhoneFloat 7s ease-in-out infinite; } @media (prefers-reduced-motion: reduce) { .pm-phone-shell { animation: none !important; } } @media (max-width: 900px) { - .pm-phone-shell { width: ${MOBILE_WIDGET_WIDTH}px !important; } + /* min(...,100%) — a hard 280px is wider than the ~256px available at + 320px, so the widget overflowed its column instead of centring. */ + .pm-phone-shell { width: min(${MOBILE_WIDGET_WIDTH}px, 100%) !important; } } `}
diff --git a/src/components/for-people/SpendAnywhereSection.tsx b/src/components/for-people/SpendAnywhereSection.tsx index 1a60719..5c5cfe5 100644 --- a/src/components/for-people/SpendAnywhereSection.tsx +++ b/src/components/for-people/SpendAnywhereSection.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useLayoutEffect, useRef } from 'react'; import { gsap } from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; +import FpIcon, { type FpIconName } from './FpIcons'; if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); @@ -51,13 +52,13 @@ export default function SpendAnywhereSection() {

- {[ - { label: 'No waiting', icon: '⚡' }, - { label: 'No cashback', icon: '🚫' }, - { label: 'No complicated redemption', icon: '✅' }, - ].map((tag) => ( + {([ + { label: 'No waiting', icon: 'bolt' }, + { label: 'No cashback', icon: 'ban' }, + { label: 'No complicated redemption', icon: 'check' }, + ] as { label: string; icon: FpIconName }[]).map((tag) => (
- {tag.icon} + {tag.label} @@ -83,7 +84,7 @@ export default function SpendAnywhereSection() { style={{ width: 300, 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)', + boxShadow: '0 28px 60px -22px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.06)', position: 'relative', }} > @@ -142,7 +143,9 @@ export default function SpendAnywhereSection() { } @media (prefers-reduced-motion: reduce) { .spa-phone-shell { animation: none !important; } } @media (max-width: 900px) { - .spa-phone-shell { width: 280px !important; --pm-scale: calc(260 / 390); } + /* min(...,100%): see EarnRewardsSection — a hard 280px overflows + the ~256px available at 320px instead of centring. */ + .spa-phone-shell { width: min(280px, 100%) !important; --pm-scale: calc(260 / 390); } } `}
diff --git a/src/components/for-people/VerticalScrollWrapper.tsx b/src/components/for-people/VerticalScrollWrapper.tsx index e3b29bd..3b53336 100644 --- a/src/components/for-people/VerticalScrollWrapper.tsx +++ b/src/components/for-people/VerticalScrollWrapper.tsx @@ -332,7 +332,11 @@ export default function VerticalScrollWrapper({ children, sections = [] }: Props } .fp-card--light { background: #fff; border: 1.5px solid var(--carbon); } .fp-card--light:hover { box-shadow: 3px 3px 0 var(--carbon); transform: translate(-2px, -2px); } - .fp-card--dark { background: rgba(255,255,255,0.04); border: 1.5px solid rgba(255,255,255,0.18); } + /* Opaque, not a translucent wash — these cards can sit over photography + (Discover), where a see-through surface wrecks legibility. #12121a is + what the old 4% white overlay composited to on the dark panels, so the + sections that aren't over art keep their existing weight. */ + .fp-card--dark { background: #12121a; border: 1.5px solid rgba(255,255,255,0.18); } .fp-card--dark:hover { border-color: var(--gold); box-shadow: 3px 3px 0 rgba(244,190,40,0.35); transform: translate(-2px, -2px); } .fp-card--fill { background: var(--carbon); border: 1.5px solid var(--carbon); } .fp-card--fill:hover { box-shadow: 3px 3px 0 rgba(244,190,40,0.4); transform: translate(-2px, -2px); } @@ -354,13 +358,20 @@ export default function VerticalScrollWrapper({ children, sections = [] }: Props } /* Force single column even when a section sets grid-template-columns inline */ .fp-split { grid-template-columns: 1fr !important; gap: 48px !important; } + /* A 1fr track still floors at its content's min-content width, so a fixed + widget would stretch the column past the viewport. min-width:0 lets it + shrink to the real available width so centring resolves correctly. */ + .fp-split > * { min-width: 0 !important; } } @media (max-width: 600px) { .fp-shell { padding: 0 22px; } - /* Full-bleed panels on phones (no left/right margin), keep bottom gap */ - .fp-panel { width: 100%; margin: 0 0 12px; } - /* Hero flush to the top on phones */ - .fp-root { padding-top: 0; } + /* Panels keep a 10px left/right margin on phones, plus the bottom gap */ + .fp-panel { + width: calc(100% - (var(--sec-gutter) * 2)); + margin: 0 var(--sec-gutter) 12px; + } + /* Hero sits 10px from the top on phones */ + .fp-root { padding-top: var(--hero-top); } } @media (prefers-reduced-motion: reduce) { diff --git a/src/components/home/HeroCarousel.tsx b/src/components/home/HeroCarousel.tsx index eee080b..b08e117 100644 --- a/src/components/home/HeroCarousel.tsx +++ b/src/components/home/HeroCarousel.tsx @@ -178,13 +178,13 @@ export default function HeroCarousel({ onReady }: HeroCarouselProps) { } } - /* Phones — full-bleed hero, flush to top (top margin 0), keep bottom gap */ + /* Phones — 10px side and top margins, keep bottom gap */ @media (max-width: 600px) { .hero-carousel { - width: 100%; - height: calc(100vh - 12px); - height: calc(100svh - 12px); - margin: 0 0 12px; + width: calc(100% - (var(--sec-gutter) * 2)); + height: calc(100vh - var(--hero-top) - 12px); + height: calc(100svh - var(--hero-top) - 12px); + margin: var(--hero-top) var(--sec-gutter) 12px; } } diff --git a/src/components/home/ProductsSection.tsx b/src/components/home/ProductsSection.tsx index 3b4a743..d0f4464 100644 --- a/src/components/home/ProductsSection.tsx +++ b/src/components/home/ProductsSection.tsx @@ -640,7 +640,7 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro reserved space when the pinned element carries its own margin, which shows up as a jump/shake right as the pin engages. */ .products-section-wrapper { - margin: 20px; + margin: 20px var(--sec-gutter); } .products-section-root { --pt: clamp(230px, 28vh, 310px); diff --git a/src/components/home/ShiftSection.tsx b/src/components/home/ShiftSection.tsx index 17764e7..c39bf39 100644 --- a/src/components/home/ShiftSection.tsx +++ b/src/components/home/ShiftSection.tsx @@ -383,6 +383,14 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) { #shift .shift-orb { animation: none; } } + /* Phones — plain white background instead of the cream gradient. + !important is needed to override the inline background style. */ + @media (max-width: 600px) { + #shift.shift-section { + background: #ffffff !important; + } + } + /* Bento grid styles */ .shift-bento-grid { display: grid;