Update about us, for business pages, and for-people components
This commit is contained in:
46
public/_fp-icon-check.html
Normal file
46
public/_fp-icon-check.html
Normal 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 |
BIN
public/assets/images/aboutus_bg.png
Normal file
BIN
public/assets/images/aboutus_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
@@ -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>
|
||||
|
||||
@@ -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'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'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'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.
|
||||
</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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
194
src/components/for-people/FpIcons.tsx
Normal file
194
src/components/for-people/FpIcons.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user