import type { Metadata, Viewport } from 'next'; import { Inter, JetBrains_Mono, IBM_Plex_Mono } from 'next/font/google'; import PostHogProvider from '@/components/PostHogProvider'; import AuthProvider from '@/contexts/AuthContext'; import ParlayProvider from '@/contexts/ParlayContext'; import ExplainModeProvider from '@/contexts/ExplainModeContext'; import Nav from '@/components/Nav'; import Footer from '@/components/Footer'; import AuthGate from '@/components/AuthGate'; import HashRedirect from '@/components/vyndr/HashRedirect'; import GlobalHosts from '@/components/vyndr/GlobalHosts'; // Session 50 — the Parlay Lab supersedes the legacy ParlayTray. import ParlayPanel from '@/components/vyndr/ParlayPanel'; import BottomTabBar from '@/components/BottomTabBar'; import InstallPrompt from '@/components/InstallPrompt'; import PushPrompt from '@/components/PushPrompt'; import MFAPrompt from '@/components/MFAPrompt'; import MFAChallenge from '@/components/MFAChallenge'; import CookieConsent from '@/components/CookieConsent'; import SentryInit from '@/components/SentryInit'; import { LocaleProvider } from '@/contexts/LocaleContext'; import { headers } from 'next/headers'; import { LOCALE_HEADER, COUNTRY_HEADER, isLocale, DEFAULT_LOCALE, LOCALE_META } from '@/lib/locales'; import './globals.css'; // Session 41 — self-hosted via next/font (downloaded at build time, served // from our origin). Replaces the runtime fonts.googleapis.com , which // returned 503 in production and added ~4s to page load. §2 brand fonts: // Inter for chrome/UI (--font-sans), JetBrains Mono for ALL data (--font-mono). // IBM Plex Mono (--font-ibm) kept for the wordmark + legacy mono surfaces while // pages migrate. globals.css :root maps --sans/--mono onto these variables. const inter = Inter({ subsets: ['latin'], variable: '--font-sans', display: 'swap' }); const jetbrainsMono = JetBrains_Mono({ subsets: ['latin'], variable: '--font-mono', display: 'swap' }); const ibmPlexMono = IBM_Plex_Mono({ subsets: ['latin'], weight: ['400', '500', '600', '700'], variable: '--font-ibm', display: 'swap', }); const fontVars = `${inter.variable} ${jetbrainsMono.variable} ${ibmPlexMono.variable}`; export const metadata: Metadata = { metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'https://vyndr.app'), title: { default: 'VYNDR — Sports Prop Intelligence', template: '%s · VYNDR', }, description: 'Pre-graded player props with proprietary archetypes. Correlation-aware Parlay Lab. Real-time line movement tracking. NBA, MLB, WNBA, and soccer. Built in Detroit.', applicationName: 'VYNDR', authors: [{ name: 'VYNDR', url: 'https://vyndr.app' }], manifest: '/manifest.json', keywords: [ 'sports prop grading', 'NBA prop bet analysis', 'MLB prop intelligence', 'WNBA prop grading', 'soccer prop intelligence', 'World Cup 2026 props', 'xG regression analysis', 'parlay correlation analysis', 'prop betting tools', ], openGraph: { title: "VYNDR — The edge the books don't want you to have", description: 'Pre-graded player props with proprietary archetypes. Correlation-aware Parlay Lab. Real-time line movement tracking. Built in Detroit.', url: 'https://vyndr.app', siteName: 'VYNDR', locale: 'en_US', type: 'website', // og:image is supplied by the file-based opengraph-image.tsx convention. }, twitter: { card: 'summary_large_image', title: "VYNDR — The edge the books don't want you to have", description: 'Pre-graded player props. Proprietary archetypes. Parlay Lab. Built in Detroit.', creator: '@getvyndr', // twitter:image is supplied by the file-based twitter-image.tsx convention. }, icons: { icon: [ { url: '/favicon.svg', type: 'image/svg+xml' }, { url: '/favicon.ico', sizes: '32x32' }, { url: '/favicon-32.png', sizes: '32x32', type: 'image/png' }, { url: '/favicon-16.png', sizes: '16x16', type: 'image/png' }, ], apple: '/apple-touch-icon.png', }, appleWebApp: { capable: true, statusBarStyle: 'black-translucent', title: 'VYNDR', }, robots: { index: true, follow: true, googleBot: { index: true, follow: true, 'max-image-preview': 'large', 'max-snippet': -1, }, }, }; export const viewport: Viewport = { themeColor: '#06060B', width: 'device-width', initialScale: 1, maximumScale: 5, // Session 37 — extend under the iOS notch / home indicator so the // mobile tab bar's env(safe-area-inset-*) padding has room to work. viewportFit: 'cover', }; export default async function RootLayout({ children }: { children: React.ReactNode }) { // Session 12 — resolve locale from the middleware-stamped request // header so server components render with the right translations // and the dir attribute is set before paint (no FOUC of // mis-directional text). const hdrs = await headers(); const localeHeader = hdrs.get(LOCALE_HEADER); const locale = isLocale(localeHeader) ? localeHeader : DEFAULT_LOCALE; const dir = LOCALE_META[locale].dir; // Session 13 — country from CF-IPCountry (set by middleware). // Empty string when traffic bypasses Cloudflare (local dev, direct // origin hits). The Africa-tier gate degrades closed on empty. const country = hdrs.get(COUNTRY_HEADER) || ''; return (