diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 73308f77f2..331cfb6f2a 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -220,10 +220,37 @@ jobs:
# The cache holds browsers, not the apt packages they link against,
# so system deps still need installing on a cache hit.
+ #
+ # `apt-get update` inside install-deps stalls intermittently on the
+ # runner's Azure mirrors: three consecutive 6-minute timeouts on
+ # 2026-08-19 (PR #2755), all of them ignoring
+ # azure.archive.ubuntu.com and then hanging on noble-security, while
+ # sibling runs minutes apart cleared the same step in seconds. The
+ # stall is transient, so retry with a per-attempt cap — one bad
+ # mirror then costs an attempt instead of the whole job — and drop
+ # to the canonical archive after the first failure. `timeout` owns
+ # the attempt's process group, so --kill-after reaps an apt child
+ # that ignores the SIGTERM it sends first. Still exits
+ # non-zero once the attempts are spent: a genuinely missing system
+ # library must stay a red job, not a silent one.
- name: Install Playwright system deps
if: steps.pw-cache.outputs.cache-hit == 'true'
- timeout-minutes: 6
- run: npx playwright install-deps chromium
+ timeout-minutes: 9
+ run: |
+ for attempt in 1 2 3; do
+ if timeout --kill-after=15s 150 npx playwright install-deps chromium; then
+ exit 0
+ fi
+ echo "::warning::playwright install-deps attempt ${attempt} failed or stalled; retrying"
+ sudo rm -rf /var/lib/apt/lists/partial/* || true
+ if [ "${attempt}" = 1 ]; then
+ sudo sed -i 's|azure.archive.ubuntu.com|archive.ubuntu.com|g' \
+ /etc/apt/sources.list.d/*.sources /etc/apt/sources.list || true
+ fi
+ sleep 5
+ done
+ echo "::error::playwright install-deps failed after 3 attempts"
+ exit 1
- name: Run E2E tests
run: pnpm test:e2e
diff --git a/src/components/LandingPage/Footer.tsx b/src/components/LandingPage/Footer.tsx
index 5610014afb..963be6b14b 100644
--- a/src/components/LandingPage/Footer.tsx
+++ b/src/components/LandingPage/Footer.tsx
@@ -114,12 +114,8 @@ const Footer = ({
{i18n.footerDocs}
-
- {i18n.footerTerms}
-
-
- {i18n.footerPrivacy}
-
+ {/* Terms and Privacy deliberately live in the Legal column of
+ the site directory below, not here — one home per document. */}
{i18n.footerSecurity}
diff --git a/src/components/LandingPage/SEOFooter.tsx b/src/components/LandingPage/SEOFooter.tsx
index a2bfc026ba..b6c5c67135 100644
--- a/src/components/LandingPage/SEOFooter.tsx
+++ b/src/components/LandingPage/SEOFooter.tsx
@@ -1,7 +1,7 @@
import Link from 'next/link'
import manifest from '@/content/generated/footer-manifest.json'
import { getTranslations, t } from '@/i18n'
-import { DEFAULT_LOCALE, type Locale } from '@/i18n/types'
+import { DEFAULT_LOCALE, type Locale, type Translations } from '@/i18n/types'
// SEO footer driven by the content manifest (peanut-content/generated/footer-manifest.json).
// Data is imported as a JSON module — works in both client and server components
@@ -28,6 +28,41 @@ function FooterSection({ title, children }: { title: string; children: React.Rea
)
}
+/**
+ * Manifest "resources" entries that now live elsewhere in the footer, so the
+ * remainder can be folded into "Learn More" without duplicating a link:
+ * - terms → a stale Notion export, superseded by the /terms page in Legal below
+ * - jobs → already a top-level link in the footer nav above
+ */
+const RESOURCES_MOVED_ELSEWHERE = new Set(['terms', 'jobs'])
+
+/**
+ * Every published legal document, in the order a reader needs them: the two
+ * that bind all users first, then the card-programme docs. Card applicants see
+ * these inline at signing time (CardTermsScreen), but app-store review and the
+ * issuer both expect them permanently reachable, which is what this column is.
+ * Hrefs are authored `/en/…` and localized by `localizeHref` like the manifest
+ * entries; the marketing pages fall back to English prose when a translation
+ * is missing, so a localized URL is always safe.
+ */
+const LEGAL_LINKS: Array<{ slug: string; href: string; label: (i18n: Translations) => string }> = [
+ { slug: 'terms', href: '/en/terms', label: (i18n) => i18n.footerTerms },
+ { slug: 'privacy', href: '/en/privacy', label: (i18n) => i18n.footerPrivacy },
+ { slug: 'card-terms-us', href: '/en/card-terms-us', label: (i18n) => i18n.footerCardTermsUs },
+ {
+ slug: 'card-terms-international',
+ href: '/en/card-terms-international',
+ label: (i18n) => i18n.footerCardTermsInternational,
+ },
+ { slug: 'card-esign', href: '/en/card-esign', label: (i18n) => i18n.footerCardEsign },
+ { slug: 'card-privacy', href: '/en/card-privacy', label: (i18n) => i18n.footerCardPrivacy },
+ {
+ slug: 'card-prohibited-activities',
+ href: '/en/card-prohibited-activities',
+ label: (i18n) => i18n.footerCardProhibitedActivities,
+ },
+]
+
function FooterLink({ href, external, children }: { href: string; external?: boolean; children: React.ReactNode }) {
if (external) {
return (
@@ -60,6 +95,9 @@ export function SEOFooter({ locale = DEFAULT_LOCALE }: { locale?: Locale } = {})
const articles = ((manifest as Record).articles ?? []) as ManifestEntry[]
const resources = (manifest.resources ?? []) as ManifestEntry[]
const hasSendMoney = sendTo.length > 0 || sendFrom.length > 0
+ // What's left of "Resources" rides on top of "Learn More" — the column
+ // itself is gone, its slot in the 4-up grid taken by Legal.
+ const learnMoreResources = resources.filter((entry) => !RESOURCES_MOVED_ELSEWHERE.has(entry.slug))
const link = (entry: ManifestEntry) => (entry.external ? entry.href : localizeHref(entry.href, locale))
@@ -91,25 +129,28 @@ export function SEOFooter({ locale = DEFAULT_LOCALE }: { locale?: Locale } = {})
)}
- {articles.length > 0 && (
+ {(articles.length > 0 || learnMoreResources.length > 0) && (
- {articles.map((entry) => (
-
+ {learnMoreResources.map((entry) => (
+
{entry.name}
))}
-
- )}
-
- {resources.length > 0 && (
-
- {resources.map((entry) => (
-
+ {articles.map((entry) => (
+
{entry.name}
))}
)}
+
+
+ {LEGAL_LINKS.map((entry) => (
+
+ {entry.label(i18n)}
+
+ ))}
+
)
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 48880d77b8..3e0b4e5325 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -88,14 +88,19 @@
"footerLegalEntity": "Peanut is a trading name of Squirrel Labs Ltd, registered in England & Wales (No. 14558823)",
"footerSupport": "Support",
"footerDocs": "Docs",
- "footerTerms": "Terms",
- "footerPrivacy": "Privacy",
+ "footerTerms": "Terms of Service",
+ "footerPrivacy": "Privacy Policy",
"footerSecurity": "Security",
"footerJobs": "Jobs",
"footerSiteDirectory": "Site directory",
"footerCompare": "Compare",
"footerLearnMoreSection": "Learn More",
- "footerResources": "Resources",
+ "footerLegalSection": "Legal",
+ "footerCardTermsUs": "Card Terms (U.S.)",
+ "footerCardTermsInternational": "Card Terms (International)",
+ "footerCardEsign": "E-Sign Consent",
+ "footerCardPrivacy": "Account Opening Privacy Notice",
+ "footerCardProhibitedActivities": "Prohibited Activities Policy",
"footerSendTo": "Send to {name}",
"footerSendFrom": "Send from {name}",
"footerPeanutVs": "Peanut vs {name}",
diff --git a/src/i18n/es-419.json b/src/i18n/es-419.json
index 9fad38b40f..3123e9dd1a 100644
--- a/src/i18n/es-419.json
+++ b/src/i18n/es-419.json
@@ -88,14 +88,19 @@
"footerLegalEntity": "Peanut es un nombre comercial de Squirrel Labs Ltd, registrada en Inglaterra y Gales (N.º 14558823)",
"footerSupport": "Soporte",
"footerDocs": "Documentación",
- "footerTerms": "Términos",
- "footerPrivacy": "Privacidad",
+ "footerTerms": "Términos de servicio",
+ "footerPrivacy": "Política de privacidad",
"footerSecurity": "Seguridad",
"footerJobs": "Empleo",
"footerSiteDirectory": "Directorio del sitio",
"footerCompare": "Comparar",
"footerLearnMoreSection": "Más información",
- "footerResources": "Recursos",
+ "footerLegalSection": "Legal",
+ "footerCardTermsUs": "Términos de la tarjeta (EE. UU.)",
+ "footerCardTermsInternational": "Términos de la tarjeta (internacional)",
+ "footerCardEsign": "Consentimiento de firma electrónica",
+ "footerCardPrivacy": "Aviso de privacidad de apertura de cuenta",
+ "footerCardProhibitedActivities": "Política de actividades prohibidas",
"footerSendTo": "Enviar a {name}",
"footerSendFrom": "Enviar desde {name}",
"footerPeanutVs": "Peanut vs. {name}",
diff --git a/src/i18n/es-ar.json b/src/i18n/es-ar.json
index 093d9fedfa..d3bc8905b0 100644
--- a/src/i18n/es-ar.json
+++ b/src/i18n/es-ar.json
@@ -88,14 +88,19 @@
"footerLegalEntity": "Peanut es un nombre comercial de Squirrel Labs Ltd, registrada en Inglaterra y Gales (N.º 14558823)",
"footerSupport": "Soporte",
"footerDocs": "Documentación",
- "footerTerms": "Términos",
- "footerPrivacy": "Privacidad",
+ "footerTerms": "Términos de servicio",
+ "footerPrivacy": "Política de privacidad",
"footerSecurity": "Seguridad",
"footerJobs": "Empleo",
"footerSiteDirectory": "Directorio del sitio",
"footerCompare": "Comparar",
"footerLearnMoreSection": "Más información",
- "footerResources": "Recursos",
+ "footerLegalSection": "Legal",
+ "footerCardTermsUs": "Términos de la tarjeta (EE. UU.)",
+ "footerCardTermsInternational": "Términos de la tarjeta (internacional)",
+ "footerCardEsign": "Consentimiento de firma electrónica",
+ "footerCardPrivacy": "Aviso de privacidad de apertura de cuenta",
+ "footerCardProhibitedActivities": "Política de actividades prohibidas",
"footerSendTo": "Enviar a {name}",
"footerSendFrom": "Enviar desde {name}",
"footerPeanutVs": "Peanut vs. {name}",
diff --git a/src/i18n/pt-br.json b/src/i18n/pt-br.json
index 257b373fb8..e3dd80a077 100644
--- a/src/i18n/pt-br.json
+++ b/src/i18n/pt-br.json
@@ -88,14 +88,19 @@
"footerLegalEntity": "Peanut é um nome comercial da Squirrel Labs Ltd, registrada na Inglaterra e no País de Gales (n.º 14558823)",
"footerSupport": "Suporte",
"footerDocs": "Documentação",
- "footerTerms": "Termos",
- "footerPrivacy": "Privacidade",
+ "footerTerms": "Termos de serviço",
+ "footerPrivacy": "Política de privacidade",
"footerSecurity": "Segurança",
"footerJobs": "Vagas",
"footerSiteDirectory": "Diretório do site",
"footerCompare": "Comparar",
"footerLearnMoreSection": "Saiba mais",
- "footerResources": "Recursos",
+ "footerLegalSection": "Jurídico",
+ "footerCardTermsUs": "Termos do cartão (EUA)",
+ "footerCardTermsInternational": "Termos do cartão (internacional)",
+ "footerCardEsign": "Consentimento de assinatura eletrônica",
+ "footerCardPrivacy": "Aviso de privacidade de abertura de conta",
+ "footerCardProhibitedActivities": "Política de atividades proibidas",
"footerSendTo": "Enviar para {name}",
"footerSendFrom": "Enviar de {name}",
"footerPeanutVs": "Peanut vs. {name}",
diff --git a/src/i18n/types.ts b/src/i18n/types.ts
index c30a82563c..8a99bd42bd 100644
--- a/src/i18n/types.ts
+++ b/src/i18n/types.ts
@@ -212,7 +212,12 @@ export interface Translations {
footerSiteDirectory: string
footerCompare: string
footerLearnMoreSection: string
- footerResources: string
+ footerLegalSection: string
+ footerCardTermsUs: string
+ footerCardTermsInternational: string
+ footerCardEsign: string
+ footerCardPrivacy: string
+ footerCardProhibitedActivities: string
footerSendTo: string // "Send to {name}"
footerSendFrom: string // "Send from {name}"
footerPeanutVs: string // "Peanut vs {name}"