diff --git a/redirects.json b/redirects.json
index c1c825b252..01ee6c7ee7 100644
--- a/redirects.json
+++ b/redirects.json
@@ -105,6 +105,11 @@
"destination": "/",
"permanent": true
},
+ {
+ "source": "/lp/card",
+ "destination": "/shhhhh",
+ "permanent": true
+ },
{
"source": "/sh",
"destination": "/card",
diff --git a/scripts/generate-valid-links.ts b/scripts/generate-valid-links.ts
index 82911450c0..64bd4b1c70 100644
--- a/scripts/generate-valid-links.ts
+++ b/scripts/generate-valid-links.ts
@@ -59,7 +59,7 @@ function main() {
// Static pages
lines.push('## Static Pages')
lines.push('')
- for (const p of ['/', '/careers', '/exchange', '/privacy', '/terms', '/lp/card']) {
+ for (const p of ['/', '/careers', '/exchange', '/privacy', '/terms']) {
lines.push(`- \`${p}\``)
}
lines.push('')
diff --git a/scripts/ping-indexnow.ts b/scripts/ping-indexnow.ts
index f0d0e0c1bd..c9ba84493d 100644
--- a/scripts/ping-indexnow.ts
+++ b/scripts/ping-indexnow.ts
@@ -27,7 +27,7 @@ async function getAllPaths(): Promise {
const { SUPPORTED_LOCALES } = await import('../src/i18n/types')
const { listContentSlugs } = await import('../src/lib/content')
- const paths: string[] = ['/', '/lp/card', '/careers', '/exchange', '/privacy', '/terms']
+ const paths: string[] = ['/', '/shhhhh', '/careers', '/exchange', '/privacy', '/terms']
for (const locale of SUPPORTED_LOCALES) {
for (const country of Object.keys(COUNTRIES_SEO)) {
diff --git a/scripts/verify-content.ts b/scripts/verify-content.ts
index 9f7374ce78..35c520cad1 100644
--- a/scripts/verify-content.ts
+++ b/scripts/verify-content.ts
@@ -152,7 +152,7 @@ function discoverRoutes(): Set {
const routes = new Set()
// Static pages
- for (const p of ['/', '/careers', '/exchange', '/privacy', '/terms', '/lp/card']) {
+ for (const p of ['/', '/careers', '/exchange', '/privacy', '/terms']) {
routes.add(p)
}
diff --git a/src/app/lp/card/CardLandingPage.tsx b/src/app/lp/card/CardLandingPage.tsx
deleted file mode 100644
index 6444eefc27..0000000000
--- a/src/app/lp/card/CardLandingPage.tsx
+++ /dev/null
@@ -1,867 +0,0 @@
-'use client'
-import { motion } from 'framer-motion'
-import Image from 'next/image'
-import { Button } from '@/components/0_Bruddle/Button'
-import { getFlagUrl } from '@/constants/countryCurrencyMapping'
-import { FAQsPanel } from '@/components/Global/FAQs'
-import PioneerCard3D from '@/components/LandingPage/PioneerCard3D'
-import { Marquee } from '@/components/LandingPage'
-import { useAuth } from '@/context/authContext'
-import { useRouter } from 'next/navigation'
-import Star from '@/assets/illustrations/star.svg'
-import HandThumbsUp from '@/assets/illustrations/hand-thumbs-up.svg'
-import { useEffect } from 'react'
-import underMaintenanceConfig from '@/config/underMaintenance.config'
-
-const faqQuestions = [
- {
- id: '0',
- question: 'What is Card Pioneers?',
- answer: 'Card Pioneers is the early-access program for the Peanut Card. Reserve a spot, get priority rollout access, and unlock Pioneer perks like $5 for every friend you refer.',
- },
- {
- id: '1',
- question: 'How does it work?',
- answer: '1. Reserve your spot by adding $10 in starter card balance. 2. Share your Peanut invite link. 3. When someone joins Card Pioneers through your invite, you earn $5 instantly, plus rewards every time they spend - forever.',
- },
- {
- id: '2',
- question: 'Is the $10 refundable?',
- answer: "Your $10 becomes starter balance when your card launches. If you're found not eligible at launch (for example: your region isn't supported, or you can't complete required verification), you can request a refund.",
- },
- {
- id: '3',
- question: 'Where is the card available?',
- answer: "We're rolling out by region in stages: US, Latin America, and Africa. You'll see eligibility details during signup.",
- },
- {
- id: '4',
- question: "Do I earn from my invites' invites too?",
- answer: 'Yes! You earn a smaller part for your entire invite tree. So if you invite someone who becomes a power-referrer, you earn from everyone they bring in too.',
- },
-]
-
-const CardLandingPage = () => {
- const { user } = useAuth()
- const router = useRouter()
- // feature flag: redirect to landing if card pioneers is disabled
- useEffect(() => {
- if (underMaintenanceConfig.disableCardPioneers) {
- router.replace('/')
- }
- }, [router])
-
- if (underMaintenanceConfig.disableCardPioneers) {
- return null
- }
-
- const handleCTA = () => {
- if (user) {
- router.push('/card')
- } else {
- router.push('/setup?redirect_uri=/card')
- }
- }
-
- // Marquee copy from CARD_coremessaging.md
- const marqueeProps = {
- visible: true,
- message: ['EARLY = EARN', 'BUILD YOUR TREE', 'ONE LINK', 'LIFETIME UPSIDE', '$5 PER INVITE', 'EARN FOREVER'],
- }
-
- return (
- <>
- {/* Hero Section - Yellow with card */}
-
-
-
-
-
-
- YOUR DOLLARS.
-
- EVERYWHERE.
-
-
-
- Pay with the peanut card. Earn with every purchase, yours or your friends.
-
-
- Non-custodial. Best rates. No hidden fees.
-
-
-
-
-
-
-
-
- JOIN PIONEERS
-
-
- $10 starter balance = your spot secured
-
-
-
-
-
-
-
-
- {/* How it works - Cream */}
-
-
-
-
-
- HOW IT WORKS
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Earn Forever - Cream Background */}
-
-
-
-
- {/* Visual - Simplified Invite Visual */}
-
-
- {/*
- LAYOUT - Calculated with Python trigonometry
- Container: 340x380
-
- L0 (YOU): center (170, 190), 80x80px
- L1 nodes: 48x48px, 120px from YOU
- - Top: center (170, 70) - outward angle -90°
- - Bottom-left: center (70, 310) - outward angle 129.8°
- - Bottom-right: center (270, 310) - outward angle 50.2°
-
- L2 nodes: 32x32px, 55px from parent L1 center, fanning at -45°, 0°, +45° from outward direction
- Top L1 (170,70): (131,31), (170,15), (209,31)
- Bottom-left L1 (70,310): (75,365), (35,352), (15,315)
- Bottom-right L1 (270,310): (325,315), (305,352), (265,365)
- */}
-
- {/* Connection lines */}
-
- {/* L0 to L1 edges */}
-
-
-
-
- {/* Top L1 (170,70) to L2 */}
-
-
-
-
- {/* Bottom-left L1 (70,310) to L2 */}
-
-
-
-
- {/* Bottom-right L1 (270,310) to L2 */}
-
-
-
-
-
- {/* L0: YOU node - center (170,190), top-left (130,150) */}
-
- YOU
-
-
- {/* +$5 BADGES - at visual midpoint between node edges
- Top edge: YOU bottom (y=150) to L1 top (y=94) -> visual mid = (150+94)/2 = 122, badge top = 114
- Bottom edges: at midpoint of line between centers
- */}
-
- +$5
-
-
- +$5
-
-
- +$5
-
-
- {/* L1: Top primary - center (170,70), top-left (146,46) */}
-
-
-
-
- {/* L1: Bottom-left primary - center (70,310), top-left (46,286) */}
-
-
-
-
- {/* L1: Bottom-right primary - center (270,310), top-left (246,286) */}
-
-
-
-
- {/* L2 NODES - positioned directly at calculated centers
- Each node is 32x32, so top-left = center - 16
- Top L1 (170,70): L2 at (131,31), (170,15), (209,31)
- Bottom-left L1 (70,310): L2 at (75,365), (35,352), (15,315)
- Bottom-right L1 (270,310): L2 at (325,315), (305,352), (265,365)
- */}
-
- {/* Top L1's children - labels 2px gap from node edge */}
-
-
-
-
- +%
-
-
-
-
-
-
- +%
-
-
-
-
-
-
- +%
-
-
- {/* Bottom-left L1's children */}
-
-
-
-
- +%
-
-
-
-
-
-
- +%
-
-
-
-
-
-
- +%
-
-
- {/* Bottom-right L1's children */}
-
-
-
-
- +%
-
-
-
-
-
-
- +%
-
-
-
-
-
-
- +%
-
-
-
-
- {/* Copy */}
-
-
- INVITE ONCE.
-
- EARN FOREVER.
-
-
-
-
-
-
-
-
-
- START EARNING
-
-
-
-
-
-
-
-
- {/* Coverage - Yellow */}
-
-
-
-
-
- ROLLING OUT
-
- GLOBALLY
-
-
-
- Starting with US , Latin America , and Africa
-
-
-
- {/* Individual country flags */}
- {[
- { name: 'United States', code: 'us' },
- { name: 'Brazil', code: 'br' },
- { name: 'Argentina', code: 'ar' },
- { name: 'Mexico', code: 'mx' },
- { name: 'Nigeria', code: 'ng' },
- { name: 'Kenya', code: 'ke' },
- { name: 'South Africa', code: 'za' },
- ].map((country, i) => (
-
-
- {country.name}
-
- ))}
-
- {/* Region pills without flags */}
-
- Latin America
-
-
- Africa
-
-
- + more
-
-
-
-
-
-
-
- {/* FAQ - Cream */}
-
-
-
-
- {/* Final CTA - Secondary Yellow */}
-
-
-
-
-
-
- Early access is open
-
-
-
- READY TO
-
- JOIN?
-
-
- $10 reserves your spot. And for every friend you invite, earn forever.
-
-
-
- BECOME A PIONEER
-
-
-
-
-
-
- >
- )
-}
-
-// Floating stars component
-const FloatingStars = () => {
- const starConfigs = [
- { className: 'absolute left-12 top-10', delay: 0.2 },
- { className: 'absolute left-56 top-1/2', delay: 0.2 },
- { className: 'absolute bottom-20 left-20', delay: 0.2 },
- { className: 'absolute -top-16 right-20 md:top-10', delay: 0.6 },
- { className: 'absolute bottom-20 right-44', delay: 0.6 },
- ]
-
- return (
- <>
- {starConfigs.map((config, index) => (
-
- ))}
- >
- )
-}
-
-// Step card component
-const StepCard = ({
- num,
- title,
- desc,
- color,
- textLight,
- delay,
-}: {
- num: string
- title: string
- desc: string
- color: string
- textLight?: boolean
- delay: number
-}) => (
-
-
- {num}
-
- {title}
- {desc}
-
-)
-
-// Reward item component
-const RewardItem = ({ amount, label }: { amount: string; label: string }) => (
-
-
- {amount}
-
- {label}
-
-)
-
-export default CardLandingPage
diff --git a/src/app/lp/card/page.tsx b/src/app/lp/card/page.tsx
deleted file mode 100644
index 5446cf17ca..0000000000
--- a/src/app/lp/card/page.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { generateMetadata as generateMeta } from '@/app/metadata'
-import { LandingPageShell } from '@/components/LandingPage/LandingPageShell'
-import Footer from '@/components/LandingPage/Footer'
-import CardLandingPage from './CardLandingPage'
-
-export const metadata = generateMeta({
- title: 'Card Pioneers | Get Early Access to Peanut Card',
- description:
- 'Join Card Pioneers for early access to the Peanut Card. Reserve your spot with $10, earn $5 for every friend who joins, and spend your dollars globally.',
- keywords:
- 'peanut card, card pioneers, crypto card, digital dollars, global spending, early access, referral rewards, international card',
-})
-
-export default function CardLPPage() {
- return (
-
-
-
-
- )
-}
diff --git a/src/app/robots.ts b/src/app/robots.ts
index 187321c1c3..6d40684050 100644
--- a/src/app/robots.ts
+++ b/src/app/robots.ts
@@ -51,7 +51,6 @@ export default function robots(): MetadataRoute.Robots {
'/privacy',
'/terms',
'/exchange',
- '/lp/card',
// SEO routes (all locale-prefixed)
...SUPPORTED_LOCALES.map((l) => `/${l}/`),
],
diff --git a/src/app/shhhhh/ShhhhhLandingPage.tsx b/src/app/shhhhh/ShhhhhLandingPage.tsx
index b0c44c7369..329d44577f 100644
--- a/src/app/shhhhh/ShhhhhLandingPage.tsx
+++ b/src/app/shhhhh/ShhhhhLandingPage.tsx
@@ -8,6 +8,7 @@ import posthog from 'posthog-js'
import { useTranslations } from 'next-intl'
import { Button } from '@/components/0_Bruddle/Button'
import { Marquee } from '@/components/LandingPage'
+import { ScarcityCounter } from '@/components/LandingPage/ScarcityCounter'
import { useAuth } from '@/context/authContext'
import { PixelatedCardFace } from '@/components/Card/share-asset/PixelatedCardFace'
import { inflateWaitlistPosition } from '@/components/Card/doorTally.utils'
@@ -68,24 +69,6 @@ const badges: Array<{ code: string; name: string; src: string }> = [
const ctaButtonClassName =
'!w-auto bg-white px-7 py-3 text-base font-extrabold hover:bg-white/90 md:px-9 md:py-8 md:text-xl'
-function ScarcityCounter() {
- const t = useTranslations('shhhhh.hero')
- const [count, setCount] = useState(21)
- useEffect(() => {
- const timer = setTimeout(() => setCount(20), 2500)
- return () => clearTimeout(timer)
- }, [])
- return (
-
- {t('onlyCount', { count })}
-
- )
-}
-
function StickyShhhhhCTA({ onClick }: { onClick: () => void }) {
const t = useTranslations('shhhhh.hero')
const [visible, setVisible] = useState(false)
@@ -302,7 +285,9 @@ export default function ShhhhhLandingPage() {
{t('hero.tagline')}
- {t.rich('hero.body', { counter: () => })}
+ {t.rich('hero.body', {
+ counter: () => t('hero.onlyCount', { count })} />,
+ })}
{isJoined ? (
diff --git a/src/app/shhhhh/page.tsx b/src/app/shhhhh/page.tsx
index 64138309d5..87752bbcb6 100644
--- a/src/app/shhhhh/page.tsx
+++ b/src/app/shhhhh/page.tsx
@@ -6,7 +6,7 @@ import ShhhhhLandingPage from './ShhhhhLandingPage'
export const metadata = generateMeta({
title: 'Shhhhh. | The peanut card is out, quietly.',
description:
- 'A non-custodial card accepted at over 150 million Visa-accepting merchants. We are letting beta users in slowly — about 20 a week.',
+ 'A non-custodial card accepted at over 150 million Visa-accepting merchants. We are letting beta users in slowly — about 50 a week.',
keywords: 'peanut card, non-custodial card, visa-accepting merchants, closed beta, stablecoins',
canonical: '/shhhhh',
})
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
index 12d74984ba..0512b51cd3 100644
--- a/src/app/sitemap.ts
+++ b/src/app/sitemap.ts
@@ -36,12 +36,12 @@ async function generateSitemap(): Promise
{
// Homepage
{ path: '', priority: 1.0, changeFrequency: 'weekly' },
- // Product pages
- { path: '/lp/card', priority: 0.9, changeFrequency: 'weekly' },
-
// Public pages
{ path: '/careers', priority: 0.7, changeFrequency: 'monthly' },
{ path: '/exchange', priority: 0.7, changeFrequency: 'weekly' },
+ // The only card landing URL left — /lp/card is a 308 to it, so the
+ // target has to be listed.
+ { path: '/shhhhh', priority: 0.7, changeFrequency: 'weekly' },
// Legal — the bare /privacy and /terms are redirects; list the real URLs.
{ path: '/en/privacy', priority: 0.5, changeFrequency: 'yearly' },
diff --git a/src/assets/icons/pix-logo.svg b/src/assets/icons/pix-logo.svg
index 4b5628c4ca..46a4882a2a 100644
--- a/src/assets/icons/pix-logo.svg
+++ b/src/assets/icons/pix-logo.svg
@@ -1,4 +1,4 @@
-
+
diff --git a/src/assets/illustrations/flag-ar.svg b/src/assets/illustrations/flag-ar.svg
new file mode 100644
index 0000000000..94a308cba6
--- /dev/null
+++ b/src/assets/illustrations/flag-ar.svg
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/illustrations/flag-br.svg b/src/assets/illustrations/flag-br.svg
new file mode 100644
index 0000000000..e87f8036cb
--- /dev/null
+++ b/src/assets/illustrations/flag-br.svg
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/illustrations/stack-coin.svg b/src/assets/illustrations/stack-coin.svg
new file mode 100644
index 0000000000..f125d3602a
--- /dev/null
+++ b/src/assets/illustrations/stack-coin.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/src/assets/logos/index.ts b/src/assets/logos/index.ts
index 4cb16fce15..535cb05908 100644
--- a/src/assets/logos/index.ts
+++ b/src/assets/logos/index.ts
@@ -1,5 +1,3 @@
export { default as PEANUT_LOGO } from './peanut-logo.svg';
export { default as PEANUT_LOGO_BLACK } from './peanut-logo-dark.svg';
-export { default as MEPA_ARGENTINA_LOGO } from './mepa-arg.svg';
-export { default as PIX_BRZ_LOGO } from './pix-brz.svg';
export { default as DEVCONNECT_LOGO } from './devconnect.svg';
\ No newline at end of file
diff --git a/src/components/Card/doorTally.utils.ts b/src/components/Card/doorTally.utils.ts
index 98dab41484..d92fcd94f0 100644
--- a/src/components/Card/doorTally.utils.ts
+++ b/src/components/Card/doorTally.utils.ts
@@ -3,7 +3,7 @@
*
* The backend (/card → waitlistTotal, admittedTotal) reports the REAL counts.
* "tried" gets inflated for FOMO — the same fake-scarcity flex the /shhhhh
- * landing already does with its "only 20 a week" ScarcityCounter — while
+ * landing already does with its "only 50 a week" ScarcityCounter — while
* "got in" is shown verbatim (the real number admitted).
*
* The inflation is a pure function of the real count (a constant multiplier
@@ -11,6 +11,13 @@
* frames. No randomness on purpose.
*/
+/**
+ * Admissions per week in the closed beta (product/card.md). Drives the
+ * ScarcityCounter pill on the homepage door fold and on /shhhhh, so both
+ * surfaces quote the same number.
+ */
+export const ADMISSIONS_PER_WEEK = 50
+
/**
* FOMO inflation factor applied to the real waitlist size for "tried". ×5 (not
* ×3) so the real number clears the 213 floor at the current prod waitlist size
diff --git a/src/components/Card/share-asset/PixelatedCardFace.tsx b/src/components/Card/share-asset/PixelatedCardFace.tsx
index f3e5f57a30..bfe6c57c37 100644
--- a/src/components/Card/share-asset/PixelatedCardFace.tsx
+++ b/src/components/Card/share-asset/PixelatedCardFace.tsx
@@ -59,6 +59,10 @@ export interface PixelatedCardFaceProps {
* display the Visa wordmark for compliance reasons (it renders crisp
* there since the share asset is the one surface that doesn't `blurAll`). */
hideVisa?: boolean
+ /** Colour of the offset drop shadow. Defaults to the design-system black;
+ * the homepage door fold overrides it because a black shadow is invisible
+ * on that fold's black ground. */
+ shadowColor?: string
/** Fires once the hand has loaded — i.e. the card face is fully
* painted. Capture surfaces gate the Share/Save buttons on this so a
* snapshot can never fire before the hand is ready. */
@@ -70,6 +74,7 @@ export const PixelatedCardFace: FC = ({
style,
blurAll = false,
hideVisa = false,
+ shadowColor = '#000',
onReady,
}) => (
// The drop-shadow is an offset sibling, NOT a CSS box-shadow: html-to-image
@@ -80,7 +85,7 @@ export const PixelatedCardFace: FC = ({
}
+// Matches the landing page's other "learn more" affordances: right-aligned,
+// underlined at rest, arrow trailing.
+const learnMoreClass = 'font-roboto-flex text-base text-n-1 underline hover:no-underline md:text-lg'
+
function linkifyText(text: string) {
const markdownLinkRegex = /\[([^\]]+)\]\(([^\s)]+)\)/g
const parts: (string | JSX.Element)[] = []
@@ -41,7 +49,7 @@ function linkifyText(text: string) {
return parts
}
-export function FAQsPanel({ heading, questions }: FAQsProps) {
+export function FAQsPanel({ heading, questions, learnMoreLabel = 'Learn more' }: FAQsProps) {
return (
@@ -78,6 +86,13 @@ export function FAQsPanel({ heading, questions }: FAQsProps) {
{faq.redirectText}
)}
+ {faq.learnMoreHref && (
+
+
+ {learnMoreLabel} →
+
+
+ )}
))}
diff --git a/src/components/Global/MarqueeWrapper/index.tsx b/src/components/Global/MarqueeWrapper/index.tsx
index d2d3eb7207..dcc2d64ccf 100644
--- a/src/components/Global/MarqueeWrapper/index.tsx
+++ b/src/components/Global/MarqueeWrapper/index.tsx
@@ -1,7 +1,9 @@
'use client'
import Image from 'next/image'
+import Link from 'next/link'
import Marquee from 'react-fast-marquee'
+import type { MarqueeItem } from './marquee.types'
type directionType = 'left' | 'right' | 'up' | 'down' | undefined
@@ -32,6 +34,19 @@ export function MarqueeWrapper({
)
}
+const wordClass = 'text-lg font-bold uppercase md:text-xl'
+
+// A word is either plain text or a link — the strip looks identical either way,
+// so a linked word still reads as part of the run of words.
+function MarqueeWord({ item }: { item: MarqueeItem }) {
+ if (typeof item === 'string') return {item}
+ return (
+
+ {item.label}
+
+ )
+}
+
// MarqueeComp: A pre-configured marquee component with message and image
export function MarqueeComp({
message,
@@ -39,7 +54,7 @@ export function MarqueeComp({
imageAnimationClass = 'animation-thumbsUp',
backgroundColor = 'bg-primary',
}: {
- message?: string | string[]
+ message?: string | MarqueeItem[]
imageSrc: string
imageAnimationClass?: string
backgroundColor?: string
@@ -50,11 +65,11 @@ export function MarqueeComp({
{Array.isArray(message)
? message.map((msg, index) => (
-
{msg}
+
{index < message.length && (
- {message}
+ {message}
{
- const router = useRouter()
- const [screenWidth, setScreenWidth] = useState(1200)
-
- useEffect(() => {
- const handleResize = () => setScreenWidth(window.innerWidth)
- handleResize() // Set actual width on mount
- window.addEventListener('resize', handleResize)
- return () => window.removeEventListener('resize', handleResize)
- }, [])
-
- const isMobile = screenWidth < 768
-
- const handleCTA = () => {
- router.push('/lp/card')
- }
-
- return (
-
- {!isMobile && }
-
- {/* Card on left */}
-
-
- {/* Copy on right */}
-
-
- {strings.cardHeading}
-
-
-
{strings.cardBody}
-
-
-
-
- {strings.cardBullet1}
-
-
-
- {strings.cardBullet2}
-
-
-
- {strings.cardBullet3}
-
-
-
- {strings.cardDisclaimer}
-
-
-
-
- {strings.cardCta}
-
-
-
-
-
- )
-}
-
-// Animated stars - matches Manteca.tsx pattern
-const Stars = () => (
- <>
-
-
-
- >
-)
-
-export { CardPioneers }
diff --git a/src/components/LandingPage/LandingPageClient.tsx b/src/components/LandingPage/LandingPageClient.tsx
index 6a754910fb..09cfff140c 100644
--- a/src/components/LandingPage/LandingPageClient.tsx
+++ b/src/components/LandingPage/LandingPageClient.tsx
@@ -2,7 +2,8 @@
import { useFooterVisibility } from '@/context/footerVisibility'
import { Suspense, useEffect, useMemo, useState, useRef, useCallback, type ReactNode } from 'react'
-import { DropLink, FAQs, Hero, Marquee, NoFees, CardPioneers } from '@/components/LandingPage'
+import { FAQs, Hero, Marquee, NoFees } from '@/components/LandingPage'
+import { ShhhhhFold } from '@/components/LandingPage/ShhhhhFold'
import { SupportedRailsFaqAnswer } from '@/components/LandingPage/SupportedRailsFaqAnswer'
import { SUPPORTED_RAILS_FAQ_ID } from '@/constants/faq.consts'
import TweetCarousel from '@/components/LandingPage/TweetCarousel'
@@ -37,6 +38,7 @@ type LandingPageClientProps = {
locale: Locale
strings: LandingStrings
// Server-rendered slots
+ problemSlot: ReactNode
mantecaSlot: ReactNode
regulatedRailsSlot: ReactNode
yourMoneySlot: ReactNode
@@ -51,6 +53,7 @@ export function LandingPageClient({
marqueeMessages,
locale,
strings,
+ problemSlot,
mantecaSlot,
regulatedRailsSlot,
yourMoneySlot,
@@ -63,8 +66,14 @@ export function LandingPageClient({
// app-locale translation (LatAm-first funnel); the flag-off label still
// comes from the content system per landing locale
const tMigration = useTranslations('migration')
+ // the strip under the door fold speaks /shhhhh's vocabulary, not the
+ // product one every other strip repeats
+ const tDoorMarquee = useTranslations('shhhhh.marquee')
const { deviceType } = useDeviceType()
const isDesktop = deviceType === DeviceType.WEB
+ // Kill switch: the door fold and the closed-beta strip under it are one
+ // promise, so they go dark together.
+ const doorFoldOn = !underMaintenanceConfig.disableLandingCardFold
// pwa-sunset hero CTAs are device-based: phones get one "Download now"
// with their store's mark deep-linking to it; desktop drops the primary
@@ -89,13 +98,26 @@ export function LandingPageClient({
// Memoized: this component re-renders per scroll frame during the button
// animation — don't rebuild the FAQ array + rich answer element each time.
- const faqQuestions = useMemo(
- () =>
- faqData.questions.map((q) =>
- q.id === SUPPORTED_RAILS_FAQ_ID ? { ...q, answerContent: } : q
- ),
- [faqData.questions]
- )
+ const faqQuestions = useMemo(() => {
+ // The questions come from the content system, which must not carry code
+ // concerns, so the article each one continues into is mapped here by id.
+ // "Why Peanut?" and "My question is not here" are left out on purpose:
+ // the first has no single article behind it, the second already links
+ // the help centre in its own answer.
+ const learnMore: Record = {
+ '1': `/${locale}/help/what-are-digital-dollars`,
+ '2': `/${locale}/help/verification`,
+ '3': `/${locale}/help/passkeys`,
+ '4': `/${locale}/help/security-custody`,
+ '5': `/${locale}/help/fees-pricing`,
+ [SUPPORTED_RAILS_FAQ_ID]: `/${locale}/help/supported-geographies`,
+ }
+ return faqData.questions.map((q) => ({
+ ...q,
+ ...(q.id === SUPPORTED_RAILS_FAQ_ID ? { answerContent: } : {}),
+ ...(learnMore[q.id] ? { learnMoreHref: learnMore[q.id] } : {}),
+ }))
+ }, [faqData.questions, locale])
const [buttonVisible, setButtonVisible] = useState(true)
const [isScrollFrozen, setIsScrollFrozen] = useState(false)
@@ -228,7 +250,42 @@ export function LandingPageClient({
}
}, [handleScrollDelta])
- const marqueeProps = { visible: true, message: marqueeMessages }
+ // Only the words with a real article behind them become links; the rest
+ // stay plain text. Words come from the content system's marquee list, so an
+ // edit there just drops out of this map and renders unlinked.
+ const marqueeProps = useMemo(() => {
+ const hrefs: Record = {
+ 'No transfer fees': `/${locale}/pricing`,
+ USD: `/${locale}/help/what-are-digital-dollars`,
+ EUR: `/${locale}/help/send-euros-argentina`,
+ 'USDT/USDC': `/${locale}/blog/stablecoin-balance-visa-merchants`,
+ GLOBAL: `/${locale}/help/supported-geographies`,
+ 'SELF-CUSTODIAL': `/${locale}/help/security-custody`,
+ // app route, not a locale-prefixed marketing page
+ '24/7': '/support',
+ }
+ return {
+ visible: true,
+ message: marqueeMessages.map((word) => (hrefs[word] ? { label: word, href: hrefs[word] } : word)),
+ }
+ }, [marqueeMessages, locale])
+
+ // Memoized for the same reason as faqQuestions above — this component
+ // re-renders per scroll frame while the send button grows.
+ const doorMarqueeProps = useMemo(
+ () => ({
+ visible: true,
+ // the whole strip is the door: every word goes to /shhhhh
+ message: [
+ tDoorMarquee('iykyk'),
+ tDoorMarquee('wordTravels'),
+ tDoorMarquee('closedBeta'),
+ tDoorMarquee('shhhh'),
+ tDoorMarquee('peanutClub'),
+ ].map((label) => ({ label, href: '/shhhhh' })),
+ }),
+ [tDoorMarquee]
+ )
return (
<>
@@ -252,34 +309,39 @@ export function LandingPageClient({
}
/>
- {mantecaSlot}
-
- {yourMoneySlot}
-
- {!underMaintenanceConfig.disableCardPioneers && (
+ {doorFoldOn && (
<>
-
-
+
+
>
)}
+ {problemSlot}
+
+ {/* Suspense needed: NoFees renders ExchangeRateWidget which uses useSearchParams().
+ Without this boundary, the entire LandingPageClient suspends during SSR,
+ sending an empty HTML shell to crawlers and killing SEO. */}
+
+
+
+
+ {yourMoneySlot}
+
{regulatedRailsSlot}
-
+ {mantecaSlot}
{securitySlot}
{sendInSecondsSlot}
- {/* Suspense needed: NoFees renders ExchangeRateWidget which uses useSearchParams().
- Without this boundary, the entire LandingPageClient suspends during SSR,
- sending an empty HTML shell to crawlers and killing SEO. */}
-
-
-
-
-
+
{footerSlot}
diff --git a/src/components/LandingPage/LandingPageContent.tsx b/src/components/LandingPage/LandingPageContent.tsx
index dc86550fd6..57807b0d14 100644
--- a/src/components/LandingPage/LandingPageContent.tsx
+++ b/src/components/LandingPage/LandingPageContent.tsx
@@ -5,6 +5,7 @@ import { RegulatedRails } from './RegulatedRails'
import { YourMoney } from './yourMoney'
import { SecurityBuiltIn } from './securityBuiltIn'
import { SendInSeconds } from './sendInSeconds'
+import { ProblemFold } from './ProblemFold'
import Footer from './Footer'
import { faqSchema } from '@/lib/seo/schemas'
import { singletonLocaleFor } from '@/lib/content'
@@ -14,11 +15,16 @@ import { getTranslations } from '@/i18n'
import { landingStrings } from './landingStrings'
import type { Locale } from '@/i18n/types'
+// Blue, not Manteca's default cream: on the homepage it follows RegulatedRails,
+// which is cream already.
+const MANTECA_BG_COLOR = '#90A8ED'
+
// Shared body of the landing page, rendered by / (en) and by each per-locale
// landing route. Reads the filesystem via getLandingContent, so this must stay
// a server component.
export function LandingPageContent({ locale }: { locale: Locale }) {
const { heroConfig, faqData, marqueeMessages } = getLandingContent(locale)
+ const strings = landingStrings(getTranslations(locale))
// inLanguage reflects the language the FAQ prose actually resolved to —
// until mono ships landing translations, that's English on every locale.
const faqJsonLd = faqSchema(
@@ -38,8 +44,9 @@ export function LandingPageContent({ locale }: { locale: Locale }) {
faqData={faqData}
marqueeMessages={marqueeMessages}
locale={locale}
- strings={landingStrings(getTranslations(locale))}
- mantecaSlot={ }
+ strings={strings}
+ problemSlot={ }
+ mantecaSlot={ }
regulatedRailsSlot={ }
yourMoneySlot={ }
securitySlot={ }
diff --git a/src/components/LandingPage/Manteca.tsx b/src/components/LandingPage/Manteca.tsx
index b6c52eede6..911a0302c0 100644
--- a/src/components/LandingPage/Manteca.tsx
+++ b/src/components/LandingPage/Manteca.tsx
@@ -1,9 +1,11 @@
import mantecaIphone from '@/assets/iphone-ss/manteca_ss.webp'
import Image from 'next/image'
-import Link from 'next/link'
-import MEPA_ARGENTINA_LOGO from '@/assets/logos/mepa-arg.svg'
-import PIX_BRZ_LOGO from '@/assets/logos/pix-brz.svg'
+import MERCADO_PAGO_ICON from '@/assets/icons/mercado-pago-logo.svg'
+import PIX_ICON from '@/assets/icons/pix-logo.svg'
+import FLAG_AR from '@/assets/illustrations/flag-ar.svg'
+import FLAG_BR from '@/assets/illustrations/flag-br.svg'
import Star from '@/assets/illustrations/star.svg'
+import { PartnerLockup } from './PartnerLockup'
import { CloudsCss } from './CloudsCss'
import { AnimateOnView } from '@/components/Global/AnimateOnView'
import { getTranslations } from '@/i18n'
@@ -17,14 +19,24 @@ const starConfigs = [
{ className: 'absolute bottom-20 right-44', delay: '0.6s', rotate: '22deg' },
]
-const Manteca = ({ locale = DEFAULT_LOCALE }: { locale?: Locale }) => {
+// Cream, as on /quests. The homepage passes blue instead — there it follows
+// RegulatedRails, which is already cream.
+const DEFAULT_BG_COLOR = '#F9F4F0'
+
+const Manteca = ({
+ locale = DEFAULT_LOCALE,
+ backgroundColor = DEFAULT_BG_COLOR,
+}: {
+ locale?: Locale
+ backgroundColor?: string
+}) => {
const i18n = getTranslations(locale)
return (
@@ -56,38 +68,37 @@ const Manteca = ({ locale = DEFAULT_LOCALE }: { locale?: Locale }) => {
{i18n.landingPayLocalBody}
- {/* Mobile layout */}
-
-
- {/* Desktop layout */}
-
-
-
-
-
-
-
-
-
+
+ {/* On mobile the two lockups sit side by side above the phone, so the
+ sticky CTA covers the screenshot rather than either link. `md:contents`
+ drops this wrapper on desktop so they flank the phone instead. */}
+
-
{i18n.landingPayLocalSettles}
+
-
-
- {i18n.landingPayLocalSettles}
-
)
}
diff --git a/src/components/LandingPage/PartnerLockup.tsx b/src/components/LandingPage/PartnerLockup.tsx
new file mode 100644
index 0000000000..8db499d77b
--- /dev/null
+++ b/src/components/LandingPage/PartnerLockup.tsx
@@ -0,0 +1,56 @@
+import Image, { type StaticImageData } from 'next/image'
+import Link from 'next/link'
+import { twMerge } from 'tailwind-merge'
+
+/**
+ * A country flag over its payment rail's mark, linking to the page that explains
+ * the rail.
+ *
+ * The mark sits directly on the section ground — black is 9:1 on the homepage
+ * blue and 19:1 on the /quests cream, so it needs no card (unlike
+ * `#regulated-rails`, whose white inner card exists because those marks sit on
+ * pink). The fixed-height slot keeps the two marks on a shared baseline; the
+ * underlined "Learn more →" is the only click affordance, so it stays.
+ */
+export function PartnerLockup({
+ flag,
+ flagAlt,
+ logo,
+ logoAlt,
+ logoClassName,
+ href,
+ ariaLabel,
+ learnMore,
+ className,
+}: {
+ flag: StaticImageData
+ flagAlt: string
+ logo: StaticImageData
+ logoAlt: string
+ logoClassName: string
+ href: string
+ ariaLabel: string
+ learnMore: string
+ className?: string
+}) {
+ return (
+
+
+
+
+
+
+
+
+ {learnMore} →
+
+
+ )
+}
diff --git a/src/components/LandingPage/PioneerCard3D.tsx b/src/components/LandingPage/PioneerCard3D.tsx
deleted file mode 100644
index dd53dcc05b..0000000000
--- a/src/components/LandingPage/PioneerCard3D.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-'use client'
-import { motion, useMotionValue, useTransform, useSpring, useMotionTemplate } from 'framer-motion'
-import Image from 'next/image'
-import { CARD_GRADIENT_4, CARD_GRADIENT_5, CARD_GRADIENT_9, CARD_GRADIENT_10 } from '@/assets/cards'
-import { useRef, useState, useEffect, useCallback } from 'react'
-
-const CARD_BACKGROUNDS = [CARD_GRADIENT_4, CARD_GRADIENT_9, CARD_GRADIENT_10, CARD_GRADIENT_5]
-const CYCLE_INTERVAL = 3000
-
-interface PioneerCard3DProps {
- className?: string
-}
-
-const PioneerCard3D = ({ className }: PioneerCard3DProps) => {
- const cardRef = useRef
(null)
- const [activeIndex, setActiveIndex] = useState(0)
- const timerRef = useRef(null)
-
- const x = useMotionValue(0)
- const y = useMotionValue(0)
- const isInteractingRef = useRef(false)
- const resumeTimerRef = useRef | null>(null)
-
- const rotateX = useTransform(y, [-100, 100], [12, -12])
- const rotateY = useTransform(x, [-100, 100], [-12, 12])
-
- const shadowX = useTransform(x, [-100, 100], [14, -2])
- const shadowY = useTransform(y, [-100, 100], [14, -2])
-
- const springRotateX = useSpring(rotateX, { stiffness: 200, damping: 25 })
- const springRotateY = useSpring(rotateY, { stiffness: 200, damping: 25 })
- const springShadowX = useSpring(shadowX, { stiffness: 200, damping: 25 })
- const springShadowY = useSpring(shadowY, { stiffness: 200, damping: 25 })
-
- const boxShadow = useMotionTemplate`${springShadowX}px ${springShadowY}px 0 #000000`
-
- const advance = useCallback(() => {
- setActiveIndex((prev) => (prev + 1) % CARD_BACKGROUNDS.length)
- }, [])
-
- const resetTimer = useCallback(() => {
- if (timerRef.current) clearInterval(timerRef.current)
- timerRef.current = setInterval(advance, CYCLE_INTERVAL)
- }, [advance])
-
- useEffect(() => {
- resetTimer()
- return () => {
- if (timerRef.current) clearInterval(timerRef.current)
- }
- }, [resetTimer])
-
- // Mobile auto-oscillation: slow sine wave to show off parallax without interaction
- // Pauses when user touches/clicks the card and resumes 2s after release
- useEffect(() => {
- const isMobile = window.matchMedia('(max-width: 768px)').matches
- if (!isMobile) return
-
- let frame: number
- const startTime = Date.now()
-
- const animate = () => {
- if (!isInteractingRef.current) {
- const elapsed = (Date.now() - startTime) / 1000
- // gentle figure-8 pattern using offset sine waves
- x.set(Math.sin(elapsed * 0.4) * 60)
- y.set(Math.sin(elapsed * 0.3 + 1) * 40)
- }
- frame = requestAnimationFrame(animate)
- }
-
- frame = requestAnimationFrame(animate)
- return () => cancelAnimationFrame(frame)
- }, [x, y])
-
- const pauseOscillation = useCallback(() => {
- isInteractingRef.current = true
- if (resumeTimerRef.current) clearTimeout(resumeTimerRef.current)
- }, [])
-
- const resumeOscillation = useCallback(() => {
- if (resumeTimerRef.current) clearTimeout(resumeTimerRef.current)
- resumeTimerRef.current = setTimeout(() => {
- isInteractingRef.current = false
- }, 2000)
- }, [])
-
- useEffect(() => {
- return () => {
- if (resumeTimerRef.current) clearTimeout(resumeTimerRef.current)
- }
- }, [])
-
- const handleClick = () => {
- advance()
- resetTimer()
- }
-
- const handlePointerDown = (e: React.PointerEvent) => {
- pauseOscillation()
- if (!cardRef.current) return
- const rect = cardRef.current.getBoundingClientRect()
- x.set(e.clientX - rect.left - rect.width / 2)
- y.set(e.clientY - rect.top - rect.height / 2)
- }
-
- const handlePointerMove = (e: React.PointerEvent) => {
- if (!cardRef.current) return
- const rect = cardRef.current.getBoundingClientRect()
- x.set(e.clientX - rect.left - rect.width / 2)
- y.set(e.clientY - rect.top - rect.height / 2)
- }
-
- const handlePointerLeave = () => {
- x.set(0)
- y.set(0)
- resumeOscillation()
- }
-
- return (
-
-
- {/* All card images layered, only the active one is visible */}
- {CARD_BACKGROUNDS.map((bg, i) => (
-
-
-
- ))}
-
-
- )
-}
-
-export default PioneerCard3D
diff --git a/src/components/LandingPage/ProblemFold.tsx b/src/components/LandingPage/ProblemFold.tsx
new file mode 100644
index 0000000000..a08839cb36
--- /dev/null
+++ b/src/components/LandingPage/ProblemFold.tsx
@@ -0,0 +1,77 @@
+import Image from 'next/image'
+import HandPeace from '@/assets/illustrations/hand-peace.svg'
+import StackCoin from '@/assets/illustrations/stack-coin.svg'
+import type { LandingStrings } from './landingStrings'
+
+const bgColor = '#F9F4F0'
+
+// One height for all three marks so the card tops line up. The glyph needs a
+// bigger type size than the drawings to reach the same optical weight — a
+// Knerd "%" only fills about two thirds of its em box.
+const iconClass = 'h-11 w-auto'
+
+// Card tilts, in source order. Small and alternating, so the row reads as
+// hand-placed rather than as a broken grid.
+const tilts = ['-rotate-1', 'rotate-[0.8deg]', '-rotate-[0.6deg]']
+
+/**
+ * "You already know the problem." — three named cards, one per person we built
+ * peanut for. No links: this fold names the pain, the folds under it answer it.
+ */
+export function ProblemFold({ strings }: { strings: LandingStrings }) {
+ const { problem } = strings
+
+ const cards = [
+ {
+ id: 'cross-border',
+ title: problem.crossBorderTitle,
+ body: problem.crossBorderBody,
+ icon: ,
+ },
+ {
+ id: 'send-home',
+ title: problem.sendHomeTitle,
+ body: problem.sendHomeBody,
+ icon: (
+
+ %
+
+ ),
+ },
+ {
+ id: 'paid-abroad',
+ title: problem.paidAbroadTitle,
+ body: problem.paidAbroadBody,
+ icon: ,
+ },
+ ]
+
+ return (
+
+
+
+ {problem.heading}
+
+
+
+ {cards.map((card, i) => (
+
+
{card.icon}
+
+ {card.title}
+
+
{card.body}
+
+ ))}
+
+
+
+ )
+}
diff --git a/src/components/LandingPage/RegulatedRails.tsx b/src/components/LandingPage/RegulatedRails.tsx
index 168d0a21d4..003c48eb99 100644
--- a/src/components/LandingPage/RegulatedRails.tsx
+++ b/src/components/LandingPage/RegulatedRails.tsx
@@ -1,4 +1,5 @@
import Image from 'next/image'
+import Link from 'next/link'
import { MarqueeWrapper } from '../Global/MarqueeWrapper'
import BBVA_ICON from '@/assets/icons/bbva-logo.svg'
import BRUBANK_ICON from '@/assets/icons/brubank-logo.svg'
@@ -17,18 +18,32 @@ import { DEFAULT_LOCALE, type Locale } from '@/i18n/types'
const bgColor = '#F9F4F0'
+/**
+ * `path` is the locale-prefixed page that honestly explains what this partner
+ * does with Peanut. A logo with nothing truthful to point at stays unlinked —
+ * Brubank and Stripe have no article covering their rail, so they do.
+ *
+ * `onWhite` gives a logo a white card behind it: these two are the only marks
+ * in the set that are not a heavy wordmark, and they disappear on the pink.
+ */
const logos = [
- { logo: BBVA_ICON, alt: 'BBVA' },
+ { logo: BBVA_ICON, alt: 'BBVA', path: 'help/deposit-bank' },
{ logo: BRUBANK_ICON, alt: 'Brubank' },
- { logo: N26_ICON, alt: 'N26' },
- { logo: SANTANDER_ICON, alt: 'Santander' },
- { logo: REVOLUT_ICON, alt: 'Revolut' },
+ { logo: N26_ICON, alt: 'N26', path: 'help/deposit-bank' },
+ { logo: SANTANDER_ICON, alt: 'Santander', path: 'help/deposit-bank' },
+ { logo: REVOLUT_ICON, alt: 'Revolut', path: 'compare/peanut-vs-revolut' },
{ logo: STRIPE_ICON, alt: 'Stripe' },
- { logo: MERCADO_PAGO_ICON, alt: 'Mercado Pago' },
- { logo: PIX_ICON, alt: 'PIX' },
- { logo: WISE_ICON, alt: 'Wise' },
+ { logo: MERCADO_PAGO_ICON, alt: 'Mercado Pago', path: 'help/mercadopago-qr', onWhite: true },
+ { logo: PIX_ICON, alt: 'PIX', path: 'brazil', onWhite: true },
+ { logo: WISE_ICON, alt: 'Wise', path: 'compare/peanut-vs-wise' },
]
+// my-2, not mb-2: react-fast-marquee's container is overflow-x:hidden, which
+// makes the Y axis compute to auto — it clips. Without top margin the tile sits
+// flush against that edge and the hover lift shaves its top border off.
+const tileClass = 'btn btn-purple btn-shadow-primary-4 mx-7 my-2 flex h-26 w-48 items-center gap-2'
+const linkedTileClass = `${tileClass} transition-transform hover:-translate-y-0.5 hover:opacity-90`
+
const regulatedRailsClouds = [
{ top: '20%', width: 200, speed: '38s', direction: 'ltr' as const },
{ top: '60%', width: 220, speed: '34s', direction: 'rtl' as const },
@@ -58,7 +73,7 @@ export function RegulatedRails({ locale = DEFAULT_LOCALE }: { locale?: Locale })
{i18n.landingRailsBody}
-
diff --git a/src/components/LandingPage/ScarcityCounter.tsx b/src/components/LandingPage/ScarcityCounter.tsx
new file mode 100644
index 0000000000..4aa43215d0
--- /dev/null
+++ b/src/components/LandingPage/ScarcityCounter.tsx
@@ -0,0 +1,40 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { motion, useReducedMotion } from 'framer-motion'
+import { ADMISSIONS_PER_WEEK } from '@/components/Card/doorTally.utils'
+
+// The counter starts one above the real weekly rate and ticks down once, so
+// the number reads as live.
+const COUNTER_START = ADMISSIONS_PER_WEEK + 1
+
+/**
+ * "Only N a week" scarcity pill, shared by the homepage door fold and the
+ * /shhhhh hero. `label` builds the copy for a given count — the caller owns
+ * the interpolation, so the pill never has to know which catalog it is in.
+ */
+export function ScarcityCounter({ label }: { label: (count: number) => string }) {
+ const prefersReducedMotion = useReducedMotion()
+ const [count, setCount] = useState(COUNTER_START)
+
+ useEffect(() => {
+ if (prefersReducedMotion) {
+ setCount(ADMISSIONS_PER_WEEK)
+ return
+ }
+ const timer = setTimeout(() => setCount(ADMISSIONS_PER_WEEK), 2500)
+ return () => clearTimeout(timer)
+ }, [prefersReducedMotion])
+
+ const ticked = count === ADMISSIONS_PER_WEEK
+
+ return (
+
+ {label(count)}
+
+ )
+}
diff --git a/src/components/LandingPage/ShhhhhFold.tsx b/src/components/LandingPage/ShhhhhFold.tsx
new file mode 100644
index 0000000000..a9b2353889
--- /dev/null
+++ b/src/components/LandingPage/ShhhhhFold.tsx
@@ -0,0 +1,80 @@
+'use client'
+
+import dynamic from 'next/dynamic'
+import Link from 'next/link'
+import { useTranslations } from 'next-intl'
+import { Button } from '@/components/0_Bruddle/Button'
+
+// The card face renders nothing on the server and rasterises four canvases on
+// mount, so it stays out of the first chunk. The host box reserves the aspect
+// ratio, which keeps the fold from shifting when the chunk lands.
+const ScaledPixelatedCardFace = dynamic(
+ () => import('@/components/Card/share-asset/ScaledPixelatedCardFace').then((m) => m.ScaledPixelatedCardFace),
+ { ssr: false }
+)
+
+// On a black ground a black drop shadow is invisible, so the card and the
+// button both carry brand pink here. Deliberate deviation from the
+// all-black-shadow rule, and it applies on this fold only. The button repeats
+// the hex in its class because Tailwind only sees literals.
+const PINK = '#FF90E8'
+
+/**
+ * The homepage door to /shhhhh: wordmark, tagline, the door itself, the card.
+ * Every click here goes to /shhhhh — the fold makes the offer and the page
+ * does the work (the pitch, the scarcity count, sign-in, badge check, waitlist).
+ */
+export function ShhhhhFold() {
+ const t = useTranslations('shhhhh.hero')
+
+ return (
+
+
+
+
+ {t('wordmark')}
+
+
+
+ {t('tagline')}
+
+
+
+
+ {/* no shadowSize: that prop only paints black. The pink
+ shadow is an arbitrary class so the base
+ `active:shadow-none` still flattens it on press. */}
+
+ {t('tryTheDoor')}
+
+
+
+ {t('orJoinWaitlist')}
+
+
+
+
+ {t('disclaimer')}
+
+
+
+
+ {/* blurAll: closed-beta tease — card shape recognisable, logos +
+ number unreadable. Rotation sits on the wrapper so the inner
+ host still measures its layout width for the fit-to-width scale. */}
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/LandingPage/__tests__/landingLinks.utils.test.ts b/src/components/LandingPage/__tests__/landingLinks.utils.test.ts
new file mode 100644
index 0000000000..73b13a44d9
--- /dev/null
+++ b/src/components/LandingPage/__tests__/landingLinks.utils.test.ts
@@ -0,0 +1,61 @@
+import { linkTerms, type LinkedTerm } from '../landingLinks.utils'
+
+// the real city terms the "global cash" line links, aliases and all
+const CITIES: LinkedTerm[] = [
+ { aliases: ['New York', 'Nueva York', 'Nova York'], href: '/en/united-states' },
+ { aliases: ['Madrid', 'Madri'], href: '/en/spain' },
+ { aliases: ['Mexico City', 'Ciudad de México', 'Cidade do México'], href: '/en/mexico' },
+]
+
+describe('linkTerms', () => {
+ it('links every city in the English line, in reading order', () => {
+ const parts = linkTerms('From New York to Madrid to Mexico City — send and share instantly.', CITIES)
+
+ expect(parts).toEqual([
+ { text: 'From ' },
+ { text: 'New York', href: '/en/united-states' },
+ { text: ' to ' },
+ { text: 'Madrid', href: '/en/spain' },
+ { text: ' to ' },
+ { text: 'Mexico City', href: '/en/mexico' },
+ { text: ' — send and share instantly.' },
+ ])
+ })
+
+ it.each([
+ ['es-419', 'De Nueva York a Madrid y Ciudad de México: envía y comparte al instante.'],
+ ['es-ar', 'De Nueva York a Madrid y Ciudad de México: enviá y compartí al instante.'],
+ ['pt-br', 'De Nova York a Madri e à Cidade do México: envie e compartilhe na hora.'],
+ ])('links all three cities in the %s translation', (_locale, line) => {
+ const linked = linkTerms(line, CITIES).filter((p) => p.href)
+
+ expect(linked.map((p) => p.href)).toEqual(['/en/united-states', '/en/spain', '/en/mexico'])
+ // the sentence survives the split intact
+ expect(
+ linkTerms(line, CITIES)
+ .map((p) => p.text)
+ .join('')
+ ).toBe(line)
+ })
+
+ it('keeps "Madrid" whole rather than matching its prefix "Madri"', () => {
+ const parts = linkTerms('to Madrid today', [{ aliases: ['Madri', 'Madrid'], href: '/en/spain' }])
+
+ expect(parts).toEqual([{ text: 'to ' }, { text: 'Madrid', href: '/en/spain' }, { text: ' today' }])
+ })
+
+ it('renders plain text for a term the translation does not contain', () => {
+ const parts = linkTerms('Send money anywhere.', CITIES)
+
+ expect(parts).toEqual([{ text: 'Send money anywhere.' }])
+ })
+
+ it('drops a term that overlaps one already matched instead of nesting it', () => {
+ const parts = linkTerms('New York', [
+ { aliases: ['New York'], href: '/en/united-states' },
+ { aliases: ['York'], href: '/en/united-kingdom' },
+ ])
+
+ expect(parts).toEqual([{ text: 'New York', href: '/en/united-states' }])
+ })
+})
diff --git a/src/components/LandingPage/faq.tsx b/src/components/LandingPage/faq.tsx
index 7d4923068f..9ac54ba6f3 100644
--- a/src/components/LandingPage/faq.tsx
+++ b/src/components/LandingPage/faq.tsx
@@ -12,7 +12,7 @@ type LocalFAQsProps = FAQsProps & {
}
}
-export function FAQs({ heading, questions, marquee = { visible: false } }: LocalFAQsProps) {
+export function FAQs({ heading, questions, learnMoreLabel, marquee = { visible: false } }: LocalFAQsProps) {
return (
-
+
{marquee.visible && (
) => void
}
+
+/**
+ * Copy for the problem fold ( ): one heading over three named
+ * cards. Each card is a separate title/body pair rather than one list string,
+ * so a translator can never collapse three cards into two.
+ */
+export interface LandingProblemStrings {
+ heading: string
+ crossBorderTitle: string
+ crossBorderBody: string
+ sendHomeTitle: string
+ sendHomeBody: string
+ paidAbroadTitle: string
+ paidAbroadBody: string
+}
diff --git a/src/components/LandingPage/landingLinks.utils.ts b/src/components/LandingPage/landingLinks.utils.ts
new file mode 100644
index 0000000000..88313d8f85
--- /dev/null
+++ b/src/components/LandingPage/landingLinks.utils.ts
@@ -0,0 +1,49 @@
+/** One term to link, with every spelling it takes across the landing catalogs. */
+export interface LinkedTerm {
+ aliases: string[]
+ href: string
+}
+
+/** A run of the original text. `href` is set only on the matched terms. */
+export interface LinkedTextPart {
+ text: string
+ href?: string
+}
+
+/**
+ * Split a translated sentence around known terms so each match can be wrapped
+ * in a link at render time.
+ *
+ * The sentence stays one catalog string, so no locale loses its translation to
+ * a restructure. A term whose aliases are all absent from `text` is simply not
+ * matched, and that part of the sentence renders as plain text — a translator
+ * rewording "Mexico City" costs a link, never the sentence.
+ */
+export function linkTerms(text: string, terms: LinkedTerm[]): LinkedTextPart[] {
+ const matches: Array<{ start: number; end: number; href: string }> = []
+
+ for (const term of terms) {
+ // longest alias first: "Madrid" must win over its own prefix "Madri"
+ for (const alias of [...term.aliases].sort((a, b) => b.length - a.length)) {
+ const start = text.indexOf(alias)
+ if (start === -1) continue
+ matches.push({ start, end: start + alias.length, href: term.href })
+ break
+ }
+ }
+
+ matches.sort((a, b) => a.start - b.start)
+
+ const parts: LinkedTextPart[] = []
+ let cursor = 0
+ for (const match of matches) {
+ // a term overlapping one already taken is dropped rather than nested
+ if (match.start < cursor) continue
+ if (match.start > cursor) parts.push({ text: text.slice(cursor, match.start) })
+ parts.push({ text: text.slice(match.start, match.end), href: match.href })
+ cursor = match.end
+ }
+ if (cursor < text.length) parts.push({ text: text.slice(cursor) })
+
+ return parts
+}
diff --git a/src/components/LandingPage/landingStrings.ts b/src/components/LandingPage/landingStrings.ts
index 43303ea7ac..07d6dfaa4b 100644
--- a/src/components/LandingPage/landingStrings.ts
+++ b/src/components/LandingPage/landingStrings.ts
@@ -1,5 +1,6 @@
import en from '@/i18n/en.json'
import { type Translations } from '@/i18n/types'
+import type { LandingProblemStrings } from './landing.types'
// Narrowed copy bag handed from the server landing page down through
// LandingPageClient. Keeps the client bundle from importing all locale
@@ -13,6 +14,7 @@ export interface LandingStrings {
heroNoLocalId: string
zeroFees: string
seeMarkupOn: string
+ learnMore: string
dropLinkHeading: string
dropLinkBody: string
currencyPlaceholder: string
@@ -25,6 +27,7 @@ export interface LandingStrings {
cardCta: string
wallOfLove: string
wallOfLoveBody: string
+ problem: LandingProblemStrings
/** Passed straight through to ExchangeRateWidget's `labels`. */
exchange: {
youSend: string
@@ -49,6 +52,7 @@ export function landingStrings(i18n: Translations): LandingStrings {
heroNoLocalId: i18n.landingHeroNoLocalId,
zeroFees: i18n.landingZeroFees,
seeMarkupOn: i18n.landingSeeMarkupOn,
+ learnMore: i18n.landingLearnMore,
dropLinkHeading: i18n.landingDropLinkHeading,
dropLinkBody: i18n.landingDropLinkBody,
currencyPlaceholder: i18n.landingCurrencyPlaceholder,
@@ -61,6 +65,15 @@ export function landingStrings(i18n: Translations): LandingStrings {
cardCta: i18n.landingCardCta,
wallOfLove: i18n.landingWallOfLove,
wallOfLoveBody: i18n.landingWallOfLoveBody,
+ problem: {
+ heading: i18n.landingProblemHeading,
+ crossBorderTitle: i18n.landingProblemCrossBorderTitle,
+ crossBorderBody: i18n.landingProblemCrossBorderBody,
+ sendHomeTitle: i18n.landingProblemSendHomeTitle,
+ sendHomeBody: i18n.landingProblemSendHomeBody,
+ paidAbroadTitle: i18n.landingProblemPaidAbroadTitle,
+ paidAbroadBody: i18n.landingProblemPaidAbroadBody,
+ },
exchange: {
youSend: i18n.exchangeYouSend,
recipientGets: i18n.exchangeRecipientGets,
diff --git a/src/components/LandingPage/marquee.tsx b/src/components/LandingPage/marquee.tsx
index 563e0fed6d..84b8ee17e0 100644
--- a/src/components/LandingPage/marquee.tsx
+++ b/src/components/LandingPage/marquee.tsx
@@ -1,9 +1,11 @@
import HandThumbsUp from '@/assets/illustrations/hand-thumbs-up.svg'
import { MarqueeComp } from '../Global/MarqueeWrapper'
+import type { MarqueeItem } from '../Global/MarqueeWrapper/marquee.types'
type MarqueeProps = {
visible?: boolean
- message?: string[]
+ /** Plain words, or `{ label, href }` for the ones that link somewhere. */
+ message?: MarqueeItem[]
imageSrc?: string
backgroundColor?: string
}
diff --git a/src/components/LandingPage/securityBuiltIn.tsx b/src/components/LandingPage/securityBuiltIn.tsx
index cad2afb8a3..a75534449a 100644
--- a/src/components/LandingPage/securityBuiltIn.tsx
+++ b/src/components/LandingPage/securityBuiltIn.tsx
@@ -91,12 +91,16 @@ export function SecurityBuiltIn({ locale = DEFAULT_LOCALE }: { locale?: Locale }
{feature.description}
{feature.learnMoreHref && (
-
- {i18n.landingLearnMore} →
-
+ // max-w matches the paragraph above, so the link
+ // lands on that right edge instead of the wider cell
+
+
+ {i18n.landingLearnMore} →
+
+
)}
{feature.id === 3 && (
diff --git a/src/components/LandingPage/yourMoney.tsx b/src/components/LandingPage/yourMoney.tsx
index 6dca98519d..68b73a2941 100644
--- a/src/components/LandingPage/yourMoney.tsx
+++ b/src/components/LandingPage/yourMoney.tsx
@@ -1,11 +1,23 @@
import Image from 'next/image'
+import Link from 'next/link'
import LandingCountries from '@/assets/illustrations/landing-countries.svg'
import { Button } from '@/components/0_Bruddle/Button'
import { getTranslations } from '@/i18n'
import { DEFAULT_LOCALE, type Locale } from '@/i18n/types'
+import { linkTerms, type LinkedTerm } from './landingLinks.utils'
+
+// The three cities named in landingGlobalCashBody, each pointing at its country
+// page. Aliases cover every spelling the catalogs use, so the line stays one
+// translated string per locale.
+const cityTerms = (locale: Locale): LinkedTerm[] => [
+ { aliases: ['New York', 'Nueva York', 'Nova York'], href: `/${locale}/united-states` },
+ { aliases: ['Madrid', 'Madri'], href: `/${locale}/spain` },
+ { aliases: ['Mexico City', 'Ciudad de México', 'Cidade do México'], href: `/${locale}/mexico` },
+]
export function YourMoney({ locale = DEFAULT_LOCALE }: { locale?: Locale }) {
const i18n = getTranslations(locale)
+ const bodyParts = linkTerms(i18n.landingGlobalCashBody, cityTerms(locale))
return (
@@ -21,7 +33,15 @@ export function YourMoney({ locale = DEFAULT_LOCALE }: { locale?: Locale }) {
- {i18n.landingGlobalCashBody}
+ {bodyParts.map((part, index) =>
+ part.href ? (
+
+ {part.text}
+
+ ) : (
+ part.text
+ )
+ )}
diff --git a/src/config/underMaintenance.config.ts b/src/config/underMaintenance.config.ts
index 99feac8e71..144ed322c2 100644
--- a/src/config/underMaintenance.config.ts
+++ b/src/config/underMaintenance.config.ts
@@ -29,9 +29,9 @@
*
* 6. disableCardPioneers: hides the card pioneers waitlist feature entirely
* - /card page redirects to /home
- * - /lp/card marketing page redirects to /
- * - card pioneers section hidden from landing page
+ * - /lp/card redirects to /shhhhh (redirects.json) — the marketing page itself is gone
* - card pioneer modal, carousel cta, and perk rewards hidden from home
+ * - the landing-page card section it used to hide was removed in 2026-08; use disableLandingCardFold for the new one
* - set to false to enable the feature
*
* 7. pixBrazilOnrampMaintenance: warn-only flag for the BRL-via-PIX onramp (Manteca Brazil deposit)
@@ -51,6 +51,11 @@
* - use during a partial Manteca outage so recovered currencies (e.g. ARS) come back while others stay blocked
* - does NOT touch QR payments (Manteca QR / Brazil PIX-over-QR stay open) — that is disabledPaymentProviders
*
+ * 10. disableLandingCardFold: hides the "shhhhh" card fold on the landing page
+ * - removes the black door fold and the closed-beta marquee strip under it
+ * - /shhhhh and the rest of the card flow stay reachable — this only mutes the homepage pitch
+ * - use if the closed beta fills up or the card goes down
+ *
* note: if either mode is enabled, the maintenance banner will show everywhere
*
* I HOPE WE NEVER NEED TO USE THIS...
@@ -67,6 +72,7 @@ interface MaintenanceConfig {
disableXchainSend: boolean
disableCardPioneers: boolean
disableCardLaunchCTA: boolean
+ disableLandingCardFold: boolean
pixBrazilOnrampMaintenance: boolean
/** Manteca fiat currencies still down (e.g. ['BRL']); currencies not listed stay live. Empty = all enabled. */
disabledMantecaCurrencies: MantecaCurrency[]
@@ -83,6 +89,7 @@ const underMaintenanceConfig: MaintenanceConfig = {
disableXchainSend: true, // set to true to disable cross-chain sends (claim, request payments - only allows USDC on Arbitrum)
disableCardPioneers: true, // set to false to enable the Card Pioneers waitlist feature
disableCardLaunchCTA: false, // kill-switch for the in-app "shhh" card CTA (funnel card step + activated home splash). Set true to mute it (dial down in-app load); /card flow + /shhhhh + waitlist stay reachable regardless.
+ disableLandingCardFold: false, // set to true to hide the landing-page card fold (black door fold + the closed-beta strip under it)
pixBrazilOnrampMaintenance: false, // BRL deposits restored via dynamic PIX QR (2026-07-02). Set true if the onramp degrades again.
disabledMantecaCurrencies: [], // Manteca restored (ARS + BRL live). Add a currency here to block it during a future outage.
}
diff --git a/src/i18n/app/__tests__/shhhhh-catalog.test.ts b/src/i18n/app/__tests__/shhhhh-catalog.test.ts
index 93fdfae1f9..b5c299d29b 100644
--- a/src/i18n/app/__tests__/shhhhh-catalog.test.ts
+++ b/src/i18n/app/__tests__/shhhhh-catalog.test.ts
@@ -14,7 +14,7 @@ describe('/shhhhh catalog', () => {
it.each(APP_LOCALES)('%s keeps the tag the hero interpolates', async (locale) => {
const messages = await loadMessages(locale)
// t.rich renders into this tag; drop it in a translation
- // and the "only 20 a week" scarcity beat silently disappears.
+ // and the "only 50 a week" scarcity beat silently disappears.
expect(messages.shhhhh.hero.body).toContain(' ')
})
diff --git a/src/i18n/app/messages/en.json b/src/i18n/app/messages/en.json
index b564cd4863..8a2661e836 100644
--- a/src/i18n/app/messages/en.json
+++ b/src/i18n/app/messages/en.json
@@ -2867,6 +2867,7 @@
"onlyCount": "only {count}",
"tryTheDoor": "TRY THE DOOR",
"orJoinWaitlist": "or join the waitlist →",
+ "disclaimer": "Verification required for all cardholders. No monthly or annual fees. Standard fees and limits apply per the cardholder terms. Beta programme.",
"joinError": "Couldn't join the waitlist — try the door again."
},
"waitlist": {
@@ -2901,7 +2902,7 @@
"badgeBody": "You skip the line. We're whitelisting holders of:",
"badgeFooter": "Sign in, we check the badges on your account, you go straight to verification. No peanut account yet? Sign up first — takes a minute.",
"noBadgeTitle": "No badge.",
- "noBadgeBody": "Join the waitlist. We let people in 20 a week.",
+ "noBadgeBody": "Join the waitlist. We let people in 50 a week.",
"noBadgeFooter": "Put your name on the list and we'll holler when your turn comes up."
},
"notForYou": {
@@ -2929,7 +2930,7 @@
},
"q5": {
"question": "HOW LONG IS THE WAITLIST?",
- "answer": "We're letting in about 20 people a week during closed beta. Order matters. Badges skip the line entirely."
+ "answer": "We're letting in about 50 people a week during closed beta. Order matters. Badges skip the line entirely."
},
"q6": {
"question": "WHERE CAN I GET THE CARD?",
diff --git a/src/i18n/app/messages/es-419.json b/src/i18n/app/messages/es-419.json
index 26a71bb45f..03417d76b9 100644
--- a/src/i18n/app/messages/es-419.json
+++ b/src/i18n/app/messages/es-419.json
@@ -2867,6 +2867,7 @@
"onlyCount": "solo {count}",
"tryTheDoor": "PRUEBA LA PUERTA",
"orJoinWaitlist": "o únete a la lista de espera →",
+ "disclaimer": "Verificación requerida para todos los titulares de tarjeta. Sin cuotas mensuales ni anuales. Se aplican tarifas y límites estándar según los términos del titular. Programa beta.",
"joinError": "No pudimos sumarte a la lista de espera. Prueba la puerta de nuevo."
},
"waitlist": {
@@ -2901,7 +2902,7 @@
"badgeBody": "Te saltas la fila. Estamos habilitando a quienes tengan:",
"badgeFooter": "Inicia sesión, revisamos las insignias de tu cuenta y pasas directo a la verificación. ¿Todavía no tienes cuenta de peanut? Regístrate primero: toma un minuto.",
"noBadgeTitle": "Sin insignia.",
- "noBadgeBody": "Únete a la lista de espera. Dejamos entrar a 20 personas por semana.",
+ "noBadgeBody": "Únete a la lista de espera. Dejamos entrar a 50 personas por semana.",
"noBadgeFooter": "Anota tu nombre en la lista y te avisamos cuando llegue tu turno."
},
"notForYou": {
@@ -2929,7 +2930,7 @@
},
"q5": {
"question": "¿CUÁNTO DURA LA LISTA DE ESPERA?",
- "answer": "Durante la beta cerrada dejamos entrar a unas 20 personas por semana. El orden importa. Las insignias saltan la fila por completo."
+ "answer": "Durante la beta cerrada dejamos entrar a unas 50 personas por semana. El orden importa. Las insignias saltan la fila por completo."
},
"q6": {
"question": "¿DÓNDE PUEDO CONSEGUIR LA TARJETA?",
diff --git a/src/i18n/app/messages/es-AR.json b/src/i18n/app/messages/es-AR.json
index 06f93c9539..bc7aa07c9b 100644
--- a/src/i18n/app/messages/es-AR.json
+++ b/src/i18n/app/messages/es-AR.json
@@ -1210,6 +1210,7 @@
"body": "Una tarjeta aceptada en más de 150 millones de comercios que aceptan Visa. Vamos dejando entrar a los usuarios beta de a poco: por semana. Si tenés la insignia correcta, te saltás la fila por completo.",
"tryTheDoor": "PROBÁ LA PUERTA",
"orJoinWaitlist": "o sumate a la lista de espera →",
+ "disclaimer": "Verificación requerida para todos los titulares de tarjeta. Sin cuotas mensuales ni anuales. Se aplican tarifas y límites estándar según los términos del titular. Programa beta.",
"joinError": "No pudimos sumarte a la lista de espera. Probá la puerta de nuevo."
},
"whatItDoes": {
@@ -1222,7 +1223,7 @@
"badgeTitle": "Tenés una insignia.",
"badgeBody": "Te saltás la fila. Estamos habilitando a quienes tengan:",
"badgeFooter": "Iniciá sesión, revisamos las insignias de tu cuenta y pasás directo a la verificación. ¿Todavía no tenés cuenta de peanut? Registrate primero: toma un minuto.",
- "noBadgeBody": "Sumate a la lista de espera. Dejamos entrar a 20 personas por semana.",
+ "noBadgeBody": "Sumate a la lista de espera. Dejamos entrar a 50 personas por semana.",
"noBadgeFooter": "Anotá tu nombre en la lista y te avisamos cuando llegue tu turno."
},
"notForYou": {
diff --git a/src/i18n/app/messages/pt-BR.json b/src/i18n/app/messages/pt-BR.json
index d40c75d675..d73a7990f4 100644
--- a/src/i18n/app/messages/pt-BR.json
+++ b/src/i18n/app/messages/pt-BR.json
@@ -2867,6 +2867,7 @@
"onlyCount": "só {count}",
"tryTheDoor": "TESTE A PORTA",
"orJoinWaitlist": "ou entre na lista de espera →",
+ "disclaimer": "Verificação obrigatória para todos os titulares do cartão. Sem tarifas mensais ou anuais. Tarifas e limites padrão se aplicam conforme os termos do titular. Programa beta.",
"joinError": "Não conseguimos te colocar na lista de espera. Teste a porta de novo."
},
"waitlist": {
@@ -2901,7 +2902,7 @@
"badgeBody": "Você fura a fila. Estamos liberando quem tiver:",
"badgeFooter": "Entre, conferimos os selos da sua conta e você vai direto para a verificação. Ainda não tem conta no peanut? Crie uma primeiro — leva um minuto.",
"noBadgeTitle": "Sem selo.",
- "noBadgeBody": "Entre na lista de espera. Liberamos 20 pessoas por semana.",
+ "noBadgeBody": "Entre na lista de espera. Liberamos 50 pessoas por semana.",
"noBadgeFooter": "Coloque seu nome na lista e a gente avisa quando chegar sua vez."
},
"notForYou": {
@@ -2929,7 +2930,7 @@
},
"q5": {
"question": "QUANTO TEMPO DEMORA A LISTA DE ESPERA?",
- "answer": "Durante o beta fechado, liberamos cerca de 20 pessoas por semana. A ordem importa. Os selos furam a fila por completo."
+ "answer": "Durante o beta fechado, liberamos cerca de 50 pessoas por semana. A ordem importa. Os selos furam a fila por completo."
},
"q6": {
"question": "ONDE POSSO CONSEGUIR O CARTÃO?",
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 282ce88606..aff22d0cbb 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -69,7 +69,6 @@
"landingPayLocalHeading": "PAY LIKE A LOCAL.",
"landingPayLocalSubheading": "RECEIVE FROM ANYWHERE. NO LOCAL ID NEEDED.",
"landingPayLocalBody": "Pay MercadoPago QR in Argentina. Send PIX in Brazil. Just your passport.",
- "landingPayLocalSettles": "Settles in digital dollars at the real exchange rate.",
"landingMercadoPagoAria": "How MercadoPago QR works",
"landingPixAria": "How PIX works without a CPF",
"landingSecurityHeading": "SECURITY. CONTROL. SUPPORT.",
@@ -78,7 +77,7 @@
"landingSecurityControlTitle": "TRUE CONTROL",
"landingSecurityControlDesc": "You verify your identity only when a feature truly requires it — like bank connections or the Peanut Card. For everyday peer-to-peer use, no mandatory KYC or friction.",
"landingSecurityHelpTitle": "24/7 HELP",
- "landingSecurityHelpDesc": "One tap connects you to real humans. Friendly, expert support is on hand day or night to resolve any question instantly.",
+ "landingSecurityHelpDesc": "One tap and support is always available. Friendly, expert support is on hand day or night to resolve any question instantly.",
"landingTalkToSupport": "Talk to Support",
"landingSendTagline1": "MOVE MONEY WORLDWIDE INSTANTLY.",
"landingSendTagline2": "ALWAYS UNDER YOUR CONTROL.",
@@ -89,7 +88,7 @@
"footerTerms": "Terms",
"footerPrivacy": "Privacy",
"footerSecurity": "Security",
- "footerJobs": "Jobs",
+ "footerJobs": "Career",
"footerSiteDirectory": "Site directory",
"footerCompare": "Compare",
"footerLearnMoreSection": "Learn More",
@@ -115,6 +114,13 @@
"landingCardCta": "GET MY CARD",
"landingWallOfLove": "WALL OF LOVE",
"landingWallOfLoveBody": "See what people are saying about Peanut on",
+ "landingProblemHeading": "You already know the problem.",
+ "landingProblemCrossBorderTitle": "Living across borders",
+ "landingProblemCrossBorderBody": "Bounced by every neobank's verification the moment you cross a border.",
+ "landingProblemSendHomeTitle": "Sending money home",
+ "landingProblemSendHomeBody": "Paying 4% on every transaction home.",
+ "landingProblemPaidAbroadTitle": "Paid from abroad",
+ "landingProblemPaidAbroadBody": "You earn in one currency and spend in another.",
"faqTitle": "FAQ",
"heroMarqueeNoFees": "No fees",
"heroMarqueeInstant": "Instant",
diff --git a/src/i18n/es-419.json b/src/i18n/es-419.json
index 61eaedf4df..6cd302cbd9 100644
--- a/src/i18n/es-419.json
+++ b/src/i18n/es-419.json
@@ -69,7 +69,6 @@
"landingPayLocalHeading": "PAGA COMO LOCAL.",
"landingPayLocalSubheading": "RECIBE DESDE CUALQUIER LUGAR. SIN DOCUMENTO LOCAL.",
"landingPayLocalBody": "Paga con QR de Mercado Pago en Argentina. Envía PIX en Brasil. Solo con tu pasaporte.",
- "landingPayLocalSettles": "Se liquida en dólares digitales al tipo de cambio real.",
"landingMercadoPagoAria": "Cómo funciona el QR de Mercado Pago",
"landingPixAria": "Cómo funciona PIX sin CPF",
"landingSecurityHeading": "SEGURIDAD. CONTROL. SOPORTE.",
@@ -78,7 +77,7 @@
"landingSecurityControlTitle": "CONTROL REAL",
"landingSecurityControlDesc": "Verificas tu identidad solo cuando una función realmente lo exige, como conectar tu banco o la Peanut Card. Para el uso diario entre personas, sin KYC obligatorio ni fricción.",
"landingSecurityHelpTitle": "AYUDA 24/7",
- "landingSecurityHelpDesc": "Con un toque hablas con personas reales. Soporte experto y amable, de día o de noche, para resolver cualquier duda al instante.",
+ "landingSecurityHelpDesc": "Con un toque siempre tienes soporte disponible. Soporte experto y amable, de día o de noche, para resolver cualquier duda al instante.",
"landingTalkToSupport": "Hablar con soporte",
"landingSendTagline1": "MUEVE DINERO POR TODO EL MUNDO AL INSTANTE.",
"landingSendTagline2": "SIEMPRE BAJO TU CONTROL.",
@@ -115,6 +114,13 @@
"landingCardCta": "QUIERO MI TARJETA",
"landingWallOfLove": "MURO DE AMOR",
"landingWallOfLoveBody": "Mira lo que dicen de Peanut en",
+ "landingProblemHeading": "You already know the problem.",
+ "landingProblemCrossBorderTitle": "Living across borders",
+ "landingProblemCrossBorderBody": "Bounced by every neobank's verification the moment you cross a border.",
+ "landingProblemSendHomeTitle": "Sending money home",
+ "landingProblemSendHomeBody": "Paying 4% on every transaction home.",
+ "landingProblemPaidAbroadTitle": "Paid from abroad",
+ "landingProblemPaidAbroadBody": "You earn in one currency and spend in another.",
"faqTitle": "Preguntas frecuentes",
"heroMarqueeNoFees": "Sin comisiones",
"heroMarqueeInstant": "Al instante",
diff --git a/src/i18n/es-ar.json b/src/i18n/es-ar.json
index 46903f4eea..c32db87190 100644
--- a/src/i18n/es-ar.json
+++ b/src/i18n/es-ar.json
@@ -69,7 +69,6 @@
"landingPayLocalHeading": "PAGÁ COMO LOCAL.",
"landingPayLocalSubheading": "RECIBÍ DESDE CUALQUIER LUGAR. SIN DOCUMENTO LOCAL.",
"landingPayLocalBody": "Pagá con QR de Mercado Pago en Argentina. Enviá PIX en Brasil. Solo con tu pasaporte.",
- "landingPayLocalSettles": "Se liquida en dólares digitales al tipo de cambio real.",
"landingMercadoPagoAria": "Cómo funciona el QR de Mercado Pago",
"landingPixAria": "Cómo funciona PIX sin CPF",
"landingSecurityHeading": "SEGURIDAD. CONTROL. SOPORTE.",
@@ -78,7 +77,7 @@
"landingSecurityControlTitle": "CONTROL REAL",
"landingSecurityControlDesc": "Verificás tu identidad solo cuando una función realmente lo exige, como conectar tu banco o la Peanut Card. Para el uso diario entre personas, sin KYC obligatorio ni fricción.",
"landingSecurityHelpTitle": "AYUDA 24/7",
- "landingSecurityHelpDesc": "Con un toque hablás con personas reales. Soporte experto y amable, de día o de noche, para resolver cualquier duda al instante.",
+ "landingSecurityHelpDesc": "Con un toque siempre tenés soporte disponible. Soporte experto y amable, de día o de noche, para resolver cualquier duda al instante.",
"landingTalkToSupport": "Hablar con soporte",
"landingSendTagline1": "MOVÉ PLATA POR TODO EL MUNDO AL INSTANTE.",
"landingSendTagline2": "SIEMPRE BAJO TU CONTROL.",
@@ -115,6 +114,13 @@
"landingCardCta": "QUIERO MI TARJETA",
"landingWallOfLove": "MURO DE AMOR",
"landingWallOfLoveBody": "Mirá lo que dicen de Peanut en",
+ "landingProblemHeading": "You already know the problem.",
+ "landingProblemCrossBorderTitle": "Living across borders",
+ "landingProblemCrossBorderBody": "Bounced by every neobank's verification the moment you cross a border.",
+ "landingProblemSendHomeTitle": "Sending money home",
+ "landingProblemSendHomeBody": "Paying 4% on every transaction home.",
+ "landingProblemPaidAbroadTitle": "Paid from abroad",
+ "landingProblemPaidAbroadBody": "You earn in one currency and spend in another.",
"faqTitle": "Preguntas frecuentes",
"heroMarqueeNoFees": "Sin comisiones",
"heroMarqueeInstant": "Al instante",
diff --git a/src/i18n/pt-br.json b/src/i18n/pt-br.json
index 82ad1c4fcf..1b6f8b67c4 100644
--- a/src/i18n/pt-br.json
+++ b/src/i18n/pt-br.json
@@ -69,7 +69,6 @@
"landingPayLocalHeading": "PAGUE COMO UM LOCAL.",
"landingPayLocalSubheading": "RECEBA DE QUALQUER LUGAR. SEM DOCUMENTO LOCAL.",
"landingPayLocalBody": "Pague com QR do Mercado Pago na Argentina. Envie Pix no Brasil. Só com seu passaporte.",
- "landingPayLocalSettles": "A liquidação é em dólares digitais pela cotação real.",
"landingMercadoPagoAria": "Como funciona o QR do Mercado Pago",
"landingPixAria": "Como funciona o Pix sem CPF",
"landingSecurityHeading": "SEGURANÇA. CONTROLE. SUPORTE.",
@@ -78,7 +77,7 @@
"landingSecurityControlTitle": "CONTROLE DE VERDADE",
"landingSecurityControlDesc": "Você verifica sua identidade só quando um recurso realmente exige — como conectar o banco ou o Peanut Card. No uso diário entre pessoas, sem KYC obrigatório nem burocracia.",
"landingSecurityHelpTitle": "AJUDA 24/7",
- "landingSecurityHelpDesc": "Com um toque você fala com pessoas de verdade. Suporte especializado e atencioso, de dia ou de noite, para resolver qualquer dúvida na hora.",
+ "landingSecurityHelpDesc": "Com um toque o suporte está sempre disponível. Suporte especializado e atencioso, de dia ou de noite, para resolver qualquer dúvida na hora.",
"landingTalkToSupport": "Falar com o suporte",
"landingSendTagline1": "MOVIMENTE DINHEIRO PELO MUNDO NA HORA.",
"landingSendTagline2": "SEMPRE SOB SEU CONTROLE.",
@@ -115,6 +114,13 @@
"landingCardCta": "QUERO MEU CARTÃO",
"landingWallOfLove": "MURAL DE AMOR",
"landingWallOfLoveBody": "Veja o que as pessoas dizem do Peanut no",
+ "landingProblemHeading": "You already know the problem.",
+ "landingProblemCrossBorderTitle": "Living across borders",
+ "landingProblemCrossBorderBody": "Bounced by every neobank's verification the moment you cross a border.",
+ "landingProblemSendHomeTitle": "Sending money home",
+ "landingProblemSendHomeBody": "Paying 4% on every transaction home.",
+ "landingProblemPaidAbroadTitle": "Paid from abroad",
+ "landingProblemPaidAbroadBody": "You earn in one currency and spend in another.",
"faqTitle": "Perguntas frequentes",
"heroMarqueeNoFees": "Sem taxas",
"heroMarqueeInstant": "Na hora",
diff --git a/src/i18n/types.ts b/src/i18n/types.ts
index f8714e82d4..5dbcbcccfe 100644
--- a/src/i18n/types.ts
+++ b/src/i18n/types.ts
@@ -97,7 +97,6 @@ export interface Translations {
landingPayLocalHeading: string
landingPayLocalSubheading: string
landingPayLocalBody: string
- landingPayLocalSettles: string
landingMercadoPagoAria: string
landingPixAria: string
@@ -145,6 +144,15 @@ export interface Translations {
landingWallOfLove: string
landingWallOfLoveBody: string
+ // Landing page — problem fold (three named cards, icons drawn by the component)
+ landingProblemHeading: string
+ landingProblemCrossBorderTitle: string
+ landingProblemCrossBorderBody: string
+ landingProblemSendHomeTitle: string
+ landingProblemSendHomeBody: string
+ landingProblemPaidAbroadTitle: string
+ landingProblemPaidAbroadBody: string
+
// Legal page headers (privacy/terms carry no in their verbatim markdown)
legalHeroSubtitlePrivacy: string
legalHeroSubtitleTerms: string