Update about us, for business pages, and for-people components

This commit is contained in:
R-Bharathraj
2026-07-16 17:32:43 +05:30
parent b627126713
commit 88ca87e3cc
20 changed files with 572 additions and 168 deletions

View File

@@ -0,0 +1,46 @@
<!doctype html>
<html>
<head><meta charset="utf-8"><title>icon check</title>
<style>
body { margin:0; padding:24px; background:#fff; font-family: system-ui, sans-serif; }
.grid { display:grid; grid-template-columns: repeat(6, 1fr); gap:18px; }
.cell { display:flex; flex-direction:column; align-items:center; gap:8px; }
.badge { display:flex; align-items:center; justify-content:center; width:76px; height:76px; border-radius:20px; background:#fff8e8; color:#bc7122; }
.name { font-size:12px; color:#444; }
svg { display:block; }
</style>
</head>
<body>
<div class="grid" id="g"></div>
<script>
const P = {
walk: '<circle cx="13.2" cy="4.2" r="1.9"/><path d="M10.3 21.5l1.9-5.4-2.4-2.2V9.6l3.4-1.7 2.1 3.1 2.9 1.3"/><path d="M8.4 12.4l1.4-2.5"/><path d="M14.7 15.9l1.9 5.6"/>',
compass: '<circle cx="12" cy="12" r="9"/><path d="M15.6 8.4l-2.1 5.1-5.1 2.1 2.1-5.1z"/>',
users: '<path d="M16 20.5v-1.9a3.6 3.6 0 00-3.6-3.6H6.6A3.6 3.6 0 003 18.6v1.9"/><circle cx="9.5" cy="7.4" r="3.4"/><path d="M21 20.5v-1.9a3.6 3.6 0 00-2.7-3.5"/><path d="M15.6 4.2a3.6 3.6 0 010 6.9"/>',
store: '<path d="M4 10.4V20a1 1 0 001 1h14a1 1 0 001-1v-9.6"/><path d="M2.6 9.6l1.6-5.1a1 1 0 01.95-.7h13.7a1 1 0 01.95.7l1.6 5.1"/><path d="M2.6 9.6a2.35 2.35 0 004.7 0 2.35 2.35 0 004.7 0 2.35 2.35 0 004.7 0 2.35 2.35 0 004.7 0"/><path d="M9.7 21v-5.3h4.6V21"/>',
tag: '<path d="M20.6 13.4l-7.2 7.2a1.6 1.6 0 01-2.3 0l-7.2-7.2a1.6 1.6 0 01-.47-1.13V4.6A1.6 1.6 0 015 3h7.67c.42 0 .83.17 1.13.47l6.8 6.8a1.6 1.6 0 010 2.26z"/><circle cx="8.1" cy="8.1" r="1.5"/>',
sparkles: '<path d="M11.4 3.2l1.85 4.45 4.45 1.85-4.45 1.85-1.85 4.45-1.85-4.45L5.1 9.5l4.45-1.85z"/><path d="M18.4 14.9l.85 2.05 2.05.85-2.05.85-.85 2.05-.85-2.05-2.05-.85 2.05-.85z"/>',
bolt: '<path d="M13.4 2.5L4.6 13.9h6.1L10.6 21.5l8.8-11.4h-6.1z"/>',
gamepad: '<path d="M7.6 8.6h8.8a5 5 0 014.92 4.11l.72 3.95a2.55 2.55 0 01-4.63 1.87l-1.13-1.62a2 2 0 00-1.64-.86H9.36a2 2 0 00-1.64.86l-1.13 1.62a2.55 2.55 0 01-4.63-1.87l.72-3.95A5 5 0 017.6 8.6z"/><path d="M7.9 11.7v3.1M6.35 13.25h3.1"/><path d="M15.7 12.6h.01M17.9 14.8h.01"/>',
bag: '<path d="M5.4 7.6h13.2l.95 12.4a1 1 0 01-1 1.08H5.45a1 1 0 01-1-1.08z"/><path d="M8.6 10.2V6.6a3.4 3.4 0 016.8 0v3.6"/>',
gift: '<path d="M19.6 12.2v8.3a.5.5 0 01-.5.5H4.9a.5.5 0 01-.5-.5v-8.3"/><rect x="2.8" y="7.6" width="18.4" height="4.6" rx="1"/><path d="M12 7.6v13.4"/><path d="M12 7.6H7.9a2.35 2.35 0 110-4.7C11 2.9 12 7.6 12 7.6z"/><path d="M12 7.6h4.1a2.35 2.35 0 100-4.7C13 2.9 12 7.6 12 7.6z"/>',
star: '<path d="M12 2.9l2.9 5.85 6.45.95-4.67 4.55 1.1 6.42L12 17.64l-5.78 3.03 1.1-6.42L2.65 9.7l6.45-.95z"/>',
pin: '<path d="M20 10.4c0 5.5-8 12.1-8 12.1s-8-6.6-8-12.1a8 8 0 1116 0z"/><circle cx="12" cy="10.2" r="2.8"/>',
camera: '<path d="M3 8.4h3.3l1.4-2.3a1 1 0 01.86-.5h6.88a1 1 0 01.86.5l1.4 2.3H21a1 1 0 011 1v9.1a1 1 0 01-1 1H3a1 1 0 01-1-1V9.4a1 1 0 011-1z"/><circle cx="12" cy="13.9" r="3.5"/>',
wheel: '<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="2.3"/><path d="M12 3v6.7M12 14.3V21M3 12h6.7M14.3 12H21"/><path d="M5.64 5.64l4.74 4.74M13.62 13.62l4.74 4.74M18.36 5.64l-4.74 4.74M10.38 13.62l-4.74 4.74"/>',
ticket: '<path d="M3 9.1V6.6a1 1 0 011-1h16a1 1 0 011 1v2.5a2.6 2.6 0 000 5.2v2.5a1 1 0 01-1 1H4a1 1 0 01-1-1v-2.5a2.6 2.6 0 000-5.2z"/><path d="M9.6 5.9v12.4" stroke-dasharray="2 2.4"/>',
trophy: '<path d="M7.1 4.4h9.8v5.2a4.9 4.9 0 01-9.8 0z"/><path d="M7.1 6.1H4.9a2.3 2.3 0 000 4.6h2.5"/><path d="M16.9 6.1h2.2a2.3 2.3 0 010 4.6h-2.5"/><path d="M12 14.5v3.2"/><path d="M8.2 21h7.6"/><path d="M9.6 17.7h4.8a1.4 1.4 0 011.4 1.4V21H8.2v-1.9a1.4 1.4 0 011.4-1.4z"/>',
map: '<path d="M9.4 4.4L3.4 6.8v12.7l6-2.4 5.2 2.4 6-2.4V4.4l-6 2.4z"/><path d="M9.4 4.4v12.7M14.6 6.8v12.7"/>',
coffee: '<path d="M17.1 8.6h1.8a3 3 0 010 6h-1.8"/><path d="M3.1 8.6h14v5.5a5 5 0 01-5 5H8.1a5 5 0 01-5-5z"/><path d="M6.6 2.6v2.9M10.1 2.6v2.9M13.6 2.6v2.9"/>',
sneaker: '<path d="M2.6 18.4v-5.6a.8.8 0 01.8-.8h3.2l3.5-2.7 1.7 1.9 4 2.4 5.5 1.85a2 2 0 011.4 1.9v1.04a.9.9 0 01-.9.9H3.5a.9.9 0 01-.9-.9z"/><path d="M10.1 12.5l-1.7 1.6M12.5 13.8l-1.7 1.6M15.1 15.1l-1.7 1.6"/>',
ban: '<circle cx="12" cy="12" r="9"/><path d="M5.64 5.64l12.72 12.72"/>',
check: '<circle cx="12" cy="12" r="9"/><path d="M7.9 12.2l2.8 2.8 5.4-5.4"/>',
flame: '<path d="M12 22c3.87 0 7-2.98 7-6.66 0-4.55-4.35-6.86-5.56-11.14-2 1.4-3.1 3.4-3.1 5.4 0 1.6-1.3 2.2-2.2 1.2a2.9 2.9 0 01-.68-1.78C6.02 10.2 5 12.6 5 15.34 5 19.02 8.13 22 12 22z"/>',
rocket: '<path d="M4.6 16.6c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 00-2.91-.09z"/><path d="M12 15l-3-3a22 22 0 012-3.95A12.88 12.88 0 0122 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 01-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>',
};
document.getElementById('g').innerHTML = Object.entries(P).map(([k,v]) =>
`<div class="cell"><span class="badge"><svg viewBox="0 0 24 24" width="44" height="44" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">${v}</svg></span><span class="name">${k}</span></div>`
).join('');
</script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@@ -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 */}
<Image src="/assets/images/about_bg.png" alt="" fill priority style={{ objectFit: 'cover', opacity: 0.9 }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(180deg, rgba(26,21,48,0.1) 0%, rgba(26,21,48,0.55) 85%)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse 60% 65% at 50% 48%, rgba(18,14,30,0.78) 0%, rgba(18,14,30,0.35) 60%, transparent 100%)' }} />
{/* 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. */}
<Image src="/assets/images/aboutus_bg.png" alt="" fill priority style={{ objectFit: 'cover', objectPosition: 'center 42%', opacity: 0.95 }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(180deg, rgba(26,21,48,0.2) 0%, rgba(26,21,48,0.32) 45%, rgba(20,16,36,0.72) 100%)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse 62% 60% at 50% 50%, rgba(18,14,30,0.28) 0%, rgba(18,14,30,0.1) 62%, transparent 100%)' }} />
{/* Accent blobs */}
<div style={{ position: 'absolute', top: '-12%', right: '-8%', width: 520, height: 520, borderRadius: '50%', background: 'radial-gradient(circle, rgba(244,190,40,0.22) 0%, transparent 70%)', filter: 'blur(10px)', pointerEvents: 'none' }} />
<div style={{ position: 'absolute', bottom: '-15%', left: '-10%', width: 460, height: 460, borderRadius: '50%', background: 'radial-gradient(circle, rgba(167,139,250,0.18) 0%, transparent 70%)', pointerEvents: 'none' }} />
@@ -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<br />
<span style={{ color: '#f4be28' }}>Behind</span> Loyaly.
</h1>
<p data-gsap="text" style={{ fontSize: 'clamp(16px, 1.5vw, 20px)', lineHeight: 1.75, color: 'rgba(255,255,255,0.55)', fontFamily: 'Sora, sans-serif', maxWidth: 560, margin: '0 auto' }}>
<p data-gsap="text" style={{ fontSize: 'clamp(16px, 1.5vw, 20px)', lineHeight: 1.75, color: 'rgba(255,255,255,0.78)', fontFamily: 'Sora, sans-serif', maxWidth: 560, margin: '0 auto', textShadow: '0 1px 16px rgba(10,7,20,0.8)' }}>
We're not just building a product. We're redefining retail, one store at a time.
</p>
</div>
@@ -77,7 +83,7 @@ export default function AboutUsPage() {
</section>
{/* ── MISSION & VISION ── */}
<section style={{ position: 'relative', overflow: 'hidden', padding: 'clamp(60px, 9vh, 104px) 8%', background: '#07070d', margin: '0 var(--sec-gutter) var(--sec-gap)', borderRadius: 28 }}>
<section className="au-mv-section" style={{ position: 'relative', overflow: 'hidden', padding: 'clamp(60px, 9vh, 104px) 8%', background: '#07070d', margin: '0 var(--sec-gutter) var(--sec-gap)', borderRadius: 28 }}>
{/* Ambient top glow */}
<div aria-hidden style={{ position: 'absolute', top: '-24%', left: '50%', transform: 'translateX(-50%)', width: 680, height: 420, background: 'radial-gradient(ellipse, rgba(244,190,40,0.12) 0%, transparent 70%)', pointerEvents: 'none' }} />
@@ -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; }
}
`}</style>
</main>
);
@@ -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}</div>
{/* Description */}
<p style={{
fontSize: 12.5,
color: 'rgba(255, 255, 255, 0.55)',
lineHeight: 1.55,
margin: 0,
{/* Description — 0fr→1fr expands to the text's real height,
so nothing clips and no magic max-height is needed. */}
<div style={{
display: 'grid',
gridTemplateRows: isActive ? '1fr' : '0fr',
opacity: isActive ? 1 : 0,
maxHeight: isActive ? 100 : 0,
overflow: 'hidden',
transition: 'opacity 0.5s ease 0.15s, maxHeight 0.5s ease 0.1s'
}}>{member.description}</p>
transition: `grid-template-rows ${DUR}s ${EASE}, opacity 0.45s ease ${isActive ? '0.15s' : '0s'}`,
}}>
<div style={{ overflow: 'hidden', minHeight: 0 }}>
<p style={{
fontSize: 12.5,
color: 'rgba(255, 255, 255, 0.55)',
lineHeight: 1.55,
margin: 0,
}}>{member.description}</p>
</div>
</div>
{/* Social icons */}
<div style={{
display: 'flex',
gap: 8,
marginTop: 16,
display: 'grid',
gridTemplateRows: isActive ? '1fr' : '0fr',
opacity: isActive ? 1 : 0,
maxHeight: isActive ? 50 : 0,
overflow: 'hidden',
transition: 'opacity 0.5s ease 0.25s, maxHeight 0.5s ease 0.2s'
transition: `grid-template-rows ${DUR}s ${EASE}, opacity 0.45s ease ${isActive ? '0.25s' : '0s'}`,
}}>
{/* 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. */}
<div style={{ overflow: 'hidden', minHeight: 0 }}>
<div style={{
display: 'flex',
gap: 8,
paddingTop: 16,
}}>
{member.socials.linkedin && (
<a
href={member.socials.linkedin}
@@ -837,6 +917,8 @@ function TeamCarousel() {
</svg>
</a>
)}
</div>
</div>
</div>
</div>
</article>
@@ -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; }
}
`}</style>
</section>

View File

@@ -150,9 +150,12 @@ export default function ForBusinessPage() {
backgroundSize: '72px 72px',
}} />
<div style={{ width: '100%', position: 'relative', zIndex: 2, display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}>
<div style={{ maxWidth: 760 }}>
<h1 data-gsap="heading" style={{
<div style={{ width: '100%', minWidth: 0, position: 'relative', zIndex: 2, display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}>
{/* 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. */}
<div style={{ maxWidth: 'min(760px, 100%)', minWidth: 0 }}>
<h1 data-gsap="heading" className="uc-hero-h1" style={{
fontSize: 'clamp(52px, 8vw, 120px)', fontWeight: 800,
letterSpacing: '-0.05em', lineHeight: 0.9,
color: '#ffffff', margin: '0 0 32px', fontFamily: 'Sora, sans-serif',
@@ -409,21 +412,33 @@ export default function ForBusinessPage() {
</section>
{/* ── PROXIMITY MARKETING ── */}
<section style={{ padding: '120px 8%', background: '#ffffff', margin: '0 var(--sec-gutter) var(--sec-gap)', borderRadius: 24, overflow: 'hidden', position: 'relative' }}>
<div style={{ position: 'absolute', inset: 0, backgroundImage: 'linear-gradient(rgba(23,21,18,0.035) 1px,transparent 1px),linear-gradient(90deg,rgba(23,21,18,0.035) 1px,transparent 1px)', backgroundSize: '56px 56px', pointerEvents: 'none' }} />
{/* 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. */}
<section className="uc-prox-panel" style={{
padding: '120px 8%',
margin: '0 var(--sec-gutter) var(--sec-gap)', borderRadius: 24, overflow: 'hidden', position: 'relative',
}}>
<div style={{ position: 'absolute', inset: 0, backgroundImage: 'linear-gradient(rgba(244,190,40,0.025) 1px,transparent 1px),linear-gradient(90deg,rgba(244,190,40,0.025) 1px,transparent 1px)', backgroundSize: '64px 64px', pointerEvents: 'none' }} />
<RevealBlock>
<div className="uc-proximity-grid" style={{ display: 'grid', gridTemplateColumns: '0.9fr 1.1fr', gap: '5vw', alignItems: 'center', position: 'relative' }}>
{/* 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. */}
<div className="uc-map-panel" style={{
position: 'relative', borderRadius: 28, width: '100%', aspectRatio: '4 / 3', overflow: 'hidden',
background: 'linear-gradient(165deg, #fffdf7 0%, #faf3e0 100%)',
border: '1px solid rgba(244,190,40,0.28)',
boxShadow: '0 34px 80px -34px rgba(201,150,15,0.4), inset 0 1px 0 rgba(255,255,255,0.7)',
background: 'linear-gradient(165deg, rgba(28,14,10,0.62) 0%, rgba(18,10,28,0.72) 100%)',
backdropFilter: 'blur(24px) saturate(1.25)',
WebkitBackdropFilter: 'blur(24px) saturate(1.25)',
border: '1px solid rgba(244,190,40,0.22)',
boxShadow: '0 22px 55px -26px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.14), inset 0 -1px 0 rgba(244,190,40,0.06)',
}}>
{/* Gold bloom pooling under the store, and a vignette to sink the edges */}
<div aria-hidden style={{ position: 'absolute', inset: 0, pointerEvents: 'none', background: 'radial-gradient(circle at 50% 50%, rgba(244,190,40,0.16) 0%, transparent 52%), radial-gradient(120% 120% at 50% 50%, transparent 55%, rgba(10,5,16,0.5) 100%)' }} />
<svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid meet" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}>
{/* Building blocks */}
<g fill="#f6edd4">
{/* Building blocks — faint glass slabs with a gold edge */}
<g fill="rgba(255,255,255,0.035)" stroke="rgba(244,190,40,0.16)" strokeWidth="0.8">
<rect x="18" y="18" width="66" height="44" rx="6" />
<rect x="316" y="22" width="62" height="60" rx="6" />
<rect x="22" y="196" width="74" height="86" rx="6" />
@@ -431,8 +446,8 @@ export default function ForBusinessPage() {
<rect x="150" y="18" width="40" height="40" rx="5" />
<rect x="236" y="238" width="54" height="46" rx="6" />
</g>
{/* Main roads */}
<g stroke="#efe3c6" strokeLinecap="round">
{/* Main roads — gold-tinted, not grey */}
<g stroke="rgba(244,190,40,0.10)" strokeLinecap="round">
<line x1="0" y1="80" x2="400" y2="80" strokeWidth="8" />
<line x1="0" y1="150" x2="400" y2="150" strokeWidth="9" />
<line x1="0" y1="222" x2="400" y2="222" strokeWidth="8" />
@@ -441,7 +456,7 @@ export default function ForBusinessPage() {
<line x1="300" y1="0" x2="300" y2="300" strokeWidth="8" />
</g>
{/* Secondary streets */}
<g stroke="#ece0c0" strokeWidth="1.4">
<g stroke="rgba(244,190,40,0.07)" strokeWidth="1.2">
<line x1="0" y1="40" x2="400" y2="40" />
<line x1="0" y1="118" x2="400" y2="118" />
<line x1="0" y1="190" x2="400" y2="190" />
@@ -451,39 +466,39 @@ export default function ForBusinessPage() {
<line x1="350" y1="0" x2="350" y2="300" />
</g>
{/* Proximity radar rings from the store */}
<g fill="none" stroke="#f4be28">
<circle className="uc-map-ring" cx="200" cy="150" r="18" strokeWidth="1.6" />
<circle className="uc-map-ring" cx="200" cy="150" r="18" strokeWidth="1.6" style={{ animationDelay: '1.1s' }} />
<circle className="uc-map-ring" cx="200" cy="150" r="18" strokeWidth="1.6" style={{ animationDelay: '2.2s' }} />
<g fill="none" stroke="#f4be28" opacity="0.85">
<circle className="uc-map-ring" cx="200" cy="150" r="18" strokeWidth="1.1" />
<circle className="uc-map-ring" cx="200" cy="150" r="18" strokeWidth="1.1" style={{ animationDelay: '1.1s' }} />
<circle className="uc-map-ring" cx="200" cy="150" r="18" strokeWidth="1.1" style={{ animationDelay: '2.2s' }} />
</g>
{/* Routes to nearby shoppers */}
<g stroke="#d9a72a" strokeWidth="1.8" fill="none" strokeLinecap="round">
<g stroke="#f4be28" strokeWidth="1.8" fill="none" strokeLinecap="round" opacity="0.7">
{[[120, 80], [300, 105], [150, 240], [312, 220]].map(([x, y], i) => (
<line key={i} className="uc-map-link" x1="200" y1="150" x2={x} y2={y} style={{ animationDelay: `${i * 0.3}s` }} />
))}
</g>
{/* Store glow */}
<circle cx="200" cy="150" r="42" fill="#f4be28" opacity="0.16" />
{/* Shopper pins */}
{/* Store glow — kept low; the panel bloom and pin already light this area */}
<circle cx="200" cy="150" r="42" fill="#f4be28" opacity="0.1" />
{/* 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) => (
<g key={i}>
<circle className="uc-map-shopper" cx={x} cy={y} r="6" fill="#f4be28" style={{ animationDelay: `${i * 0.5}s` }} />
<circle cx={x} cy={y} r="5" fill="#171512" stroke="#fffdf7" strokeWidth="2" />
<circle cx={x} cy={y} r="5" fill="#f4be28" stroke="#1d1024" strokeWidth="2" />
</g>
))}
</svg>
{/* Store pin (centered, matches SVG store at 200,150) */}
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)', width: 76, height: 76 }}>
<span className="uc-pin-halo" aria-hidden style={{ position: 'absolute', inset: '-24%', borderRadius: '50%', background: 'radial-gradient(circle, rgba(244,190,40,0.5) 0%, transparent 70%)' }} />
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)', width: 64, height: 64 }}>
<span className="uc-pin-halo" aria-hidden style={{ position: 'absolute', inset: '-38%', borderRadius: '50%', background: 'radial-gradient(circle, rgba(244,190,40,0.45) 0%, transparent 68%)' }} />
<div style={{
position: 'absolute', inset: 0, borderRadius: '50%',
background: 'radial-gradient(circle at 35% 30%, #ffd75e 0%, #f4be28 45%, #c9960f 100%)',
background: 'radial-gradient(circle at 34% 28%, #ffe27a 0%, #f4be28 46%, #c9820f 100%)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: '0 14px 34px -8px rgba(201,150,15,0.65), inset 0 2px 4px rgba(255,255,255,0.55)',
border: '3px solid #fffdf7',
/* Concentric gold rings instead of a heavy white outline */
boxShadow: '0 0 0 1.5px rgba(255,255,255,0.5), 0 0 0 8px rgba(244,190,40,0.12), 0 0 0 17px rgba(244,190,40,0.06), 0 18px 50px -8px rgba(244,190,40,0.7), inset 0 2px 5px rgba(255,255,255,0.6)',
}}>
<svg viewBox="0 0 24 24" fill="#171512" style={{ width: 30, height: 30 }}>
<svg viewBox="0 0 24 24" fill="#2b1509" style={{ width: 26, height: 26 }}>
<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-7zm0 9.5A2.5 2.5 0 1112 6.5a2.5 2.5 0 010 5z" />
</svg>
</div>
@@ -492,26 +507,28 @@ export default function ForBusinessPage() {
{/* Live counter chip */}
<div style={{
position: 'absolute', left: 16, bottom: 16, display: 'inline-flex', alignItems: 'center', gap: 8,
padding: '8px 14px', borderRadius: 100, background: 'rgba(255,255,255,0.92)',
border: '1px solid rgba(244,190,40,0.35)', boxShadow: '0 8px 22px -12px rgba(23,21,18,0.3)', backdropFilter: 'blur(6px)',
padding: '9px 15px', borderRadius: 100, background: 'rgba(255,255,255,0.07)',
border: '1px solid rgba(255,255,255,0.16)',
boxShadow: '0 6px 16px -8px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.16)',
backdropFilter: 'blur(14px) saturate(1.2)', WebkitBackdropFilter: 'blur(14px) saturate(1.2)',
}}>
<span className="uc-map-livedot" style={{ width: 7, height: 7, borderRadius: '50%', background: '#16a34a', display: 'block' }} />
<span style={{ fontSize: 12, fontWeight: 800, color: '#171512', fontFamily: 'Sora, sans-serif' }}>18 shoppers nearby</span>
<span className="uc-map-livedot" style={{ width: 7, height: 7, borderRadius: '50%', background: '#22c55e', display: 'block' }} />
<span style={{ fontSize: 12, fontWeight: 800, color: '#fff', fontFamily: 'Sora, sans-serif' }}>18 shoppers nearby</span>
</div>
</div>
{/* Content */}
<div>
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 9, marginBottom: 22, padding: '6px 15px 6px 6px', borderRadius: 100, background: 'rgba(244,190,40,0.1)', border: '1px solid rgba(244,190,40,0.3)' }}>
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 9, marginBottom: 22, padding: '6px 15px 6px 6px', borderRadius: 100, background: 'rgba(244,190,40,0.12)', border: '1px solid rgba(244,190,40,0.35)', boxShadow: '0 0 24px -8px rgba(244,190,40,0.5)' }}>
<span style={{ width: 22, height: 22, borderRadius: '50%', background: '#f4be28', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#171512" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M18 8a6 6 0 10-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>
</span>
<span style={{ fontSize: 11, fontWeight: 800, letterSpacing: '0.16em', textTransform: 'uppercase', color: '#8a5a14', fontFamily: 'Sora, sans-serif' }}>Capability Spotlight</span>
<span style={{ fontSize: 11, fontWeight: 800, letterSpacing: '0.16em', textTransform: 'uppercase', color: '#f4be28', fontFamily: 'Sora, sans-serif' }}>Capability Spotlight</span>
</div>
<h2 style={{ fontSize: 'clamp(28px, 3.6vw, 48px)', fontWeight: 800, letterSpacing: '-0.03em', lineHeight: 1.08, color: '#0a0a0f', margin: '0 0 20px', fontFamily: 'Sora, sans-serif' }}>
Reach customers the moment they&apos;re nearby.
<h2 style={{ fontSize: 'clamp(32px, 4.2vw, 56px)', fontWeight: 900, letterSpacing: '-0.045em', lineHeight: 0.98, color: '#fff', margin: '0 0 22px', fontFamily: 'Sora, sans-serif' }}>
Reach customers the moment they&apos;re <span style={{ color: '#f4be28' }}>nearby.</span>
</h2>
<p style={{ fontSize: 16, lineHeight: 1.8, color: 'rgba(0,0,0,0.55)', fontFamily: 'Sora, sans-serif', margin: '0 0 32px', maxWidth: 480 }}>
<p style={{ fontSize: 16, lineHeight: 1.8, color: 'rgba(255,255,255,0.72)', fontFamily: 'Sora, sans-serif', margin: '0 0 32px', maxWidth: 480 }}>
Loyaly&apos;s proximity engine sends instant notifications to shoppers browsing within your store&apos;s radius. No ad spend. No guesswork. Just timely, relevant offers delivered at exactly the right moment.
</p>
<div className="uc-prox-stats" style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 12, maxWidth: 500 }}>
@@ -521,18 +538,21 @@ export default function ForBusinessPage() {
{ v: 'Real-time', l: 'Radius targeting', icon: <><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.5" /></> },
].map((s) => (
<div key={s.l} className="uc-prox-stat" style={{
padding: '18px', borderRadius: 16, background: '#ffffff',
border: '1px solid rgba(23,21,18,0.08)', boxShadow: '0 10px 30px -18px rgba(23,21,18,0.3)',
padding: '20px 18px', borderRadius: 18,
background: 'linear-gradient(180deg, rgba(255,255,255,0.06) 0%, rgba(255,255,255,0.015) 100%)',
border: '1px solid rgba(255,255,255,0.1)',
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.08)',
}}>
<div style={{
width: 34, height: 34, borderRadius: 10, marginBottom: 12,
background: 'rgba(244,190,40,0.14)', border: '1px solid rgba(244,190,40,0.32)',
display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#c9960f',
width: 36, height: 36, borderRadius: 11, marginBottom: 14,
background: 'rgba(244,190,40,0.14)', border: '1px solid rgba(244,190,40,0.4)',
display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#f4be28',
boxShadow: '0 0 20px -6px rgba(244,190,40,0.6)',
}}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">{s.icon}</svg>
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">{s.icon}</svg>
</div>
<div style={{ fontSize: 18, fontWeight: 900, color: '#171512', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.02em' }}>{s.v}</div>
<div style={{ fontSize: 11.5, color: 'rgba(0,0,0,0.5)', fontFamily: 'Sora, sans-serif', marginTop: 3 }}>{s.l}</div>
<div style={{ fontSize: 20, fontWeight: 900, color: '#fff', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em' }}>{s.v}</div>
<div style={{ fontSize: 11.5, color: 'rgba(255,255,255,0.66)', fontFamily: 'Sora, sans-serif', marginTop: 4 }}>{s.l}</div>
</div>
))}
</div>
@@ -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; }
}
`}</style>
</main>

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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() {
<div ref={tagsRef} style={{ display: 'flex', flexWrap: 'wrap', gap: 10, maxWidth: 460 }}>
{TAGS.map((tag) => (
<FpTagCard key={tag.label} theme="dark" icon={tag.icon}>{tag.label}</FpTagCard>
<FpTagCard key={tag.label} theme="dark" icon={<FpIcon name={tag.icon} />}>{tag.label}</FpTagCard>
))}
</div>
</div>

View File

@@ -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); }
}
`}</style>
</div>
@@ -163,7 +167,7 @@ export default function EarnRewardsSection() {
<div ref={tagsRef} style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginBottom: 20, maxWidth: 460 }}>
{earnTags.map((tag) => (
<FpTagCard key={tag.label} theme="light" icon={tag.icon} iconBg={tag.bg}>
<FpTagCard key={tag.label} theme="light" icon={<FpIcon name={tag.icon} />} iconBg={tag.bg} iconColor={tag.fg}>
{tag.label}
</FpTagCard>
))}
@@ -171,11 +175,11 @@ export default function EarnRewardsSection() {
{/* Stat strip — same card system as the tags above, distinguished by a filled dark surface */}
<div ref={statsRef} style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginBottom: 28, maxWidth: 460 }}>
<FpStatCard theme="fill" layout="row" style={{ flex: '1 1 130px', minWidth: 0 }} icon="⚡"
<FpStatCard theme="fill" layout="row" style={{ flex: '1 1 130px', minWidth: 0 }} icon={<FpIcon name="bolt" size={20} />}
value={<span ref={waysCountRef}>10</span>} label="ways to earn" />
<FpStatCard theme="fill" layout="row" style={{ flex: '1 1 130px', minWidth: 0 }} icon="🔥"
<FpStatCard theme="fill" layout="row" style={{ flex: '1 1 130px', minWidth: 0 }} icon={<FpIcon name="flame" size={20} />}
value="24/7" label="always live" />
<FpStatCard theme="fill" layout="row" style={{ flex: '1 1 130px', minWidth: 0 }} icon="🚀"
<FpStatCard theme="fill" layout="row" style={{ flex: '1 1 130px', minWidth: 0 }} icon={<FpIcon name="rocket" size={20} />}
value="∞" label="no earn cap" />
</div>

View File

@@ -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() {
<span style={{
display: 'flex', alignItems: 'center', justifyContent: 'center',
width: 40, height: 40, borderRadius: 12, background: p.iconBg,
fontSize: 18, marginBottom: 12,
}}>{p.icon}</span>
color: p.color, marginBottom: 12,
}}><FpIcon name={p.icon} size={20} /></span>
<div style={{ fontSize: 'clamp(20px,2.4vw,26px)', fontWeight: 900, color: p.color, fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em' }}>{p.val}</div>
<div style={{ fontSize: 11.5, color: 'rgba(23,21,18,0.5)', fontFamily: 'Sora, sans-serif', marginTop: 5, letterSpacing: '0.02em' }}>{p.label}</div>
</div>

View File

@@ -23,22 +23,25 @@ const DEFAULT_ICON_BG: Record<Theme, string> = {
};
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 (
<div className={`fp-card ${themeClass[theme]} ${className}`.trim()} style={style}>
<span className="fp-card-icon" style={{ background: iconBg ?? DEFAULT_ICON_BG[theme] }}>{icon}</span>
<span className="fp-card-icon" style={{ background: iconBg ?? DEFAULT_ICON_BG[theme], color: iconColor ?? GOLD }}>{icon}</span>
<span style={{
fontSize: 13, fontWeight: 700, fontFamily: sora, letterSpacing: '-0.01em',
whiteSpace: 'nowrap', color: theme === 'light' ? '#171512' : '#fff',
@@ -54,6 +57,8 @@ interface StatCardProps {
layout?: 'row' | 'column';
icon: React.ReactNode;
iconBg?: string;
/** Icon tint — icons draw in `currentColor`. Falls back to the theme's gold. */
iconColor?: string;
/** May be a plain node or a count-up <span> 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 }}
>
<span className="fp-card-icon fp-card-icon--lg" style={{ background: iconBg ?? DEFAULT_ICON_BG[theme] }}>{icon}</span>
<span className="fp-card-icon fp-card-icon--lg" style={{ background: iconBg ?? DEFAULT_ICON_BG[theme], color: iconColor ?? GOLD }}>{icon}</span>
<div style={{ textAlign: column ? 'center' : 'left' }}>
<div style={{
fontSize: column ? 'clamp(20px, 2.4vw, 26px)' : 18, fontWeight: 900, fontFamily: sora,

View File

@@ -0,0 +1,194 @@
'use client';
import React from 'react';
/**
* The icon set for the "For People" page — replaces the emoji that used to sit
* in the `.fp-card-icon` badges.
*
* One cohesive line system: 24px grid, no fill, 1.75 stroke, round caps/joins,
* and `currentColor` throughout so a badge only has to set `color` to tint its
* icon. Sized in px (not `em`) so a badge's font-size can't rescale the art.
*/
export type FpIconName =
| 'walk' | 'compass' | 'users' | 'store' | 'tag' | 'sparkles'
| 'bolt' | 'gamepad' | 'bag' | 'gift' | 'star' | 'pin'
| 'camera' | 'wheel' | 'ticket' | 'trophy' | 'map' | 'coffee'
| 'sneaker' | 'ban' | 'check' | 'flame' | 'rocket';
const PATHS: Record<FpIconName, React.ReactNode> = {
walk: (
<>
<circle cx="13.2" cy="4.2" r="1.9" />
<path d="M10.3 21.5l1.9-5.4-2.4-2.2V9.6l3.4-1.7 2.1 3.1 2.9 1.3" />
<path d="M8.4 12.4l1.4-2.5" />
<path d="M14.7 15.9l1.9 5.6" />
</>
),
compass: (
<>
<circle cx="12" cy="12" r="9" />
<path d="M15.6 8.4l-2.1 5.1-5.1 2.1 2.1-5.1z" />
</>
),
users: (
<>
<path d="M16 20.5v-1.9a3.6 3.6 0 00-3.6-3.6H6.6A3.6 3.6 0 003 18.6v1.9" />
<circle cx="9.5" cy="7.4" r="3.4" />
<path d="M21 20.5v-1.9a3.6 3.6 0 00-2.7-3.5" />
<path d="M15.6 4.2a3.6 3.6 0 010 6.9" />
</>
),
store: (
<>
<path d="M4 10.4V20a1 1 0 001 1h14a1 1 0 001-1v-9.6" />
<path d="M2.6 9.6l1.6-5.1a1 1 0 01.95-.7h13.7a1 1 0 01.95.7l1.6 5.1" />
<path d="M2.6 9.6a2.35 2.35 0 004.7 0 2.35 2.35 0 004.7 0 2.35 2.35 0 004.7 0 2.35 2.35 0 004.7 0" />
<path d="M9.7 21v-5.3h4.6V21" />
</>
),
tag: (
<>
<path d="M20.6 13.4l-7.2 7.2a1.6 1.6 0 01-2.3 0l-7.2-7.2a1.6 1.6 0 01-.47-1.13V4.6A1.6 1.6 0 015 3h7.67c.42 0 .83.17 1.13.47l6.8 6.8a1.6 1.6 0 010 2.26z" />
<circle cx="8.1" cy="8.1" r="1.5" />
</>
),
sparkles: (
<>
<path d="M11.4 3.2l1.85 4.45 4.45 1.85-4.45 1.85-1.85 4.45-1.85-4.45L5.1 9.5l4.45-1.85z" />
<path d="M18.4 14.9l.85 2.05 2.05.85-2.05.85-.85 2.05-.85-2.05-2.05-.85 2.05-.85z" />
</>
),
bolt: <path d="M13.4 2.5L4.6 13.9h6.1L10.6 21.5l8.8-11.4h-6.1z" />,
gamepad: (
<>
<path d="M7.6 8.6h8.8a5 5 0 014.92 4.11l.72 3.95a2.55 2.55 0 01-4.63 1.87l-1.13-1.62a2 2 0 00-1.64-.86H9.36a2 2 0 00-1.64.86l-1.13 1.62a2.55 2.55 0 01-4.63-1.87l.72-3.95A5 5 0 017.6 8.6z" />
<path d="M7.9 11.7v3.1M6.35 13.25h3.1" />
<path d="M15.7 12.6h.01M17.9 14.8h.01" />
</>
),
bag: (
<>
<path d="M5.4 7.6h13.2l.95 12.4a1 1 0 01-1 1.08H5.45a1 1 0 01-1-1.08z" />
<path d="M8.6 10.2V6.6a3.4 3.4 0 016.8 0v3.6" />
</>
),
gift: (
<>
<path d="M19.6 12.2v8.3a.5.5 0 01-.5.5H4.9a.5.5 0 01-.5-.5v-8.3" />
<rect x="2.8" y="7.6" width="18.4" height="4.6" rx="1" />
<path d="M12 7.6v13.4" />
<path d="M12 7.6H7.9a2.35 2.35 0 110-4.7C11 2.9 12 7.6 12 7.6z" />
<path d="M12 7.6h4.1a2.35 2.35 0 100-4.7C13 2.9 12 7.6 12 7.6z" />
</>
),
star: <path d="M12 2.9l2.9 5.85 6.45.95-4.67 4.55 1.1 6.42L12 17.64l-5.78 3.03 1.1-6.42L2.65 9.7l6.45-.95z" />,
pin: (
<>
<path d="M20 10.4c0 5.5-8 12.1-8 12.1s-8-6.6-8-12.1a8 8 0 1116 0z" />
<circle cx="12" cy="10.2" r="2.8" />
</>
),
camera: (
<>
<path d="M3 8.4h3.3l1.4-2.3a1 1 0 01.86-.5h6.88a1 1 0 01.86.5l1.4 2.3H21a1 1 0 011 1v9.1a1 1 0 01-1 1H3a1 1 0 01-1-1V9.4a1 1 0 011-1z" />
<circle cx="12" cy="13.9" r="3.5" />
</>
),
wheel: (
<>
<circle cx="12" cy="12" r="9" />
<circle cx="12" cy="12" r="2.3" />
<path d="M12 3v6.7M12 14.3V21M3 12h6.7M14.3 12H21" />
<path d="M5.64 5.64l4.74 4.74M13.62 13.62l4.74 4.74M18.36 5.64l-4.74 4.74M10.38 13.62l-4.74 4.74" />
</>
),
ticket: (
<>
<path d="M3 9.1V6.6a1 1 0 011-1h16a1 1 0 011 1v2.5a2.6 2.6 0 000 5.2v2.5a1 1 0 01-1 1H4a1 1 0 01-1-1v-2.5a2.6 2.6 0 000-5.2z" />
<path d="M9.6 5.9v12.4" strokeDasharray="2 2.4" />
</>
),
trophy: (
<>
<path d="M7.1 4.4h9.8v5.2a4.9 4.9 0 01-9.8 0z" />
<path d="M7.1 6.1H4.9a2.3 2.3 0 000 4.6h2.5" />
<path d="M16.9 6.1h2.2a2.3 2.3 0 010 4.6h-2.5" />
<path d="M12 14.5v3.2" />
<path d="M8.2 21h7.6" />
<path d="M9.6 17.7h4.8a1.4 1.4 0 011.4 1.4V21H8.2v-1.9a1.4 1.4 0 011.4-1.4z" />
</>
),
map: (
<>
<path d="M9.4 4.4L3.4 6.8v12.7l6-2.4 5.2 2.4 6-2.4V4.4l-6 2.4z" />
<path d="M9.4 4.4v12.7M14.6 6.8v12.7" />
</>
),
coffee: (
<>
<path d="M17.1 8.6h1.8a3 3 0 010 6h-1.8" />
<path d="M3.1 8.6h14v5.5a5 5 0 01-5 5H8.1a5 5 0 01-5-5z" />
<path d="M6.6 2.6v2.9M10.1 2.6v2.9M13.6 2.6v2.9" />
</>
),
sneaker: (
<>
<path d="M2.6 18.4v-5.6a.8.8 0 01.8-.8h3.2l3.5-2.7 1.7 1.9 4 2.4 5.5 1.85a2 2 0 011.4 1.9v1.04a.9.9 0 01-.9.9H3.5a.9.9 0 01-.9-.9z" />
<path d="M10.1 12.5l-1.7 1.6M12.5 13.8l-1.7 1.6M15.1 15.1l-1.7 1.6" />
</>
),
ban: (
<>
<circle cx="12" cy="12" r="9" />
<path d="M5.64 5.64l12.72 12.72" />
</>
),
check: (
<>
<circle cx="12" cy="12" r="9" />
<path d="M7.9 12.2l2.8 2.8 5.4-5.4" />
</>
),
flame: (
<path d="M12 22c3.87 0 7-2.98 7-6.66 0-4.55-4.35-6.86-5.56-11.14-2 1.4-3.1 3.4-3.1 5.4 0 1.6-1.3 2.2-2.2 1.2a2.9 2.9 0 01-.68-1.78C6.02 10.2 5 12.6 5 15.34 5 19.02 8.13 22 12 22z" />
),
rocket: (
<>
<path d="M4.6 16.6c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 00-2.91-.09z" />
<path d="M12 15l-3-3a22 22 0 012-3.95A12.88 12.88 0 0122 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 01-4 2z" />
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" />
<path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
</>
),
};
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 (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
focusable="false"
style={{ display: 'block', flexShrink: 0 }}
>
{PATHS[name]}
</svg>
);
}

View File

@@ -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() {
</p>
<div className="fp-rise" style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginBottom: 28, maxWidth: 460 }}>
{[
{ 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) => (
<div key={tag.label} className="fp-card fp-card--light">
<span className="fp-card-icon" style={{ background: tag.bg }}>{tag.icon}</span>
<span className="fp-card-icon" style={{ background: tag.bg, color: tag.fg }}><FpIcon name={tag.icon} /></span>
<span style={{ fontSize: 13, fontWeight: 700, fontFamily: 'Sora, sans-serif', color: '#171512', letterSpacing: '-0.01em', whiteSpace: 'nowrap' }}>
{tag.label}
</span>

View File

@@ -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() {
<div className="fp-rise" style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginBottom: 28, maxWidth: 460 }}>
{CHALLENGES.map((tag) => (
<div key={tag.label} className="fp-card fp-card--dark">
<span className="fp-card-icon" style={{ background: 'rgba(244,190,40,0.14)' }}>{tag.icon}</span>
<span className="fp-card-icon" style={{ background: 'rgba(244,190,40,0.14)', color: '#f4be28' }}><FpIcon name={tag.icon} /></span>
<span style={{ fontSize: 13, fontWeight: 700, fontFamily: 'Sora, sans-serif', color: '#fff', letterSpacing: '-0.01em', whiteSpace: 'nowrap' }}>
{tag.label}
</span>

View File

@@ -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; }
}
`}</style>
</div>
@@ -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; }
}
`}</style>
</div>

View File

@@ -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() {
</p>
<div className="fp-rise" style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginBottom: 28, maxWidth: 460 }}>
{[
{ 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) => (
<div key={tag.label} className="fp-card fp-card--dark">
<span className="fp-card-icon" style={{ background: 'rgba(244,190,40,0.14)' }}>{tag.icon}</span>
<span className="fp-card-icon" style={{ background: 'rgba(244,190,40,0.14)', color: '#f4be28' }}><FpIcon name={tag.icon} /></span>
<span style={{ fontSize: 13, fontWeight: 700, fontFamily: 'Sora, sans-serif', color: '#fff', letterSpacing: '-0.01em', whiteSpace: 'nowrap' }}>
{tag.label}
</span>
@@ -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); }
}
`}</style>
</div>

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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;