icon check
+
+
+
+
+
+
+
diff --git a/public/assets/images/about_bg.png b/public/assets/images/about_bg.png
deleted file mode 100644
index 5cdfe3d..0000000
Binary files a/public/assets/images/about_bg.png and /dev/null differ
diff --git a/public/assets/images/aboutus_bg.png b/public/assets/images/aboutus_bg.png
new file mode 100644
index 0000000..c6f767f
Binary files /dev/null and b/public/assets/images/aboutus_bg.png differ
diff --git a/src/app/about_us/page.tsx b/src/app/about_us/page.tsx
index a73aea6..b952fed 100644
--- a/src/app/about_us/page.tsx
+++ b/src/app/about_us/page.tsx
@@ -31,10 +31,14 @@ export default function AboutUsPage() {
margin: 'var(--hero-top) var(--sec-gutter) var(--sec-gap) var(--sec-gutter)', borderRadius: '32px',
position: 'relative', overflow: 'hidden',
}}>
- {/* Background photo */}
-
-
-
+ {/* Background photo — the team shot's subjects sit centre-frame, which is
+ exactly where the headline lands. The scrims below are tuned to keep
+ the faces readable rather than blacking them out: a bottom-weighted
+ gradient carries most of the text contrast, and the centre ellipse is
+ kept light. The headline earns the rest back with a text shadow. */}
+
+
+
{/* Accent blobs */}
@@ -45,11 +49,13 @@ export default function AboutUsPage() {
fontSize: 'clamp(52px, 9vw, 140px)', fontWeight: 800,
letterSpacing: '-0.06em', lineHeight: 0.88,
color: '#ffffff', margin: '0 0 36px', fontFamily: 'Sora, sans-serif',
+ // Carries legibility over the photo so the scrim doesn't have to.
+ textShadow: '0 2px 30px rgba(10,7,20,0.7)',
}}>
The People Behind Loyaly.
-
+
We're not just building a product. We're redefining retail, one store at a time.
@@ -77,7 +83,7 @@ export default function AboutUsPage() {
{/* ── MISSION & VISION ── */}
-
+
{/* Ambient top glow */}
@@ -326,6 +332,37 @@ export default function AboutUsPage() {
.au-mv-grid { grid-template-columns: 1fr !important; }
.au-mv-grid > div > div { padding: 48px 32px !important; min-height: 280px !important; }
}
+ /* Phones — the icon and copy sat side by side, which left the text roughly
+ 90px wide at 320px (two words per line). Stack them so the copy gets the
+ full card width, and drop the min-height so the card sizes to content. */
+ @media (max-width: 600px) {
+ /* 8% side padding costs ~48px of a 320px screen on top of the section
+ gutters and card padding — trim it so the copy gets the width back. */
+ .au-mv-section { padding: 56px 14px !important; }
+ .au-mv-card-layout {
+ flex-direction: column !important;
+ gap: 20px !important;
+ }
+ .au-mv-grid > div > div,
+ .au-mv-card {
+ padding: 32px 22px !important;
+ min-height: 0 !important;
+ justify-content: flex-start !important;
+ }
+ /* Slightly smaller badge now that it leads the stack */
+ .au-mv-card-layout > div:first-child {
+ width: 64px !important;
+ height: 64px !important;
+ }
+ .au-mv-card-layout > div:first-child svg {
+ width: 28px !important;
+ height: 28px !important;
+ }
+ }
+ @media (max-width: 360px) {
+ .au-mv-grid > div > div,
+ .au-mv-card { padding: 26px 18px !important; }
+ }
`}
);
@@ -374,6 +411,20 @@ function TeamCarousel() {
// Dimensions matching premium layout
const CARD_W = 320;
const CARD_GAP = 24;
+ // One card height for every state. The active/inactive size difference is done
+ // with scale (compositor-only) instead of animating height, which used to
+ // relayout the whole track on every frame of the transition.
+ const CARD_H = 470;
+ const INACTIVE_SCALE = 0.88;
+
+ // Shared motion tokens so the track, cards and details all move on one curve.
+ const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)';
+ const DUR = 0.75;
+
+ // The per-card stagger belongs to the entrance only. It used to stay applied
+ // forever, so every later slide change inherited a delay that grew with the
+ // card's index — the far cards visibly lagged the near ones.
+ const [entered, setEntered] = useState(false);
// The card width (and gap) is responsive via CSS media queries, so the slide
// distance can't be a hard-coded constant. We measure the real pixel offset
@@ -402,6 +453,15 @@ function TeamCarousel() {
};
}, [visible]);
+ // Once the staggered entrance has finished, drop the per-card delay so slide
+ // changes respond instantly and all cards move together.
+ useEffect(() => {
+ if (!visible) return;
+ const totalMs = (0.15 + (count - 1) * 0.08 + DUR) * 1000;
+ const t = setTimeout(() => setEntered(true), totalMs);
+ return () => clearTimeout(t);
+ }, [visible, count]);
+
const go = (dir: number) => setActive(a => Math.min(count - 1, Math.max(0, a + dir)));
const handleTouchStart = (e: React.TouchEvent) => {
@@ -640,8 +700,9 @@ function TeamCarousel() {
gap: CARD_GAP,
alignItems: 'center',
height: '100%',
- transform: `translateX(${-active * step}px)`,
- transition: 'transform 0.7s cubic-bezier(0.2, 0.8, 0.2, 1)',
+ transform: `translate3d(${-active * step}px, 0, 0)`,
+ transition: `transform ${DUR}s ${EASE}`,
+ willChange: 'transform',
}}
>
{TEAM_MEMBERS.map((member, i) => {
@@ -657,21 +718,28 @@ function TeamCarousel() {
flexShrink: 0,
width: CARD_W,
position: 'relative',
- height: isActive ? 470 : 400,
+ height: CARD_H,
borderRadius: 20,
overflow: 'hidden',
cursor: isActive ? 'default' : 'pointer',
background: '#0b0b10',
- border: isActive ? '2px solid #f4be28' : '1px solid rgba(255,255,255,0.06)',
- boxShadow: isActive
- ? '0 25px 50px rgba(0,0,0,0.8), 0 0 30px rgba(244,190,40,0.18)'
- : '0 10px 30px rgba(0,0,0,0.4)',
- transition: 'all 0.7s cubic-bezier(0.2, 0.8, 0.2, 1)',
+ // 2px→1px border used to shift the inner layout by a pixel
+ // mid-transition; keep the box identical and change colour only.
+ border: '1.5px solid',
+ borderColor: isActive ? '#f4be28' : 'rgba(255,255,255,0.06)',
+ boxShadow: isActive
+ ? '0 14px 30px -8px rgba(0,0,0,0.45), 0 0 22px rgba(244,190,40,0.14)'
+ : '0 8px 20px -6px rgba(0,0,0,0.28)',
+ // Explicit list — `all` was also transitioning height, forcing
+ // a full relayout of the track on every frame.
+ transition: `transform ${DUR}s ${EASE}, opacity ${DUR}s ${EASE}, border-color 0.45s ease, box-shadow 0.6s ease`,
+ transitionDelay: entered ? '0s' : `${0.15 + i * 0.08}s`,
opacity: visible ? (isActive ? 1 : 0.45) : 0,
- transform: visible
- ? (isActive ? 'scale(1)' : 'scale(0.94)')
- : 'translateY(48px) scale(0.92)',
- transitionDelay: visible ? `${0.15 + i * 0.08}s` : '0s',
+ transform: visible
+ ? (isActive ? 'scale(1)' : `scale(${INACTIVE_SCALE})`)
+ : `translate3d(0,48px,0) scale(${INACTIVE_SCALE - 0.02})`,
+ willChange: 'transform, opacity',
+ backfaceVisibility: 'hidden',
}}
>
{/* Top Left Number Tag */}
@@ -767,28 +835,40 @@ function TeamCarousel() {
marginBottom: 14
}}>{member.role}
- {/* Description */}
-
+ {/* The clipping box must own no padding/margin of its own —
+ either would floor its height and leave a stub when the
+ row collapses to 0fr. Spacing goes on the child inside. */}
+
+
{member.socials.linkedin && (
)}
+
+
@@ -960,7 +1042,7 @@ function TeamCarousel() {
background: rgba(7, 7, 13, 0.95) !important;
border-color: #f4be28 !important;
transform: translateY(-50%) scale(1.08) !important;
- box-shadow: 0 15px 35px rgba(0,0,0,0.7), 0 0 20px rgba(244,190,40,0.3) !important;
+ box-shadow: 0 10px 22px -6px rgba(0,0,0,0.45), 0 0 14px rgba(244,190,40,0.22) !important;
}
.team-social:hover {
border-color: #f4be28 !important;
@@ -972,7 +1054,8 @@ function TeamCarousel() {
.team-card:not(.is-active):hover {
border-color: rgba(255,255,255,0.15) !important;
opacity: 0.75 !important;
- transform: scale(0.96) !important;
+ /* Sits just above the 0.88 resting scale — a lift toward the active card. */
+ transform: scale(0.92) !important;
}
.team-card:not(.is-active):hover img {
filter: grayscale(0.5) brightness(0.5) contrast(0.95) !important;
@@ -1004,9 +1087,9 @@ function TeamCarousel() {
rim, and a soft light spilling from the top edge. ── */
.team-card.is-active {
box-shadow:
- 0 34px 70px -20px rgba(0,0,0,0.9),
+ 0 20px 42px -18px rgba(0,0,0,0.55),
0 0 0 1px rgba(244,190,40,0.35),
- 0 0 46px -6px rgba(244,190,40,0.28) !important;
+ 0 0 30px -8px rgba(244,190,40,0.2) !important;
}
.team-card.is-active::after {
content: '';
@@ -1077,11 +1160,9 @@ function TeamCarousel() {
.team-track {
gap: 18px !important;
}
+ /* One height for both states — the active/inactive difference is scale. */
.team-card {
width: min(300px, 80vw) !important;
- height: 412px !important;
- }
- .team-card.is-active {
height: 452px !important;
}
.team-next-overlay {
@@ -1108,14 +1189,13 @@ function TeamCarousel() {
}
.team-card {
width: min(272px, 82vw) !important;
- height: 388px !important;
- }
- .team-card.is-active {
height: 424px !important;
}
}
@media (prefers-reduced-motion: reduce) {
.team-glow, .team-kb img, .team-sheen { animation: none !important; }
+ /* Cards still change state, just without the slide/scale travel. */
+ .team-track, .team-card { transition-duration: 0.01ms !important; }
}
`}
diff --git a/src/app/for_business/page.tsx b/src/app/for_business/page.tsx
index 24df384..c0992d7 100644
--- a/src/app/for_business/page.tsx
+++ b/src/app/for_business/page.tsx
@@ -150,9 +150,12 @@ export default function ForBusinessPage() {
backgroundSize: '72px 72px',
}} />
-
-
-
+ {/* maxWidth:100% — this is a fit-content flex item, so without it a long
+ unbreakable word in the h1 stretches it past the viewport and drags
+ the centred paragraphs out with it. */}
+
+
{/* ── PROXIMITY MARKETING ── */}
-
-
+ {/* Colored gradient panel — copper bloom into gold over a warm near-black.
+ Deliberately warmer than the flat #07070d "How It Works" panel above it,
+ so two dark sections in a row still read as distinct surfaces. */}
+
+
{/* Animated city map with proximity pins */}
+ {/* Glass panel — deliberately translucent so the animated gradient
+ drifts through the map instead of being blocked by an opaque box. */}
+ {/* Gold bloom pooling under the store, and a vignette to sink the edges */}
+
{/* Store pin (centered, matches SVG store at 200,150) */}
-
-
+
+
-
@@ -492,26 +507,28 @@ export default function ForBusinessPage() {
{/* Live counter chip */}
-
- 18 shoppers nearby
+
+ 18 shoppers nearby
{/* Content */}
-
+
- Capability Spotlight
+ Capability Spotlight
-
- Reach customers the moment they're nearby.
+
+ Reach customers the moment they're nearby.
-
+
Loyaly's proximity engine sends instant notifications to shoppers browsing within your store's radius. No ad spend. No guesswork. Just timely, relevant offers delivered at exactly the right moment.
@@ -163,7 +167,7 @@ export default function EarnRewardsSection() {
{earnTags.map((tag) => (
-
+ } iconBg={tag.bg} iconColor={tag.fg}>
{tag.label}
))}
@@ -171,11 +175,11 @@ export default function EarnRewardsSection() {
{/* Stat strip — same card system as the tags above, distinguished by a filled dark surface */}