Enhance About page with updated help cards, improved sharing functionality, growth visualization, and refined translations

This commit is contained in:
MartinBraquet
2026-07-23 17:32:56 +02:00
parent 8d66e324e8
commit f168ce2466
5 changed files with 353 additions and 104 deletions

View File

@@ -47,7 +47,20 @@
"about.seo.title": "Über uns",
"about.settings.copied": "Kopiert!",
"about.settings.copy_info": "Infos kopieren",
"about.share.benefit.events.text": "Mehr Menschen in der Nähe bedeuten bessere Treffen und Veranstaltungen.",
"about.share.benefit.events.title": "Reichere Events",
"about.share.benefit.free.text": "Mehr Beitragende halten Compass werbefrei und ohne Bezahlschranke.",
"about.share.benefit.free.title": "Für immer kostenlos",
"about.share.benefit.match.text": "Jede Person, die beitritt, erhöht die Chance, dass die Person, die Sie suchen, schon hier ist.",
"about.share.benefit.match.title": "Bessere Chancen auf ein Match",
"about.share.benefit.people.text": "Ein größerer Pool an Menschen, die Ihre Werte wirklich teilen.",
"about.share.benefit.people.title": "Mehr Gleichgesinnte",
"about.share.button": "Teilen",
"about.share.button_cta": "Compass teilen",
"about.share.copied": "Link kopiert!",
"about.share.headline": "Compass wird für Sie mit jeder Person besser, die Sie mitbringen.",
"about.share.kicker": "Ein Teilen, eine Person — so entsteht eine Gemeinschaft wie diese.",
"about.share.reframe": "Selbst wenn ein Freund nicht Ihr Match ist, bringt er seine Welt mit — seine Freunde, seine Kreise, die nachdenklichen Menschen, denen Sie sonst nie begegnet wären. Teilen ist kein Gefallen an ihn. Es ist eine Investition in Ihre eigenen künftigen Verbindungen.",
"about.share.text": "Durchdachte Eins-zu-eins-Verbindungen, offen entwickelt.",
"about.share.title": "Compass",
"about.subtitle": "Finden Sie Ihre Leute ganz einfach — basierend auf wer sie sind, nicht wie sie aussehen.",
@@ -64,6 +77,7 @@
"about.eyebrow": "Über Compass",
"about.features.label": "Was uns unterscheidet",
"about.growth.label": "Mitglieder, Tendenz steigend",
"about.growth.recent": "+{count} diesen Monat",
"about.growth.today": "Heute",
"about.help.label": "Helfen Sie Compass zu wachsen",
"about.mission.label": "Warum es uns gibt",

View File

@@ -47,7 +47,20 @@
"about.seo.title": "À propos",
"about.settings.copied": "Copié !",
"about.settings.copy_info": "Copier les infos",
"about.share.benefit.events.text": "Plus de monde à proximité, ce sont de meilleures rencontres et rassemblements.",
"about.share.benefit.events.title": "Des événements plus riches",
"about.share.benefit.free.text": "Plus de contributeurs gardent Compass sans pub ni abonnement.",
"about.share.benefit.free.title": "Gratuit, pour toujours",
"about.share.benefit.match.text": "Chaque nouvelle personne augmente les chances que celle que tu cherches soit déjà là.",
"about.share.benefit.match.title": "De meilleures chances de rencontre",
"about.share.benefit.people.text": "Un plus grand vivier de personnes qui partagent vraiment tes valeurs.",
"about.share.benefit.people.title": "Plus de personnes pour vous",
"about.share.button": "Partager",
"about.share.button_cta": "Partager Compass",
"about.share.copied": "Lien copié !",
"about.share.headline": "Compass s'améliore pour toi à chaque personne que tu invites.",
"about.share.kicker": "Un partage, une personne — c'est ainsi qu'une communauté comme celle-ci se construit.",
"about.share.reframe": "Même si un ami n'est pas ton match, il amène son monde avec lui — ses amis, ses cercles, les personnes réfléchies que tu n'aurais jamais rencontrées autrement. Partager n'est pas un service que tu lui rends. C'est un investissement dans tes propres connexions futures.",
"about.share.text": "Des connexions authentiques en tête-à-tête, construites en toute transparence.",
"about.share.title": "Compass",
"about.subtitle": "Trouve tes personnes en toute simplicité — selon qui ils sont, pas selon leur apparence.",
@@ -64,6 +77,7 @@
"about.eyebrow": "À propos de Compass",
"about.features.label": "Ce qui nous différencie",
"about.growth.label": "membres, et ça continue",
"about.growth.recent": "+{count} ce mois-ci",
"about.growth.today": "Aujourd'hui",
"about.help.label": "Aide Compass à grandir",
"about.mission.label": "Pourquoi nous existons",

View File

@@ -119,7 +119,7 @@ export function VoteEvidence() {
<p className="text-sm text-ink-600 leading-relaxed max-w-md">
{t(
'about.vote.intro',
'Members propose changes, everyone votes, and the result is binding. This one added a step to signing up:',
'Members propose changes, everyone votes, and the result is binding. The one highlighted here added a step to signing up.',
)}
</p>

View File

@@ -1,4 +1,5 @@
import {useEffect, useState} from 'react'
import {ArrowTrendingUpIcon} from '@heroicons/react/24/outline'
import {useEffect, useRef, useState} from 'react'
import {
Area,
AreaChart,
@@ -182,10 +183,18 @@ function CustomLegend({payload}: any) {
* does, but it grows linearly — past a few thousand members this wants an aggregate endpoint
* returning daily totals instead.
*/
// How long the line takes to draw itself in on reveal. Reused as the delay before the endpoint dot
// fades in, so the dot lands exactly when the line reaches it rather than floating ahead of it.
const GROWTH_DRAW_MS = 1400
export function MemberGrowth() {
const t = useT()
const [data, setData] = useState<{dateTs: number; members: number}[]>([])
const [failed, setFailed] = useState(false)
// 'hidden' → <Area> not mounted; 'drawing' → recharts animates it in; 'done' → static, endpoint dot
// shown. Driven by an IntersectionObserver so the curve grows when the reader scrolls to it.
const [phase, setPhase] = useState<'hidden' | 'drawing' | 'done'>('hidden')
const figureRef = useRef<HTMLElement>(null)
useEffect(() => {
async function load() {
@@ -211,6 +220,30 @@ export function MemberGrowth() {
void load()
}, [])
// Draw the curve on reveal, not on data load — a line "growing" only reads as growth if the reader
// watches it happen. Re-armed when the data arrives (the figure, and so the ref, only mounts then).
// Reduced-motion visitors skip the draw and get the final curve immediately.
useEffect(() => {
const el = figureRef.current
if (!el || phase !== 'hidden') return
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
setPhase('done')
return
}
const obs = new IntersectionObserver(
(entries) => {
if (entries.some((e) => e.isIntersecting)) {
obs.disconnect()
setPhase('drawing')
window.setTimeout(() => setPhase('done'), GROWTH_DRAW_MS)
}
},
{rootMargin: '0px 0px -10% 0px'},
)
obs.observe(el)
return () => obs.disconnect()
}, [data.length, phase])
// Render nothing rather than a zero or a spinner: this is supporting evidence on a page that reads
// fine without it, and an empty chart frame claims more than it shows.
if (failed || !data.length) return null
@@ -219,33 +252,78 @@ export function MemberGrowth() {
const monthYear = (ts: number) =>
new Date(ts).toLocaleDateString('en-US', {month: 'short', year: 'numeric'})
// Trailing-30-day growth, shown as a momentum badge beside the total. Read from the real series (never
// hardcoded), and rendered only when positive — a flat or empty period simply omits it rather than
// showing "+0", the same "say nothing rather than something weak" rule the rest of the page follows.
const lastIndex = data.length - 1
const recent = total - data[Math.max(0, lastIndex - 30)].members
return (
<figure className="bg-canvas-50 border-[1.5px] border-canvas-200 rounded-2xl p-5 sm:p-7">
<div className="flex items-baseline gap-2 mb-[-20px] sm:mb-[-40px]">
<span className="text-3xl font-bold text-ink-1000">{total.toLocaleString()}</span>
<span className="text-sm text-ink-600">
{t('about.growth.label', 'members and counting')}
</span>
<figure
ref={figureRef}
className="bg-canvas-50 border-[1.5px] border-canvas-200 rounded-2xl p-5 sm:p-7"
>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1.5 mb-[-20px] sm:mb-[-40px]">
<div className="flex items-baseline gap-2">
<span className="text-3xl font-bold text-ink-1000">{total.toLocaleString()}</span>
<span className="text-sm text-ink-600">
{t('about.growth.label', 'members and counting')}
</span>
</div>
{recent > 0 && (
<span className="inline-flex items-center gap-1 rounded-full bg-primary-100 px-2 py-0.5 text-xs font-semibold text-primary-700 ring-1 ring-primary-200">
<ArrowTrendingUpIcon className="h-3.5 w-3.5" strokeWidth={2.2} />
{t('about.growth.recent', '+{count} this month', {count: recent})}
</span>
)}
</div>
<div className="h-[120px] sm:h-[140px] -mx-1">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data} margin={{top: 8, right: 4, bottom: 0, left: 4}}>
<AreaChart data={data} margin={{top: 8, right: 8, bottom: 0, left: 4}}>
<defs>
<linearGradient id="gradGrowth" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="rgb(193 127 62)" stopOpacity={0.22} />
<stop offset="5%" stopColor="rgb(193 127 62)" stopOpacity={0.24} />
<stop offset="95%" stopColor="rgb(193 127 62)" stopOpacity={0} />
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="members"
stroke="rgb(193 127 62)"
strokeWidth={2.5}
fill="url(#gradGrowth)"
dot={false}
isAnimationActive={false}
/>
{/* Mounted only once in view, so recharts draws it in from the left on reveal. Held out
entirely while 'hidden' rather than rendered invisibly — that keeps the animation from
firing off-screen before the reader ever sees it. */}
{phase !== 'hidden' && (
<Area
type="monotone"
dataKey="members"
stroke="rgb(193 127 62)"
strokeWidth={2.5}
strokeLinecap="round"
fill="url(#gradGrowth)"
isAnimationActive={phase === 'drawing'}
animationDuration={GROWTH_DRAW_MS}
animationEasing="ease-out"
activeDot={false}
// A marker on the last point only: the current value, haloed so the line reads as "still
// climbing, you are here". Amber-on-amber, so it needs no theme-dependent ring colour.
// Faded in only once the draw completes ('done'), so it never sits ahead of the line.
dot={(props: any) => {
const {cx, cy, index} = props
if (index !== lastIndex || cx == null || cy == null)
return <g key={`d-${index}`} />
return (
<g
key="end"
style={{
opacity: phase === 'done' ? 1 : 0,
transition: 'opacity 300ms ease-out',
}}
>
<circle cx={cx} cy={cy} r={8} fill="rgb(193 127 62)" opacity={0.16} />
<circle cx={cx} cy={cy} r={3.5} fill="rgb(193 127 62)" />
</g>
)
}}
/>
)}
</AreaChart>
</ResponsiveContainer>
</div>

View File

@@ -2,6 +2,7 @@ import {
BellIcon,
BookmarkIcon,
ChatBubbleLeftRightIcon,
CheckIcon,
CodeBracketIcon,
EnvelopeIcon,
FlagIcon,
@@ -12,12 +13,14 @@ import {
MegaphoneIcon,
ShareIcon,
SparklesIcon,
UserGroupIcon,
} from '@heroicons/react/24/outline'
import {GlobeAltIcon} from '@heroicons/react/24/solid'
import clsx from 'clsx'
import {discordLink, formLink, githubRepo} from 'common/constants'
import {DEPLOYED_WEB_URL} from 'common/envs/constants'
import {ComponentType, ReactNode, SVGProps, useEffect, useState} from 'react'
import Link from 'next/link'
import {ComponentType, ReactNode, SVGProps, useState} from 'react'
import {StatBand} from 'web/components/about/platform-stats'
import {RepoActivity} from 'web/components/about/repo-activity'
import {AlertDemo} from 'web/components/about/search-alert-demo'
@@ -29,8 +32,9 @@ import {SEO} from 'web/components/SEO'
import {MemberGrowth} from 'web/components/widgets/charts'
import {Reveal} from 'web/components/widgets/reveal'
import {eyebrow, Section, surface, surfaceHover} from 'web/components/widgets/surface'
import {useIsMobile} from 'web/hooks/use-is-mobile'
import {useUser} from 'web/hooks/use-user'
import {useT} from 'web/lib/locale'
import {copyToClipboard} from 'web/lib/util/copy'
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -260,12 +264,47 @@ function MissionStatement({title, text}: {title: string; text: string}) {
)
}
// ─── Help Card ───────────────────────────────────────────────────────────────
// ─── Help Cards ───────────────────────────────────────────────────────────────
function HelpCard({icon, title, text, buttonLabel, buttonUrl, buttonPrimary, id}: HelpCardProps) {
/**
* The lead of the "other ways to help" group. Contributing suggestions/help is the one we most want of
* the four, so it is the full-width horizontal card with a large icon and a *button* rather than a bare
* text link — but only an outline button, never a filled one. The filled amber CTA is spoken for by
* "Share Compass" one block up, which must stay the loudest thing here; a second filled CTA next to it
* out-shouts it. So the hierarchy is deliberately: filled Share outlined Suggest the three links.
*/
function FeaturedHelpCard({icon, title, text, buttonLabel, buttonUrl, id}: HelpCardProps) {
return (
<div
className={clsx(surface, surfaceHover, 'h-full flex flex-col p-6 sm:p-7')}
className={clsx(
surface,
surfaceHover,
'p-6 sm:p-8',
'flex flex-col gap-5 sm:flex-row sm:items-center sm:gap-7',
)}
>
<IconChip icon={icon} large />
<div className="min-w-0 flex-1">
<h3 id={id} className="font-bold text-lg text-ink-900 mb-1.5">
{title}
</h3>
<p className="text-sm text-ink-600 leading-relaxed max-w-xl">{text}</p>
</div>
<div className="shrink-0 -mb-3 -ml-3 sm:m-0">
<GeneralButton
url={buttonUrl}
content={buttonLabel}
color={'bg-transparent text-cta border-cta hover:bg-cta hover:text-white'}
/>
</div>
</div>
)
}
function HelpCard({icon, title, text, buttonLabel, buttonUrl, id}: HelpCardProps) {
return (
<div
className={clsx(surface, surfaceHover, 'flex h-full flex-col p-5 sm:p-6')}
// NOTE: Abandoned the left accent bar due to a known Firefox rendering bug.
// Firefox fails to correctly apply overflow-hidden on rounded containers with borders,
// causing the absolute/flex-item to bleed past the corner radius.
@@ -278,20 +317,17 @@ function HelpCard({icon, title, text, buttonLabel, buttonUrl, buttonPrimary, id}
{title}
</h3>
<p className="text-sm text-ink-600 leading-relaxed mb-5">{text}</p>
{/* `mt-auto` on the button rather than `flex-1` on the paragraph, and the parent is now actually
`flex flex-col`. It was not, so the old `flex-1` did nothing and the four buttons in this grid
sat wherever their card's text happened to end — measured 20-23px out of line with each other. */}
<div className="mt-auto -ml-3 -mb-3">
<GeneralButton
url={buttonUrl}
content={buttonLabel}
color={
buttonPrimary
? 'bg-cta hover:bg-cta-hover text-white border-cta shadow-[0_3px_12px_rgba(193,127,62,0.3)]'
: 'bg-canvas-100 hover:border-primary-600 hover:text-primary-600 border-canvas-300 text-ink-900'
}
/>
</div>
{/* A text link, not a boxed button: these three are the secondary asks, so their action is
deliberately lighter than the featured card's filled CTA. `mt-auto` pins it to the card's base
so the three links line up regardless of copy length. */}
<Link
href={buttonUrl}
target={buttonUrl.startsWith('http') ? '_blank' : undefined}
rel={buttonUrl.startsWith('http') ? 'noopener noreferrer' : undefined}
className="mt-auto inline-flex w-fit items-center text-sm font-semibold text-primary-700 transition-colors hover:text-primary-800"
>
{buttonLabel}
</Link>
</div>
)
}
@@ -299,47 +335,63 @@ function HelpCard({icon, title, text, buttonLabel, buttonUrl, buttonPrimary, id}
// ─── Share Strip ──────────────────────────────────────────────────────────────
/**
* The closing ask, and the page's visual climax.
*
* It is the only dark block, which is what makes it land — so it is also the reason the "One Mission"
* statement above is tinted-light rather than dark. Two dark full-width panels on one page would read as
* a repeating band and neither would be the ending.
* One network-effect benefit in the closing share block. Kept terse — the argument is carried by the
* headline and the reframe beside it; these three just make "better for you" concrete (more people who
* fit, better events, still free). Styled for the dark panel: faint tile, amber glyph, warm-white text.
*/
function ShareBenefit({icon: Icon, title, text}: {icon: IconType; title: string; text: string}) {
return (
<li className="flex items-start gap-3.5">
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-white/[0.06] ring-1 ring-white/10">
<Icon className="h-[18px] w-[18px] text-primary-500" strokeWidth={1.8} />
</div>
<div className="min-w-0">
<div className="text-[15px] font-semibold leading-snug text-white">{title}</div>
<div className="mt-0.5 text-sm leading-snug text-white/55">{text}</div>
</div>
</li>
)
}
/**
* The share control on the closing strip.
* The share control on the closing block.
*
* Deliberately mobile-only. On a phone the platform's own share sheet is the right surface — it lets the
* reader pick WhatsApp, Messages, whatever they actually talk to friends and family in, which is exactly
* who this block asks them to tell. On desktop there is no good equivalent (a "copy link" pill was the
* old stand-in and it was doing very little), so we show nothing rather than a weaker button.
* Universal, not mobile-only: the block's whole argument is that sharing is easy and in the reader's
* interest, so a desktop with no button would undercut it. Phones get the native share sheet (they can
* pick WhatsApp, Messages, ...); everywhere else — and any browser without the Web Share API — it copies
* the link and confirms. The API is probed at click time, so there is no SSR/first-paint branch to get
* wrong and the button always renders.
*
* Two gates, both required: `useIsMobile()` (viewport under the sm breakpoint) AND a live check that the
* Web Share API exists. Width alone would render a dead button on the odd narrow desktop window; the API
* check alone would surface it on desktop Safari/Edge, which do implement `navigator.share`. `canShare`
* starts false and is only set in an effect, so SSR and the first client paint render nothing — the safe
* desktop state — and the button appears on phones once mounted.
* When the sharer is signed in the link carries their `?referrer=` tag, the same attribution the
* /referrals page and users.ts already speak — so the shares this block is arguing for actually get
* credited to them, which is the whole self-interest case. Logged-out visitors (the page is public)
* have no username, so they share the bare URL.
*/
function MobileShareButton() {
function ShareCTA() {
const t = useT()
const isMobile = useIsMobile()
const [canShare, setCanShare] = useState(false)
const user = useUser()
const [copied, setCopied] = useState(false)
useEffect(() => {
setCanShare(typeof navigator !== 'undefined' && typeof navigator.share === 'function')
}, [])
if (!isMobile || !canShare) return null
const shareUrl = user?.username
? `${DEPLOYED_WEB_URL}/?referrer=${user.username}`
: DEPLOYED_WEB_URL
const onClick = async () => {
try {
await navigator.share({
title: t('about.share.title', 'Compass'),
text: t('about.share.text', 'Thoughtful 1-on-1 connections, built in the open.'),
url: DEPLOYED_WEB_URL,
})
} catch {
// The user dismissing the share sheet rejects the promise; that is not an error worth surfacing.
if (typeof navigator !== 'undefined' && typeof navigator.share === 'function') {
try {
await navigator.share({
title: t('about.share.title', 'Compass'),
text: t('about.share.text', 'Thoughtful 1-on-1 connections, built in the open.'),
url: shareUrl,
})
} catch {
// The user dismissing the share sheet rejects the promise; not an error worth surfacing.
}
return
}
copyToClipboard(shareUrl)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
@@ -347,44 +399,130 @@ function MobileShareButton() {
type="button"
onClick={onClick}
className={clsx(
'inline-flex items-center gap-2 px-5 py-2.5 rounded-xl text-sm font-semibold border',
'inline-flex items-center gap-2 rounded-xl border px-5 py-2.5 text-sm font-semibold',
'transition-all duration-200 ease-out',
'bg-cta text-white border-cta hover:bg-cta-hover',
'shadow-[0_6px_20px_-6px_rgba(193,127,62,0.6)]',
)}
>
<ShareIcon className="w-[1.05rem] h-[1.05rem]" strokeWidth={2} aria-hidden="true" />
{t('about.share.button', 'Share')}
{copied ? (
<CheckIcon className="h-[1.05rem] w-[1.05rem]" strokeWidth={2.5} aria-hidden="true" />
) : (
<ShareIcon className="h-[1.05rem] w-[1.05rem]" strokeWidth={2} aria-hidden="true" />
)}
{copied
? t('about.share.copied', 'Link copied!')
: t('about.share.button_cta', 'Share Compass')}
</button>
)
}
function ShareStrip({title, text}: {title: string; text: string}) {
/**
* The closing ask, and the page's visual climax.
*
* It is the only dark block, which is what makes it land — so it is also the reason the "One Mission"
* statement above is tinted-light rather than dark. Two dark full-width panels on one page would read as
* a repeating band and neither would be the ending.
*
* The ask is framed as self-interest rather than charity (it used to close with "thank you for
* supporting our mission", which asks for a favour). Same argument as the share-compass email: growth is
* a network effect the sharer benefits from, and even a friend who is not your match brings their world
* with them. The headline states the payoff, the paragraph reframes the obvious objection, the benefits
* make the payoff concrete, and the full-width bar at the base carries the one action.
*/
function ShareStrip() {
const t = useT()
const benefits = [
{
icon: UserGroupIcon,
title: t('about.share.benefit.people.title', 'More kindred spirits'),
text: t(
'about.share.benefit.people.text',
'A bigger pool of people who actually share your values.',
),
},
// {
// icon: CalendarDaysIcon,
// title: t('about.share.benefit.events.title', 'Richer events'),
// text: t(
// 'about.share.benefit.events.text',
// 'More people nearby means better meetups and gatherings.',
// ),
// },
// Dropped: "Better odds of a match". "Match" is swipe-app vocabulary, and Compass positions itself
// against exactly that — it was the one benefit here that sounded like the thing we aren't.
// {
// icon: HeartIcon,
// title: t('about.share.benefit.match.title', 'Better odds of a match'),
// text: t(
// 'about.share.benefit.match.text',
// "Every person who joins raises the chance the one you're looking for is already here.",
// ),
// },
{
icon: GiftIcon,
title: t('about.share.benefit.free.title', 'Free, forever'),
text: t(
'about.share.benefit.free.text',
'More contributors keep Compass ad-free and paywall-free.',
),
},
]
return (
<div className="relative overflow-hidden bg-canvas-950 rounded-3xl px-7 py-10 sm:px-12 sm:py-14">
<div
aria-hidden
className="pointer-events-none absolute -top-32 -right-20 w-[440px] h-[440px] rounded-full bg-primary-500/20 blur-3xl"
/>
<div className="relative flex items-center justify-between gap-8 flex-wrap">
<div className="max-w-[520px]">
<div className="flex items-center gap-2.5 mb-3">
<MegaphoneIcon className="w-5 h-5 text-primary-500 flex-shrink-0" strokeWidth={1.8} />
<span className={clsx(eyebrow, 'text-primary-500')}>
{t('about.final.label', 'Spread the word')}
</span>
</div>
<h3 className="font-heading text-white text-[clamp(22px,2.4vw,32px)] font-bold leading-tight tracking-tight mb-3 text-balance">
{title}
</h3>
{/* Was `text-primary-500` — amber body copy on espresso. The accent belongs on the eyebrow
and the icon; the sentence itself reads better in plain warm white. */}
<p className="text-white/70 text-base leading-relaxed">{text}</p>
<div className="relative">
<div className="flex items-center gap-2.5 mb-5">
<MegaphoneIcon className="w-5 h-5 text-primary-500 flex-shrink-0" strokeWidth={1.8} />
<span className={clsx(eyebrow, 'text-primary-500')}>
{t('about.final.label', 'Spread the word')}
</span>
</div>
<div className="grid gap-10 lg:grid-cols-2 lg:items-stretch lg:gap-14">
<div>
<h3 className="font-heading text-white text-[clamp(24px,2.6vw,34px)] font-bold leading-tight tracking-tight mb-4 text-balance">
{t(
'about.share.headline',
'Compass gets better for you with every person you bring.',
)}
</h3>
{/* The reframe, condensed from the share-compass email: the friend you tell need not be your
match — they bring their world, and that is the reader's own upside. */}
<p className="text-white/70 text-base leading-relaxed max-w-xl">
{t(
'about.share.reframe',
"Even if a friend isn't your match, they bring their world with them — their friends, their circles, the thoughtful people you'd never have met otherwise. Sharing isn't just a favor to them. It's an investment in your own future connections.",
)}
</p>
</div>
{/* An inset panel rather than a bare list: a few short rows floating in a tall column read as
empty space. The panel gives the right side visual mass, stretches to the copy's height
(items-stretch on the grid), and centres the benefits within it. */}
<ul className="flex flex-col justify-center gap-6 rounded-2xl bg-white/[0.04] p-6 ring-1 ring-white/10 sm:p-8 lg:h-full">
{benefits.map((b) => (
<ShareBenefit key={b.title} icon={b.icon} title={b.title} text={b.text} />
))}
</ul>
</div>
{/* The share action sits at the base of the block, under a hairline rule and spanning the full
width, so it reads as the block's climax rather than a mid-column element. */}
<div className="mt-8 flex flex-wrap items-center gap-x-5 gap-y-3 border-t border-white/10 pt-6">
<ShareCTA />
{/*<span className="text-white/45 text-sm max-w-xs leading-snug">*/}
{/* {t(*/}
{/* 'about.share.kicker',*/}
{/* "One share, one person — that's how a community like this is built.",*/}
{/* )}*/}
{/*</span>*/}
</div>
{/* Renders only on mobile; on desktop MobileShareButton returns null and the strip is copy-only
by design. */}
<MobileShareButton />
</div>
</div>
)
@@ -587,24 +725,29 @@ export default function About() {
<Section>
<SectionLabel>{t('about.help.label', 'Help Compass grow')}</SectionLabel>
{/* ── Share strip ── */}
<Reveal>
<ShareStrip
title={t('about.final.title', 'Tell Your Friends and Family')}
text={t(
'about.final.text',
'The best way to grow Compass is word of mouth. Thank you for supporting our mission.',
)}
/>
</Reveal>
{/* Growth line first, share block second: the momentum is what motivates the ask. Show that
Compass is growing, then invite the reader to add to it — the same order as the
share-compass email, which opens with the member count before asking. The share block then
closes the section on the action rather than on a passive chart. */}
<MemberGrowth />
{/* ── Share strip ── */}
<div className="mt-5">
<MemberGrowth />
<Reveal>
<ShareStrip />
</Reveal>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-5 mt-5">
{helpCards.map((card, i) => (
<Reveal key={card.id} delay={(i % 2) * 70}>
{/* One primary ask leads, then the three lighter ones. "Give Suggestions or Contribute" is
the contribution we most want, so it is the full-width featured card with the filled CTA;
the rest sit below as a quieter row of three with text-link actions. */}
<Reveal className="mt-5">
<FeaturedHelpCard {...helpCards[0]} />
</Reveal>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 sm:gap-5 mt-4 sm:mt-5">
{helpCards.slice(1).map((card, i) => (
<Reveal key={card.id} delay={i * 70}>
<HelpCard {...card} />
</Reveal>
))}