/* @jsx React.createElement */
// Styled by gf-landing.css (the --lp-* tokens) and gf-profile.css (the pf-* classes).
const { useState, useEffect, useMemo, useCallback } = React;

// ───────── Timezone data ─────────
const TIMEZONES = [
  { value: 'UTC', label: 'UTC', region: '' },
  { value: 'Africa/Abidjan', label: 'Abidjan (Ivory Coast)', region: 'Africa' },
  { value: 'Africa/Accra', label: 'Accra (Ghana)', region: 'Africa' },
  { value: 'Africa/Algiers', label: 'Algiers (Algeria)', region: 'Africa' },
  { value: 'Africa/Cairo', label: 'Cairo (Egypt)', region: 'Africa' },
  { value: 'Africa/Casablanca', label: 'Casablanca (Morocco)', region: 'Africa' },
  { value: 'Africa/Dakar', label: 'Dakar (Senegal)', region: 'Africa' },
  { value: 'Africa/Douala', label: 'Douala (Cameroon)', region: 'Africa' },
  { value: 'Africa/Johannesburg', label: 'Johannesburg (South Africa)', region: 'Africa' },
  { value: 'Africa/Kinshasa', label: 'Kinshasa (DR Congo)', region: 'Africa' },
  { value: 'Africa/Lagos', label: 'Lagos (Nigeria)', region: 'Africa' },
  { value: 'Africa/Nairobi', label: 'Nairobi (Kenya)', region: 'Africa' },
  { value: 'Africa/Tunis', label: 'Tunis (Tunisia)', region: 'Africa' },
  { value: 'America/Bogota', label: 'Bogotá (Colombia)', region: 'Americas' },
  { value: 'America/Buenos_Aires', label: 'Buenos Aires (Argentina)', region: 'Americas' },
  { value: 'America/Caracas', label: 'Caracas (Venezuela)', region: 'Americas' },
  { value: 'America/Chicago', label: 'Chicago (US Central)', region: 'Americas' },
  { value: 'America/Denver', label: 'Denver (US Mountain)', region: 'Americas' },
  { value: 'America/Los_Angeles', label: 'Los Angeles (US Pacific)', region: 'Americas' },
  { value: 'America/Lima', label: 'Lima (Peru)', region: 'Americas' },
  { value: 'America/Mexico_City', label: 'Mexico City', region: 'Americas' },
  { value: 'America/New_York', label: 'New York (US Eastern)', region: 'Americas' },
  { value: 'America/Santiago', label: 'Santiago (Chile)', region: 'Americas' },
  { value: 'America/Sao_Paulo', label: 'São Paulo (Brazil)', region: 'Americas' },
  { value: 'America/Toronto', label: 'Toronto (Canada East)', region: 'Americas' },
  { value: 'America/Vancouver', label: 'Vancouver (Canada West)', region: 'Americas' },
  { value: 'Asia/Bangkok', label: 'Bangkok (Thailand)', region: 'Asia' },
  { value: 'Asia/Beirut', label: 'Beirut (Lebanon)', region: 'Asia' },
  { value: 'Asia/Dhaka', label: 'Dhaka (Bangladesh)', region: 'Asia' },
  { value: 'Asia/Dubai', label: 'Dubai (UAE)', region: 'Asia' },
  { value: 'Asia/Almaty', label: 'Almaty (Kazakhstan)', region: 'Asia' },
  { value: 'Asia/Baku', label: 'Baku (Azerbaijan)', region: 'Asia' },
  { value: 'Asia/Tbilisi', label: 'Tbilisi (Georgia)', region: 'Asia' },
  { value: 'Asia/Tehran', label: 'Tehran (Iran)', region: 'Asia' },
  { value: 'Asia/Yerevan', label: 'Yerevan (Armenia)', region: 'Asia' },
  { value: 'Asia/Ho_Chi_Minh', label: 'Ho Chi Minh City (Vietnam)', region: 'Asia' },
  { value: 'Asia/Istanbul', label: 'Istanbul (Turkey)', region: 'Asia' },
  { value: 'Asia/Jakarta', label: 'Jakarta (Indonesia)', region: 'Asia' },
  { value: 'Asia/Jerusalem', label: 'Jerusalem (Israel)', region: 'Asia' },
  { value: 'Asia/Karachi', label: 'Karachi (Pakistan)', region: 'Asia' },
  { value: 'Asia/Kuala_Lumpur', label: 'Kuala Lumpur (Malaysia)', region: 'Asia' },
  { value: 'Asia/Kolkata', label: 'Mumbai / Delhi (India)', region: 'Asia' },
  { value: 'Asia/Manila', label: 'Manila (Philippines)', region: 'Asia' },
  { value: 'Asia/Riyadh', label: 'Riyadh (Saudi Arabia)', region: 'Asia' },
  { value: 'Asia/Seoul', label: 'Seoul (South Korea)', region: 'Asia' },
  { value: 'Asia/Shanghai', label: 'Shanghai / Beijing (China)', region: 'Asia' },
  { value: 'Asia/Singapore', label: 'Singapore', region: 'Asia' },
  { value: 'Asia/Tokyo', label: 'Tokyo (Japan)', region: 'Asia' },
  { value: 'Europe/Amsterdam', label: 'Amsterdam (Netherlands)', region: 'Europe' },
  { value: 'Europe/Budapest', label: 'Budapest (Hungary)', region: 'Europe' },
  { value: 'Europe/Kyiv', label: 'Kyiv (Ukraine)', region: 'Europe' },
  { value: 'Europe/Prague', label: 'Prague (Czechia)', region: 'Europe' },
  { value: 'Europe/Athens', label: 'Athens (Greece)', region: 'Europe' },
  { value: 'Europe/Berlin', label: 'Berlin (Germany)', region: 'Europe' },
  { value: 'Europe/Brussels', label: 'Brussels (Belgium)', region: 'Europe' },
  { value: 'Europe/Bucharest', label: 'Bucharest (Romania)', region: 'Europe' },
  { value: 'Europe/Copenhagen', label: 'Copenhagen (Denmark)', region: 'Europe' },
  { value: 'Europe/Dublin', label: 'Dublin (Ireland)', region: 'Europe' },
  { value: 'Europe/Helsinki', label: 'Helsinki (Finland)', region: 'Europe' },
  { value: 'Europe/Lisbon', label: 'Lisbon (Portugal)', region: 'Europe' },
  { value: 'Europe/London', label: 'London (UK)', region: 'Europe' },
  { value: 'Europe/Madrid', label: 'Madrid (Spain)', region: 'Europe' },
  { value: 'Europe/Moscow', label: 'Moscow (Russia)', region: 'Europe' },
  { value: 'Europe/Oslo', label: 'Oslo (Norway)', region: 'Europe' },
  { value: 'Europe/Paris', label: 'Paris (France)', region: 'Europe' },
  { value: 'Europe/Rome', label: 'Rome (Italy)', region: 'Europe' },
  { value: 'Europe/Stockholm', label: 'Stockholm (Sweden)', region: 'Europe' },
  { value: 'Europe/Vienna', label: 'Vienna (Austria)', region: 'Europe' },
  { value: 'Europe/Warsaw', label: 'Warsaw (Poland)', region: 'Europe' },
  { value: 'Europe/Zurich', label: 'Zurich (Switzerland)', region: 'Europe' },
  { value: 'Australia/Perth', label: 'Perth (Australia)', region: 'Oceania' },
  { value: 'Australia/Melbourne', label: 'Melbourne (Australia)', region: 'Oceania' },
  { value: 'Australia/Sydney', label: 'Sydney (Australia)', region: 'Oceania' },
  { value: 'Pacific/Auckland', label: 'Auckland (New Zealand)', region: 'Oceania' },
  { value: 'Pacific/Honolulu', label: 'Honolulu (Hawaii)', region: 'Oceania' },
];

const TZ_REGIONS = ['Africa', 'Americas', 'Asia', 'Europe', 'Oceania'];

function isValidTimeZone(tz) {
  try { new Intl.DateTimeFormat('en-US', { timeZone: tz }); return true; } catch { return false; }
}

/** The zone this phone is set to, or null when the browser won't say. */
function browserTimeZone() {
  try {
    const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
    return tz && isValidTimeZone(tz) ? tz : null;
  } catch { return null; }
}

const regionOf = (tz) => ({
  Africa: 'Africa', America: 'Americas', Asia: 'Asia', Indian: 'Asia', Europe: 'Europe',
  Atlantic: 'Europe', Australia: 'Oceania', Pacific: 'Oceania',
})[tz.split('/')[0]] || 'Asia';

/*
 * Resolve a stored timezone to a value in the list. A real zone the list
 * lacks is added to it, never shown as UTC: the server infers zones for about
 * 170 dialling codes, and showing one as UTC would save UTC over it the next
 * time the profile is saved.
 */
function resolveTimezone(tz) {
  if (!tz) return 'UTC';
  const match = TIMEZONES.find((t) => t.value === tz);
  if (match) return match.value;
  if (isValidTimeZone(tz)) {
    TIMEZONES.push({ value: tz, label: tz.split('/').pop().replace(/_/g, ' '), region: regionOf(tz) });
    return tz;
  }
  // Legacy free-text: try to match by label substring
  const lower = tz.toLowerCase();
  const fuzzy = TIMEZONES.find((t) => t.label.toLowerCase().includes(lower) || lower.includes(t.value.split('/')[1].toLowerCase()));
  return fuzzy ? fuzzy.value : 'UTC';
}

// ───────── Primitives ─────────

function Card({ title, children }) {
  return (
    <section className="pf-card">
      <h2 className="pf-card-title">{title}</h2>
      {children}
    </section>
  );
}

// A label over its control, with an optional hint under it. Fields space
// themselves; a field inside a field sits closer to its parent's control.
function Field({ label, hint, children }) {
  return (
    <div className="pf-field">
      <div className="pf-field-label">{label}</div>
      {children}
      {hint && <div className="pf-hint">{hint}</div>}
    </div>
  );
}

function Pill({ active, onClick, children }) {
  return (
    <button type="button" className="pf-choice" aria-pressed={!!active} onClick={onClick}>{children}</button>
  );
}

function PillRow({ children }) {
  return <div className="pf-choices">{children}</div>;
}

function TextInput({ value, onChange, placeholder, suffix, inputMode }) {
  return (
    <div className="pf-input">
      <input
        value={value ?? ''}
        onChange={(e) => onChange(e.target.value)}
        placeholder={placeholder}
        inputMode={inputMode}
      />
      {suffix && <span className="pf-input-suffix">{suffix}</span>}
    </div>
  );
}

function SelectInput({ value, onChange, children }) {
  return (
    <div className="pf-select">
      <select value={value ?? ''} onChange={(e) => onChange(e.target.value)}>{children}</select>
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
        <polyline points="6 9 12 15 18 9" />
      </svg>
    </div>
  );
}

// ───────── Icons ─────────

// The landing page's WhatsApp glyph (gf-landing-hero.jsx).
function WhatsappGlyph({ size = 20 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
      <path d="M17.4 14.4c-.3-.1-1.7-.8-2-.9-.3-.1-.5-.1-.7.1-.2.3-.8.9-1 1.1-.2.2-.3.2-.6.1-.3-.1-1.2-.4-2.4-1.5-.9-.8-1.5-1.8-1.7-2.1-.2-.3 0-.5.1-.6.1-.1.3-.4.5-.6.1-.2.2-.3.3-.5.1-.2 0-.4 0-.5 0-.1-.7-1.6-.9-2.2-.2-.6-.5-.5-.7-.5h-.6c-.2 0-.5.1-.8.4-.3.3-1 1-1 2.5s1.1 2.9 1.2 3.1c.1.2 2.1 3.3 5.2 4.6 2.5 1.1 3.1 1 3.6 1 .6-.1 1.7-.7 2-1.4.2-.7.2-1.2.2-1.4-.1-.2-.3-.3-.6-.4zM12 2C6.5 2 2 6.5 2 12c0 1.9.5 3.6 1.4 5.2L2 22l4.9-1.3c1.5.8 3.3 1.3 5.1 1.3 5.5 0 10-4.5 10-10S17.5 2 12 2z" />
    </svg>
  );
}

function TargetIcon() {
  return (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
      <circle cx="12" cy="12" r="9" /><circle cx="12" cy="12" r="5" /><circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none" />
    </svg>
  );
}

function ScaleIcon() {
  return (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
      <rect x="3" y="3" width="18" height="18" rx="5" /><path d="M8 10a5.5 5.5 0 0 1 8 0M12 10l1.5-2" />
    </svg>
  );
}

function PinIcon() {
  return (
    <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M9 3h6l-1 7 3.5 3.5h-11L10 10 9 3zM12 13.5V21" />
    </svg>
  );
}

// ───────── Constants ─────────

const GOALS = ['lose_weight', 'build_muscle', 'maintain_weight', 'get_healthier'];
const ACTIVITY = ['sedentary', 'lightly_active', 'moderately_active', 'very_active', 'extra_active'];
const DIETS = ['balanced', 'low_fat', 'low_carb', 'high_protein'];
const LANGUAGES = [
  { id: 'en', label: '🇬🇧 English' }, { id: 'fr', label: '🇫🇷 Français' }, { id: 'es', label: '🇪🇸 Español' },
  { id: 'pt', label: '🇧🇷 Português' }, { id: 'de', label: '🇩🇪 Deutsch' }, { id: 'it', label: '🇮🇹 Italiano' },
];
const CHECKIN_TIMES = ['07:00', '08:00', '12:00', '18:00', '20:00', '22:00'];
const CHECKIN_LABEL = { '07:00': '7:00am', '08:00': '8:00am', '12:00': '12:00pm', '18:00': '6:00pm', '20:00': '8:00pm', '22:00': '10:00pm' };

// ───────── Copy ─────────
/*
 * The page speaks the user's language: profile.language, which the server
 * sets at signup from the website's Start word or the dialling code. Someone
 * who wrote "Empezar" to the bot gets the six questions in Spanish. Before the
 * profile loads, the browser's language stands in.
 *
 * The six languages the coach speaks (Language in apps/server/src/types and
 * COPY_LANGUAGES in apps/server/src/lib/i18n.ts). Keep the key sets the same.
 */
const STRINGS = {
  en: {
    loading: 'Loading your profile…', missingLink: 'Missing or invalid profile link.',
    loadFailed: 'Could not load your profile.', saveFailed: 'Could not save your profile.',
    yourProfile: 'Your profile', pinned: 'Pinned in your chat', back: 'Back', you: 'You',
    aboutYou: 'About you', name: 'Name', namePlaceholder: 'Your name',
    gender: 'Gender', male: 'Male', female: 'Female',
    birthYear: 'Birth year', yearsOld: (n) => `${n} years old`,
    height: 'Height', heightPlaceholder: `175cm or 5'9"`, heightHint: `cm or 5'9"`,
    goalActivity: 'Goal & activity', yourGoal: 'Your goal',
    goals: { lose_weight: 'Lose weight', build_muscle: 'Build muscle', maintain_weight: 'Maintain', get_healthier: 'Get healthier' },
    units: 'Units', kilograms: 'Kilograms (kg)', pounds: 'Pounds (lb)',
    currentWeight: 'Current weight', startingWeight: 'Starting weight',
    startingWeightHint: 'Your weight when you started your Getfiit journey',
    targetWeight: 'Target weight', setTarget: 'Set a target', skip: 'Skip', targetValue: 'Target value',
    activityLevel: 'Activity level',
    activity: { sedentary: 'Sedentary', lightly_active: 'Lightly active', moderately_active: 'Moderately active', very_active: 'Very active', extra_active: 'Extra active' },
    dietPref: 'Dietary preference',
    diets: { balanced: 'Balanced', low_fat: 'Low fat', low_carb: 'Low carb', high_protein: 'High protein' },
    preferences: 'Preferences', language: 'Language', timezone: 'Timezone',
    timezoneHint: 'Used to time your daily check-ins', dailyCheckin: 'Daily check-in', noReminder: 'No reminder',
    checkinLabel: (t) => CHECKIN_LABEL[t],
    subscription: 'Subscription', trial: 'Trial', active: 'Active', renews: (d) => `renews ${d}`,
    manageSubscription: 'Manage subscription', intervals: { month: 'month', year: 'year' },
    stillNeeded: 'Still needed to work out your calories:', saving: 'Saving…', save: 'Save & return to WhatsApp',
    startEyebrow: 'Before WhatsApp', next: 'Next', stepOf: (n, t) => `${n} of ${t}`,
    startSub: 'So your coach knows your calorie target from your first message.',
    startSave: 'Continue on WhatsApp', alreadySetUp: 'Already set up? Open WhatsApp',
    opening: 'Opening WhatsApp…', sendHint: "Tap send on the message that's waiting for you.",
    didntOpen: "Didn't open?", openWhatsApp: 'Open WhatsApp',
    scanTitle: 'Scan with your phone', scanBody: 'WhatsApp opens with your profile ready. Send the message to start.',
    openHere: 'Or open WhatsApp on this computer',
  },
  fr: {
    loading: 'Chargement de ton profil…', missingLink: 'Lien de profil manquant ou invalide.',
    loadFailed: 'Impossible de charger ton profil.', saveFailed: "Impossible d'enregistrer ton profil.",
    yourProfile: 'Ton profil', pinned: 'Épinglé dans ta discussion', back: 'Retour', you: 'Toi',
    aboutYou: 'À propos de toi', name: 'Prénom', namePlaceholder: 'Ton prénom',
    gender: 'Sexe', male: 'Homme', female: 'Femme',
    birthYear: 'Année de naissance', yearsOld: (n) => `${n} ans`,
    height: 'Taille', heightPlaceholder: '175cm', heightHint: 'En cm',
    goalActivity: 'Objectif et activité', yourGoal: 'Ton objectif',
    goals: { lose_weight: 'Perdre du poids', build_muscle: 'Prendre du muscle', maintain_weight: 'Garder mon poids', get_healthier: 'Être en meilleure santé' },
    units: 'Unités', kilograms: 'Kilogrammes (kg)', pounds: 'Livres (lb)',
    currentWeight: 'Poids actuel', startingWeight: 'Poids de départ',
    startingWeightHint: 'Ton poids quand tu as commencé avec Getfiit',
    targetWeight: 'Poids cible', setTarget: 'Fixer un objectif', skip: 'Passer', targetValue: 'Valeur cible',
    activityLevel: "Niveau d'activité",
    activity: { sedentary: 'Sédentaire', lightly_active: 'Activité légère', moderately_active: 'Activité modérée', very_active: 'Activité intense', extra_active: 'Activité très intense' },
    dietPref: 'Préférence alimentaire',
    diets: { balanced: 'Équilibrée', low_fat: 'Pauvre en graisses', low_carb: 'Pauvre en glucides', high_protein: 'Riche en protéines' },
    preferences: 'Préférences', language: 'Langue', timezone: 'Fuseau horaire',
    timezoneHint: 'Sert à caler ton point quotidien', dailyCheckin: 'Point quotidien', noReminder: 'Pas de rappel',
    checkinLabel: (t) => t,
    subscription: 'Abonnement', trial: 'Essai', active: 'Actif', renews: (d) => `renouvellement le ${d}`,
    manageSubscription: "Gérer l'abonnement", intervals: { month: 'mois', year: 'an' },
    stillNeeded: 'Encore nécessaire pour calculer tes calories :', saving: 'Enregistrement…', save: 'Enregistrer et revenir sur WhatsApp',
    startEyebrow: 'Avant WhatsApp', next: 'Suivant', stepOf: (n, t) => `${n} sur ${t}`,
    startSub: 'Pour que ton coach connaisse ton objectif calorique dès ton premier message.',
    startSave: 'Continuer sur WhatsApp', alreadySetUp: 'Déjà inscrit ? Ouvrir WhatsApp',
    opening: 'Ouverture de WhatsApp…', sendHint: 'Appuie sur Envoyer, le message est déjà prêt.',
    didntOpen: "Ça ne s'est pas ouvert ?", openWhatsApp: 'Ouvrir WhatsApp',
    scanTitle: 'Scanne avec ton téléphone', scanBody: "WhatsApp s'ouvre avec ton profil prêt. Envoie le message pour commencer.",
    openHere: 'Ou ouvre WhatsApp sur cet ordinateur',
  },
  es: {
    loading: 'Cargando tu perfil…', missingLink: 'Falta el enlace del perfil o no es válido.',
    loadFailed: 'No se pudo cargar tu perfil.', saveFailed: 'No se pudo guardar tu perfil.',
    yourProfile: 'Tu perfil', pinned: 'Fijado en tu chat', back: 'Atrás', you: 'Tú',
    aboutYou: 'Sobre ti', name: 'Nombre', namePlaceholder: 'Tu nombre',
    gender: 'Sexo', male: 'Hombre', female: 'Mujer',
    birthYear: 'Año de nacimiento', yearsOld: (n) => `${n} años`,
    height: 'Altura', heightPlaceholder: '175cm', heightHint: 'En cm',
    goalActivity: 'Objetivo y actividad', yourGoal: 'Tu objetivo',
    goals: { lose_weight: 'Perder peso', build_muscle: 'Ganar músculo', maintain_weight: 'Mantener mi peso', get_healthier: 'Mejorar mi salud' },
    units: 'Unidades', kilograms: 'Kilogramos (kg)', pounds: 'Libras (lb)',
    currentWeight: 'Peso actual', startingWeight: 'Peso inicial',
    startingWeightHint: 'Tu peso cuando empezaste con Getfiit',
    targetWeight: 'Peso objetivo', setTarget: 'Fijar un objetivo', skip: 'Omitir', targetValue: 'Valor objetivo',
    activityLevel: 'Nivel de actividad',
    activity: { sedentary: 'Vida sedentaria', lightly_active: 'Actividad ligera', moderately_active: 'Actividad moderada', very_active: 'Actividad alta', extra_active: 'Actividad muy alta' },
    dietPref: 'Preferencia alimentaria',
    diets: { balanced: 'Equilibrada', low_fat: 'Baja en grasas', low_carb: 'Baja en carbohidratos', high_protein: 'Alta en proteínas' },
    preferences: 'Preferencias', language: 'Idioma', timezone: 'Zona horaria',
    timezoneHint: 'Sirve para enviarte el check-in diario a tu hora', dailyCheckin: 'Check-in diario', noReminder: 'Sin recordatorio',
    checkinLabel: (t) => t,
    subscription: 'Suscripción', trial: 'Prueba', active: 'Activa', renews: (d) => `se renueva el ${d}`,
    manageSubscription: 'Gestionar suscripción', intervals: { month: 'mes', year: 'año' },
    stillNeeded: 'Falta para calcular tus calorías:', saving: 'Guardando…', save: 'Guardar y volver a WhatsApp',
    startEyebrow: 'Antes de WhatsApp', next: 'Siguiente', stepOf: (n, t) => `${n} de ${t}`,
    startSub: 'Para que tu coach conozca tu objetivo de calorías desde tu primer mensaje.',
    startSave: 'Seguir en WhatsApp', alreadySetUp: '¿Ya tienes perfil? Abrir WhatsApp',
    opening: 'Abriendo WhatsApp…', sendHint: 'Toca enviar, el mensaje ya está listo.',
    didntOpen: '¿No se abrió?', openWhatsApp: 'Abrir WhatsApp',
    scanTitle: 'Escanea con tu teléfono', scanBody: 'WhatsApp se abre con tu perfil listo. Envía el mensaje para empezar.',
    openHere: 'O abre WhatsApp en este ordenador',
  },
  pt: {
    loading: 'Carregando seu perfil…', missingLink: 'Link do perfil ausente ou inválido.',
    loadFailed: 'Não foi possível carregar seu perfil.', saveFailed: 'Não foi possível salvar seu perfil.',
    yourProfile: 'Seu perfil', pinned: 'Fixado na sua conversa', back: 'Voltar', you: 'Você',
    aboutYou: 'Sobre você', name: 'Nome', namePlaceholder: 'Seu nome',
    gender: 'Sexo', male: 'Masculino', female: 'Feminino',
    birthYear: 'Ano de nascimento', yearsOld: (n) => `${n} anos`,
    height: 'Altura', heightPlaceholder: '175cm', heightHint: 'Em cm',
    goalActivity: 'Objetivo e atividade', yourGoal: 'Seu objetivo',
    goals: { lose_weight: 'Perder peso', build_muscle: 'Ganhar músculo', maintain_weight: 'Manter o peso', get_healthier: 'Ter mais saúde' },
    units: 'Unidades', kilograms: 'Quilogramas (kg)', pounds: 'Libras (lb)',
    currentWeight: 'Peso atual', startingWeight: 'Peso inicial',
    startingWeightHint: 'Seu peso quando você começou no Getfiit',
    targetWeight: 'Peso desejado', setTarget: 'Definir uma meta', skip: 'Pular', targetValue: 'Valor da meta',
    activityLevel: 'Nível de atividade',
    activity: { sedentary: 'Vida sedentária', lightly_active: 'Atividade leve', moderately_active: 'Atividade moderada', very_active: 'Atividade intensa', extra_active: 'Atividade muito intensa' },
    dietPref: 'Preferência alimentar',
    diets: { balanced: 'Equilibrada', low_fat: 'Pouca gordura', low_carb: 'Low carb', high_protein: 'Rica em proteína' },
    preferences: 'Preferências', language: 'Idioma', timezone: 'Fuso horário',
    timezoneHint: 'Usado para o horário do seu check-in diário', dailyCheckin: 'Check-in diário', noReminder: 'Sem lembrete',
    checkinLabel: (t) => t,
    subscription: 'Assinatura', trial: 'Teste', active: 'Ativa', renews: (d) => `renova em ${d}`,
    manageSubscription: 'Gerenciar assinatura', intervals: { month: 'mês', year: 'ano' },
    stillNeeded: 'Falta para calcular suas calorias:', saving: 'Salvando…', save: 'Salvar e voltar ao WhatsApp',
    startEyebrow: 'Antes do WhatsApp', next: 'Próximo', stepOf: (n, t) => `${n} de ${t}`,
    startSub: 'Para o seu coach saber sua meta de calorias desde a primeira mensagem.',
    startSave: 'Continuar no WhatsApp', alreadySetUp: 'Já tem perfil? Abrir o WhatsApp',
    opening: 'Abrindo o WhatsApp…', sendHint: 'Toque em enviar, a mensagem já está pronta.',
    didntOpen: 'Não abriu?', openWhatsApp: 'Abrir o WhatsApp',
    scanTitle: 'Escaneie com o celular', scanBody: 'O WhatsApp abre com seu perfil pronto. Envie a mensagem para começar.',
    openHere: 'Ou abra o WhatsApp neste computador',
  },
  de: {
    loading: 'Dein Profil wird geladen…', missingLink: 'Profil-Link fehlt oder ist ungültig.',
    loadFailed: 'Dein Profil konnte nicht geladen werden.', saveFailed: 'Dein Profil konnte nicht gespeichert werden.',
    yourProfile: 'Dein Profil', pinned: 'In deinem Chat angepinnt', back: 'Zurück', you: 'Du',
    aboutYou: 'Über dich', name: 'Name', namePlaceholder: 'Dein Name',
    gender: 'Geschlecht', male: 'Männlich', female: 'Weiblich',
    birthYear: 'Geburtsjahr', yearsOld: (n) => `${n} Jahre alt`,
    height: 'Größe', heightPlaceholder: '175cm', heightHint: 'In cm',
    goalActivity: 'Ziel & Aktivität', yourGoal: 'Dein Ziel',
    goals: { lose_weight: 'Abnehmen', build_muscle: 'Muskeln aufbauen', maintain_weight: 'Gewicht halten', get_healthier: 'Gesünder leben' },
    units: 'Einheiten', kilograms: 'Kilogramm (kg)', pounds: 'Pfund (lb)',
    currentWeight: 'Aktuelles Gewicht', startingWeight: 'Startgewicht',
    startingWeightHint: 'Dein Gewicht, als du mit Getfiit angefangen hast',
    targetWeight: 'Zielgewicht', setTarget: 'Ziel festlegen', skip: 'Überspringen', targetValue: 'Zielwert',
    activityLevel: 'Aktivitätslevel',
    activity: { sedentary: 'Kaum aktiv', lightly_active: 'Leicht aktiv', moderately_active: 'Mäßig aktiv', very_active: 'Sehr aktiv', extra_active: 'Extrem aktiv' },
    dietPref: 'Ernährungsweise',
    diets: { balanced: 'Ausgewogen', low_fat: 'Fettarm', low_carb: 'Kohlenhydratarm', high_protein: 'Proteinreich' },
    preferences: 'Einstellungen', language: 'Sprache', timezone: 'Zeitzone',
    timezoneHint: 'Damit dein täglicher Check-in zur richtigen Zeit kommt', dailyCheckin: 'Täglicher Check-in', noReminder: 'Keine Erinnerung',
    checkinLabel: (t) => t,
    subscription: 'Abo', trial: 'Testphase', active: 'Aktiv', renews: (d) => `verlängert sich am ${d}`,
    manageSubscription: 'Abo verwalten', intervals: { month: 'Monat', year: 'Jahr' },
    stillNeeded: 'Fehlt noch, um deine Kalorien zu berechnen:', saving: 'Wird gespeichert…', save: 'Speichern & zurück zu WhatsApp',
    startEyebrow: 'Vor WhatsApp', next: 'Weiter', stepOf: (n, t) => `${n} von ${t}`,
    startSub: 'Damit dein Coach dein Kalorienziel ab deiner ersten Nachricht kennt.',
    startSave: 'Weiter zu WhatsApp', alreadySetUp: 'Schon eingerichtet? WhatsApp öffnen',
    opening: 'WhatsApp wird geöffnet…', sendHint: 'Tippe auf Senden, die Nachricht ist schon fertig.',
    didntOpen: 'Nicht geöffnet?', openWhatsApp: 'WhatsApp öffnen',
    scanTitle: 'Mit dem Handy scannen', scanBody: 'WhatsApp öffnet sich mit deinem fertigen Profil. Schick die Nachricht ab, um loszulegen.',
    openHere: 'Oder WhatsApp auf diesem Computer öffnen',
  },
  it: {
    loading: 'Caricamento del profilo…', missingLink: 'Link del profilo mancante o non valido.',
    loadFailed: 'Impossibile caricare il tuo profilo.', saveFailed: 'Impossibile salvare il tuo profilo.',
    yourProfile: 'Il tuo profilo', pinned: 'Fissato nella tua chat', back: 'Indietro', you: 'Tu',
    aboutYou: 'Su di te', name: 'Nome', namePlaceholder: 'Il tuo nome',
    gender: 'Sesso', male: 'Uomo', female: 'Donna',
    birthYear: 'Anno di nascita', yearsOld: (n) => `${n} anni`,
    height: 'Altezza', heightPlaceholder: '175cm', heightHint: 'In cm',
    goalActivity: 'Obiettivo e attività', yourGoal: 'Il tuo obiettivo',
    goals: { lose_weight: 'Perdere peso', build_muscle: 'Mettere massa', maintain_weight: 'Mantenere il peso', get_healthier: 'Stare meglio' },
    units: 'Unità', kilograms: 'Chilogrammi (kg)', pounds: 'Libbre (lb)',
    currentWeight: 'Peso attuale', startingWeight: 'Peso iniziale',
    startingWeightHint: 'Il tuo peso quando hai iniziato con Getfiit',
    targetWeight: 'Peso obiettivo', setTarget: 'Imposta un obiettivo', skip: 'Salta', targetValue: 'Valore obiettivo',
    activityLevel: 'Livello di attività',
    activity: { sedentary: 'Vita sedentaria', lightly_active: 'Attività leggera', moderately_active: 'Attività moderata', very_active: 'Attività intensa', extra_active: 'Attività molto intensa' },
    dietPref: 'Preferenza alimentare',
    diets: { balanced: 'Equilibrata', low_fat: 'Povera di grassi', low_carb: 'Povera di carboidrati', high_protein: 'Ricca di proteine' },
    preferences: 'Preferenze', language: 'Lingua', timezone: 'Fuso orario',
    timezoneHint: "Serve per inviarti il check-in giornaliero all'ora giusta", dailyCheckin: 'Check-in giornaliero', noReminder: 'Nessun promemoria',
    checkinLabel: (t) => t,
    subscription: 'Abbonamento', trial: 'Prova', active: 'Attivo', renews: (d) => `si rinnova il ${d}`,
    manageSubscription: "Gestisci l'abbonamento", intervals: { month: 'mese', year: 'anno' },
    stillNeeded: 'Manca per calcolare le tue calorie:', saving: 'Salvataggio…', save: 'Salva e torna su WhatsApp',
    startEyebrow: 'Prima di WhatsApp', next: 'Avanti', stepOf: (n, t) => `${n} di ${t}`,
    startSub: 'Così il tuo coach conosce il tuo obiettivo di calorie dal primo messaggio.',
    startSave: 'Continua su WhatsApp', alreadySetUp: 'Hai già un profilo? Apri WhatsApp',
    opening: 'Apertura di WhatsApp…', sendHint: 'Tocca invia, il messaggio è già pronto.',
    didntOpen: 'Non si è aperto?', openWhatsApp: 'Apri WhatsApp',
    scanTitle: 'Scansiona con il telefono', scanBody: 'WhatsApp si apre con il tuo profilo pronto. Invia il messaggio per iniziare.',
    openHere: 'Oppure apri WhatsApp su questo computer',
  },
};

const pickLang = (language) => (STRINGS[language] ? language : 'en');
const browserLang = () => pickLang(String(navigator.language || '').toLowerCase().split('-')[0]);

const CopyContext = React.createContext(STRINGS.en);
const useCopy = () => React.useContext(CopyContext);

// ───────── Helpers ─────────

function formatPriceCents(cents, currency) {
  if (cents == null) return '';
  const symbol = currency === 'EUR' ? '€' : currency === 'GBP' ? '£' : '$';
  return `${symbol}${(cents / 100).toFixed(0)}`;
}

function formatRenewDate(iso) {
  if (!iso) return '';
  try { return new Date(iso).toLocaleDateString(undefined, { month: 'long', day: 'numeric', year: 'numeric' }); }
  catch { return ''; }
}

function getTokenFromUrl() {
  return new URL(window.location.href).searchParams.get('t') || '';
}

function displayWeight(kg, unit) {
  if (kg == null || isNaN(kg)) return null;
  return unit === 'lb' ? +(kg * 2.20462).toFixed(1) : +kg;
}

// ───────── App ─────────

function App() {
  const token = useMemo(getTokenFromUrl, []);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [profile, setProfile] = useState(null);
  const [subscription, setSubscription] = useState(null);
  const [waReturnUrl, setWaReturnUrl] = useState('');
  const [waManageSubscriptionUrl, setWaManageSubscriptionUrl] = useState('');
  const [saving, setSaving] = useState(false);
  /*
   * Onboarding shows six fields; a returning user gets the full editor.
   * Same page, same save endpoint — only the fields on screen differ.
   */
  const [isOnboarding, setIsOnboarding] = useState(false);

  useEffect(() => {
    if (!token) { setError(STRINGS[browserLang()].missingLink); setLoading(false); return; }
    fetch(`/api/profile?t=${encodeURIComponent(token)}`)
      .then(async (r) => {
        if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || STRINGS[browserLang()].loadFailed);
        return r.json();
      })
      .then((data) => {
        // Normalise stored timezone to a value in our dropdown list
        const p = { ...data.profile, timezone: resolveTimezone(data.profile?.timezone) };
        setProfile(p);
        setIsOnboarding(data.onboarding_completed !== true);
        setSubscription(data.subscription || null);
        setWaReturnUrl(data.waReturnUrl || '');
        setWaManageSubscriptionUrl(data.waManageSubscriptionUrl || '');
        setLoading(false);
      })
      .catch((e) => { setError(e.message); setLoading(false); });
  }, [token]);

  const set = useCallback((key) => (value) => setProfile((p) => ({ ...p, [key]: value })), []);
  const setUnit = useCallback((unit) => {
    setProfile((p) => (!p || p.unit === unit) ? (p ? { ...p, unit } : p) : { ...p, unit });
  }, []);

  const save = useCallback(async () => {
    if (!profile) return;
    if (isOnboarding && missingOnboardingFields(profile).length) return;
    setSaving(true);
    setError(null);
    try {
      /*
       * The six fields onboarding asks for. Everything else is either
       * inferred at signup or captured in chat later — and crucially, the
       * onboarding form must NOT send those keys at all. Sending
       * `timezone: 'UTC'` because the field was never rendered would
       * overwrite the value inferred from the user's dialling code.
       */
      const payload = {
        gender: profile.gender || null,
        birth_year: profile.birth_year ? Number(profile.birth_year) : null,
        height_cm: profile.height_cm ? Number(profile.height_cm) : null,
        goal: profile.goal || null,
        current_weight_kg: profile.current_weight_kg != null ? Number(profile.current_weight_kg) : null,
        activity_level: profile.activity_level || null,
      };

      /*
       * Except the phone's own timezone: it beats the server's guess from the
       * dialling code, which is wrong for travellers, for countries spanning
       * zones, and for countries the server does not know.
       */
      const deviceTimeZone = browserTimeZone();
      if (isOnboarding && deviceTimeZone) payload.timezone = deviceTimeZone;

      if (!isOnboarding) {
        Object.assign(payload, {
          name: profile.name?.trim() || null,
          unit: profile.unit || 'kg',
          starting_weight_kg:
            profile.starting_weight_kg != null
              ? Number(profile.starting_weight_kg)
              : profile.current_weight_kg != null
                ? Number(profile.current_weight_kg)
                : null,
          target_weight_kg: profile.has_target && profile.target_weight_kg != null ? Number(profile.target_weight_kg) : null,
          diet_type: profile.diet_type || null,
          language: profile.language || 'en',
          timezone: profile.timezone || 'UTC',
          daily_checkin_time: profile.daily_checkin_time || null,
        });
      }
      const res = await fetch(`/api/profile?t=${encodeURIComponent(token)}`, {
        method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) throw new Error(data.error || STRINGS[pickLang(profile.language)].saveFailed);
      // The welcome, and the free first meal, wait in the chat.
      const url = data.waReturnUrl || waReturnUrl;
      if (url) window.location.href = url;
      else setSaving(false);
    } catch (e) {
      setError(e.message);
      setSaving(false);
    }
  }, [profile, token, waReturnUrl, isOnboarding]);

  const lang = profile ? pickLang(profile.language) : browserLang();
  useEffect(() => { document.documentElement.lang = lang; }, [lang]);

  if (loading) return <CenteredMessage>{STRINGS[lang].loading}</CenteredMessage>;
  if (error && !profile) return <CenteredMessage tone="error">{error}</CenteredMessage>;

  return (
    <CopyContext.Provider value={STRINGS[lang]}>
      <Page profile={profile} subscription={subscription} waManageSubscriptionUrl={waManageSubscriptionUrl} set={set} setUnit={setUnit} save={save} saving={saving} error={error} isOnboarding={isOnboarding} />
    </CopyContext.Provider>
  );
}

function CenteredMessage({ children, tone }) {
  return (
    <div className={`lp pf-message${tone === 'error' ? ' is-error' : ''}`}>
      <img src="logo.svg" alt="Getfiit" width="84" height="22" />
      <p role={tone === 'error' ? 'alert' : undefined}>{children}</p>
    </div>
  );
}

// ───────── /start ─────────

/* An event for the website funnel, where analytics runs (gf-analytics.js). */
const capture = (event, properties) => window.GfAnalytics && window.GfAnalytics.capture(event, properties);

/* Pounds where the server would guess them from the phone number (+1, in
 * helpers/profile-inference.ts): the United States and Canada. */
function browserUnit() {
  const region = String(navigator.language || '').split('-')[1] || '';
  return ['US', 'CA', 'PR'].includes(region.toUpperCase()) ? 'lb' : 'kg';
}

/*
 * The questions on getfiit.co/start, one to a screen. A choice moves on by
 * itself; a typed answer waits for Next. The six the calorie target is
 * worked out from can't be skipped (ONBOARDING_REQUIRED). The others can,
 * and the coach asks for those in the chat later (helpers/profile-gaps.ts in
 * apps/server). A target weight is only asked of someone losing weight or
 * building muscle.
 */
const START_STEPS = [
  { key: 'name', label: 'name', optional: true },
  { key: 'gender', label: 'gender' },
  { key: 'birth_year', label: 'birthYear' },
  { key: 'height_cm', label: 'height' },
  { key: 'goal', label: 'yourGoal' },
  { key: 'current_weight_kg', label: 'currentWeight' },
  {
    key: 'target_weight_kg', label: 'targetWeight', optional: true,
    when: (p) => p.goal === 'lose_weight' || p.goal === 'build_muscle',
  },
  { key: 'activity_level', label: 'activityLevel' },
  { key: 'diet_type', label: 'dietPref', optional: true },
  { key: 'daily_checkin_time', label: 'dailyCheckin', optional: true },
];

const stepsFor = (profile) => START_STEPS.filter((step) => !step.when || step.when(profile));

/* Whether an answer is one the server takes (RANGES in server.js). */
function answered(key, value) {
  if (value == null || value === '') return false;
  const n = Number(value);
  switch (key) {
    case 'name': return String(value).trim().length > 0;
    case 'birth_year': return n >= 1900 && n <= new Date().getFullYear() - 5;
    case 'height_cm': return n >= 50 && n <= 260;
    case 'current_weight_kg':
    case 'target_weight_kg': return n >= 20 && n <= 500;
    default: return true;
  }
}

/*
 * getfiit.co/start: the profile, before WhatsApp, for a visitor whose
 * profile this browser hasn't saved (gf-start.js sends them here). The
 * answers go to the server under a Start code (POST /api/start-profile in
 * server.js), and the first WhatsApp message carries that code, so the coach
 * knows them before the visitor says hi. This browser keeps only the code.
 *
 * The time zone comes from the browser, and so do the units, which stay on
 * the weight questions: a phone set to US English is no proof of pounds. So
 * does the language, until the visitor picks another from the flags.
 */
function StartApp() {
  const [lang, setLang] = useState(() => pickLang(window.GfI18n ? window.GfI18n.detectLanguage() : browserLang()));
  const s = STRINGS[lang];
  const [saved] = useState(() => (window.GfStart ? window.GfStart.savedCode() : null));
  const [profile, setProfile] = useState(() => ({ unit: browserUnit() }));
  const [index, setIndex] = useState(0);
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);
  // Set once there is a way into WhatsApp: the code saved before, or now.
  const [leaving, setLeaving] = useState(saved ? { code: saved } : null);
  // A choice is moving on: a second tap in that beat must not skip a question.
  const moving = React.useRef(false);

  const steps = stepsFor(profile);
  const step = steps[Math.min(index, steps.length - 1)];
  const last = index >= steps.length - 1;
  const ready = answered(step.key, profile[step.key]);

  useEffect(() => { document.documentElement.lang = lang; }, [lang]);
  useEffect(() => { if (!saved) capture('PROFILE_FORM_OPENED'); }, []);
  useEffect(() => { window.scrollTo(0, 0); }, [index]);

  const set = useCallback((key) => (value) => setProfile((p) => ({ ...p, [key]: value })), []);

  // The page's language, and the Start word's, so the coach answers in it
  // too. Kept in the address as ?lang=, which gf-i18n.js reads, so a reload
  // keeps it.
  const chooseLanguage = (next) => {
    setLang(next);
    try {
      const url = new URL(window.location.href);
      url.searchParams.set('lang', next);
      history.replaceState(history.state, '', url.pathname + url.search + url.hash);
    } catch (e) { /* the choice holds for this page */ }
  };

  const submit = async (p) => {
    setSaving(true);
    setError(null);
    const analytics = window.GfAnalytics;
    const asked = stepsFor(p).map((st) => st.key);
    const payload = {
      name: answered('name', p.name) ? p.name.trim() : null,
      gender: p.gender,
      birth_year: Number(p.birth_year),
      height_cm: Number(p.height_cm),
      goal: p.goal,
      current_weight_kg: Number(p.current_weight_kg),
      target_weight_kg: asked.includes('target_weight_kg') && answered('target_weight_kg', p.target_weight_kg)
        ? Number(p.target_weight_kg)
        : null,
      activity_level: p.activity_level,
      diet_type: p.diet_type || null,
      daily_checkin_time: p.daily_checkin_time || null,
      unit: p.unit,
      language: lang,
      timezone: browserTimeZone(),
      // This visit's analytics code, where analytics is allowed. The server
      // saves the answers under it, so the visit joins the chat too.
      code: analytics ? analytics.startCode() : null,
    };

    let res = null;
    let data = {};
    try {
      res = await fetch('/api/start-profile', {
        method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload),
      });
      data = await res.json().catch(() => ({}));
    } catch (e) { /* offline or blocked: on to WhatsApp regardless, below */ }

    if (res && res.status === 400) {
      setError(data.error || s.saveFailed);
      setSaving(false);
      return;
    }
    if (res && res.ok && data.code) {
      window.GfStart.save(data.code);
      capture('PROFILE_FORM_SUBMITTED');
      setLeaving({ code: data.code });
      return;
    }
    // Not saved: WhatsApp anyway, where the coach sends the profile link, as
    // it did before this page existed.
    setLeaving({ code: payload.code });
  };

  // The next question, or WhatsApp after the last. `p` is the profile with
  // the answer just given, which state may not have caught up with.
  const next = (p = profile) => {
    if (index >= stepsFor(p).length - 1) submit(p);
    else setIndex((i) => i + 1);
  };

  const choose = (key, value) => {
    if (moving.current) return;
    moving.current = true;
    const p = { ...profile, [key]: value };
    setProfile(p);
    // A beat to see the choice land before the question changes.
    setTimeout(() => { moving.current = false; next(p); }, 180);
  };

  if (leaving) {
    return (
      <CopyContext.Provider value={s}>
        <ToWhatsApp link={window.GfI18n.waLink(lang, leaving.code)} />
      </CopyContext.Provider>
    );
  }

  const canGo = (ready || step.optional) && !saving;
  return (
    <CopyContext.Provider value={s}>
      <form className="lp pf" onSubmit={(e) => { e.preventDefault(); if (canGo) next(); }}>
        <Nav start onBack={index > 0 ? () => setIndex((i) => i - 1) : undefined} />
        <header className="pf-hero">
          <div className="pf-hero-field">
            <div className="pf-hero-copy">
              <LanguageFlags lang={lang} onChange={chooseLanguage} />
              <p className="pf-eyebrow">{s.startEyebrow} · {s.stepOf(index + 1, steps.length)}</p>
              <h1 className="pf-name">{s[step.label]}</h1>
              {index === 0 && <p className="pf-hero-sub">{s.startSub}</p>}
              <div className="pf-progress" aria-hidden="true">
                <i style={{ width: `${((index + 1) / steps.length) * 100}%` }} />
              </div>
            </div>
          </div>
        </header>
        <main className="pf-main">
          <section className="pf-card">
            <StartQuestion key={step.key} step={step} profile={profile} set={set} choose={choose} />
          </section>
          {error && <p className="pf-error" role="alert">{error}</p>}
        </main>
        <div className="pf-savebar">
          <div className="pf-savebar-inner">
            <button type="submit" className="pf-save" disabled={!canGo} aria-busy={saving}>
              {last && <WhatsappGlyph />}
              {saving ? s.saving : last ? s.startSave : ready || !step.optional ? s.next : s.skip}
            </button>
            {/* Someone with a profile on another browser or phone: WhatsApp
                without the questions, where the coach already knows them. */}
            {index === 0 && (
              <a className="pf-already" href={window.GfI18n.waLink(lang, null)} data-wa-return>{s.alreadySetUp}</a>
            )}
          </div>
        </div>
      </form>
    </CopyContext.Provider>
  );
}

/* The six languages the coach speaks, as flags over every question. */
function LanguageFlags({ lang, onChange }) {
  const s = useCopy();
  return (
    <div className="pf-flags" role="group" aria-label={s.language}>
      {LANGUAGES.map(({ id, label }) => {
        const [flag, ...name] = label.split(' ');
        return (
          <button
            key={id} type="button" className="pf-flag" aria-pressed={id === lang} aria-label={name.join(' ')}
            title={name.join(' ')} onClick={() => onChange(id)}
          >
            {flag}
          </button>
        );
      })}
    </div>
  );
}

/* One question's control. A choice goes through `choose`, which moves on. */
function StartQuestion({ step, profile, set, choose }) {
  const s = useCopy();
  const unit = profile.unit || 'kg';

  const pills = (ids, labelOf) => (
    <PillRow>
      {ids.map((id) => (
        <Pill key={id} active={profile[step.key] === id} onClick={() => choose(step.key, id)}>{labelOf(id)}</Pill>
      ))}
    </PillRow>
  );

  // Kept in kg, shown and typed in the unit picked above it.
  const weight = () => {
    const setKg = (val) => {
      const n = parseFloat(String(val).replace(/[^\d.]/g, ''));
      set(step.key)(isNaN(n) ? null : +(unit === 'lb' ? n / 2.20462 : n).toFixed(2));
    };
    return (
      <div className="pf-step">
        <PillRow>
          <Pill active={unit === 'kg'} onClick={() => set('unit')('kg')}>{s.kilograms}</Pill>
          <Pill active={unit === 'lb'} onClick={() => set('unit')('lb')}>{s.pounds}</Pill>
        </PillRow>
        <TextInput value={displayWeight(profile[step.key], unit) ?? ''} onChange={setKg} suffix={unit} inputMode="decimal" />
      </div>
    );
  };

  switch (step.key) {
    case 'name':
      return <TextInput value={profile.name} onChange={set('name')} placeholder={s.namePlaceholder} />;
    case 'gender':
      return pills(['male', 'female'], (id) => s[id]);
    case 'birth_year': {
      const age = profile.birth_year ? new Date().getFullYear() - Number(profile.birth_year) : null;
      return (
        <div className="pf-step">
          <TextInput value={profile.birth_year ?? ''} onChange={set('birth_year')} inputMode="numeric" placeholder="1990" />
          {age != null && age >= 5 && age <= 120 && <div className="pf-hint">{s.yearsOld(age)}</div>}
        </div>
      );
    }
    case 'height_cm':
      return (
        <div className="pf-step">
          <HeightInput value={profile.height_cm} onChange={set('height_cm')} />
          <div className="pf-hint">{s.heightHint}</div>
        </div>
      );
    case 'goal':
      return pills(GOALS, (id) => s.goals[id]);
    case 'current_weight_kg':
    case 'target_weight_kg':
      return weight();
    case 'activity_level':
      return pills(ACTIVITY, (id) => s.activity[id]);
    case 'diet_type':
      return pills(DIETS, (id) => s.diets[id]);
    case 'daily_checkin_time':
      return (
        <PillRow>
          {CHECKIN_TIMES.map((t) => (
            <Pill key={t} active={profile.daily_checkin_time === t} onClick={() => choose(step.key, t)}>{s.checkinLabel(t)}</Pill>
          ))}
          <Pill active={profile.daily_checkin_time === null} onClick={() => choose(step.key, null)}>{s.noReminder}</Pill>
        </PillRow>
      );
    default:
      return null;
  }
}

/*
 * On to WhatsApp, with the message ready to send. A phone goes at once, and
 * gets a button if the jump stalls, as it can in TikTok's or Instagram's own
 * browser. A computer shows a QR code for the phone (gf-qr.js), and the link
 * for WhatsApp on the computer under it.
 */
function ToWhatsApp({ link }) {
  const s = useCopy();
  const phone = useMemo(() => !!(window.matchMedia && window.matchMedia('(pointer: coarse)').matches), []);
  const [stalled, setStalled] = useState(false);

  useEffect(() => {
    if (!phone || !link) return undefined;
    window.location.href = link;
    const timer = setTimeout(() => setStalled(true), 1500);
    return () => clearTimeout(timer);
  }, [phone, link]);

  if (!link) return <CenteredMessage tone="error">{s.saveFailed}</CenteredMessage>;

  if (!phone) {
    return (
      <div className="lp pf-message pf-door">
        <img src="logo.svg" alt="Getfiit" width="84" height="22" />
        <h1 className="pf-door-title">{s.scanTitle}</h1>
        {window.GfQrCode && (
          <div className="pf-door-qr">
            <window.GfQrCode size={168} value={link} title={s.scanTitle} />
          </div>
        )}
        <p className="pf-door-body">{s.scanBody}</p>
        <a className="pf-door-later" href={link} data-wa-return>{s.openHere}</a>
      </div>
    );
  }

  return (
    <div className="lp pf-message pf-door">
      <img src="logo.svg" alt="Getfiit" width="84" height="22" />
      <h1 className="pf-door-title">{s.opening}</h1>
      <p className="pf-door-body">{s.sendHint}</p>
      {stalled && (
        <React.Fragment>
          <p className="pf-door-body">{s.didntOpen}</p>
          <a className="pf-save pf-door-btn" href={link} data-wa-return><WhatsappGlyph />{s.openWhatsApp}</a>
        </React.Fragment>
      )}
    </div>
  );
}

// The landing page's floating capsule, with a way back to the chat. On
// /start, where there is no chat yet, back is the previous question.
function Nav({ start, onBack }) {
  const s = useCopy();
  return (
    <nav className="pf-nav">
      <button type="button" className="pf-back" onClick={onBack || (() => history.length > 1 ? history.back() : window.close())} aria-label={s.back}>
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M19 12H5M12 19l-7-7 7-7" /></svg>
      </button>
      <img className="pf-nav-logo" src="logo.svg" alt="Getfiit" width="84" height="22" />
      {!start && <span className="pf-nav-pinned"><PinIcon /><span>{s.pinned}</span></span>}
    </nav>
  );
}

function Chip({ icon, tone, title, sub }) {
  return (
    <div className="pf-chip">
      <span className={`pf-chip-icon pf-chip-icon--${tone}`}>{icon}</span>
      <span className="pf-chip-text"><b>{title}</b><span>{sub}</span></span>
    </div>
  );
}

// The sky field: whose profile this is, with the goal and the weights as
// the landing hero's metric chips.
function Hero({ profile }) {
  const s = useCopy();
  const unit = profile.unit || 'kg';
  const goal = s.goals[profile.goal];
  const cw = displayWeight(profile.current_weight_kg, unit);
  const tw = profile.has_target ? displayWeight(profile.target_weight_kg, unit) : null;

  return (
    <header className="pf-hero">
      <div className="pf-hero-field">
        <div className="pf-hero-copy">
          <p className="pf-eyebrow">{s.yourProfile}</p>
          <h1 className="pf-name">{profile.name || s.you}</h1>
          {(goal || cw != null) && (
            <div className="pf-chips">
              {goal && <Chip icon={<TargetIcon />} tone="green" title={goal} sub={s.yourGoal} />}
              {cw != null && (
                <Chip
                  icon={<ScaleIcon />}
                  tone="blue"
                  title={tw != null ? `${cw} → ${tw} ${unit}` : `${cw} ${unit}`}
                  sub={tw != null ? s.targetWeight : s.currentWeight}
                />
              )}
            </div>
          )}
        </div>
      </div>
    </header>
  );
}

function HeightInput({ value, onChange }) {
  const s = useCopy();
  const [raw, setRaw] = useState(value != null ? `${value}cm` : '');
  useEffect(() => { setRaw(value != null ? `${value}cm` : ''); }, []); // init once
  const handle = (s) => {
    setRaw(s);
    const cmMatch = s.match(/(\d+(?:\.\d+)?)\s*cm/i);
    if (cmMatch) return onChange(Number(cmMatch[1]));
    const ftMatch = s.match(/(\d+)\s*['′]\s*(\d+)?/);
    if (ftMatch) return onChange(Math.round(Number(ftMatch[1]) * 30.48 + Number(ftMatch[2] || 0) * 2.54));
    const num = parseFloat(s.replace(/[^\d.]/g, ''));
    if (!isNaN(num)) return onChange(num);
    onChange(null);
  };
  return <TextInput value={raw} onChange={handle} placeholder={s.heightPlaceholder} />;
}

/*
 * The fields onboarding must not finish without.
 *
 * `computeDailyTargets` on the server returns null if any of gender,
 * birth_year, current_weight_kg or height_cm is missing, and a null target
 * means no macro card on any log — silently, forever. This form had no
 * validation, so profiles exist that completed onboarding with a blank
 * height and have never seen a macros card. Goal and activity level are here
 * too because they shape the number the card shows.
 */
const ONBOARDING_REQUIRED = [
  ['gender', 'gender'],
  ['birth_year', 'birthYear'],
  ['height_cm', 'height'],
  ['goal', 'yourGoal'],
  ['current_weight_kg', 'currentWeight'],
  ['activity_level', 'activityLevel'],
];

/** The labels of the required fields still empty, in the page's language. */
function missingOnboardingFields(profile, s = STRINGS.en) {
  if (!profile) return ONBOARDING_REQUIRED.map(([, label]) => s[label]);
  return ONBOARDING_REQUIRED.filter(([key]) => !profile[key]).map(([, label]) => s[label]);
}

function AboutSection({ profile, set, isOnboarding }) {
  const s = useCopy();
  const age = profile.birth_year ? new Date().getFullYear() - Number(profile.birth_year) : null;
  return (
    <Card title={s.aboutYou}>
      {/* Already captured from the WhatsApp profile name at signup. */}
      {!isOnboarding && (
        <Field label={s.name}>
          <TextInput value={profile.name} onChange={set('name')} placeholder={s.namePlaceholder} />
        </Field>
      )}

      <Field label={s.gender}>
        <PillRow>
          <Pill active={profile.gender === 'male'} onClick={() => set('gender')('male')}>{s.male}</Pill>
          <Pill active={profile.gender === 'female'} onClick={() => set('gender')('female')}>{s.female}</Pill>
        </PillRow>
      </Field>

      <Field label={s.birthYear} hint={age != null && age >= 5 && age <= 120 ? s.yearsOld(age) : null}>
        <TextInput value={profile.birth_year ?? ''} onChange={set('birth_year')} inputMode="numeric" placeholder="1990" />
      </Field>

      <Field label={s.height} hint={s.heightHint}>
        <HeightInput value={profile.height_cm} onChange={set('height_cm')} />
      </Field>
    </Card>
  );
}

function GoalSection({ profile, set, setUnit, isOnboarding }) {
  const s = useCopy();
  const cw = displayWeight(profile.current_weight_kg, profile.unit) ?? '';
  const tw = displayWeight(profile.target_weight_kg, profile.unit) ?? '';
  const setKg = (key) => (val) => {
    const n = parseFloat(String(val).replace(/[^\d.]/g, ''));
    if (isNaN(n)) return set(key)(null);
    set(key)(+(profile.unit === 'lb' ? n / 2.20462 : n).toFixed(2));
  };
  return (
    <Card title={s.goalActivity}>
      <Field label={s.yourGoal}>
        <PillRow>{GOALS.map((id) => <Pill key={id} active={profile.goal === id} onClick={() => set('goal')(id)}>{s.goals[id]}</Pill>)}</PillRow>
      </Field>

      {!isOnboarding && (
        <Field label={s.units}>
          <PillRow>
            <Pill active={profile.unit === 'kg'} onClick={() => setUnit('kg')}>{s.kilograms}</Pill>
            <Pill active={profile.unit === 'lb'} onClick={() => setUnit('lb')}>{s.pounds}</Pill>
          </PillRow>
        </Field>
      )}

      <Field label={s.currentWeight}>
        <TextInput value={cw} onChange={setKg('current_weight_kg')} suffix={profile.unit || 'kg'} inputMode="decimal" />
      </Field>

      {/* Starting weight is derived from the first current weight server-side. */}
      {!isOnboarding && (
        <React.Fragment>
          <Field label={s.startingWeight} hint={s.startingWeightHint}>
            <TextInput
              value={displayWeight(profile.starting_weight_kg, profile.unit) ?? ''}
              onChange={setKg('starting_weight_kg')}
              suffix={profile.unit || 'kg'}
              inputMode="decimal"
            />
          </Field>

          <Field label={s.targetWeight}>
            <PillRow>
              <Pill active={!!profile.has_target} onClick={() => set('has_target')(true)}>{s.setTarget}</Pill>
              <Pill active={!profile.has_target} onClick={() => set('has_target')(false)}>{s.skip}</Pill>
            </PillRow>
            {profile.has_target && (
              <Field label={s.targetValue}>
                <TextInput value={tw} onChange={setKg('target_weight_kg')} suffix={profile.unit || 'kg'} inputMode="decimal" />
              </Field>
            )}
          </Field>
        </React.Fragment>
      )}

      <Field label={s.activityLevel}>
        <PillRow>{ACTIVITY.map((id) => <Pill key={id} active={profile.activity_level === id} onClick={() => set('activity_level')(id)}>{s.activity[id]}</Pill>)}</PillRow>
      </Field>

      {/* Asked in chat once there are enough meals to make it meaningful. */}
      {!isOnboarding && (
        <Field label={s.dietPref}>
          <PillRow>{DIETS.map((id) => <Pill key={id} active={profile.diet_type === id} onClick={() => set('diet_type')(id)}>{s.diets[id]}</Pill>)}</PillRow>
        </Field>
      )}
    </Card>
  );
}

function PreferencesSection({ profile, set }) {
  const s = useCopy();
  return (
    <Card title={s.preferences}>
      <Field label={s.language}>
        <PillRow>{LANGUAGES.map((l) => <Pill key={l.id} active={profile.language === l.id} onClick={() => set('language')(l.id)}>{l.label}</Pill>)}</PillRow>
      </Field>

      <Field label={s.timezone} hint={s.timezoneHint}>
        <SelectInput value={profile.timezone || 'UTC'} onChange={set('timezone')}>
          <option value="UTC">UTC</option>
          {TZ_REGIONS.map((region) => (
            <optgroup key={region} label={region}>
              {TIMEZONES.filter((t) => t.region === region).map((t) => (
                <option key={t.value} value={t.value}>{t.label}</option>
              ))}
            </optgroup>
          ))}
        </SelectInput>
      </Field>

      <Field label={s.dailyCheckin}>
        <PillRow>
          {CHECKIN_TIMES.map((t) => (
            <Pill key={t} active={profile.daily_checkin_time === t} onClick={() => set('daily_checkin_time')(t)}>{s.checkinLabel(t)}</Pill>
          ))}
          <Pill active={!profile.daily_checkin_time} onClick={() => set('daily_checkin_time')(null)}>{s.noReminder}</Pill>
        </PillRow>
      </Field>
    </Card>
  );
}

function SubscriptionSection({ subscription, waManageSubscriptionUrl }) {
  const s = useCopy();
  if (!subscription) return null;
  const price = formatPriceCents(subscription.priceCents, subscription.currency);
  const interval = s.intervals[subscription.interval || 'month'] || subscription.interval;
  const renew = formatRenewDate(subscription.renewsAt);
  return (
    <section className="pf-card pf-card--plan">
      <div className="pf-plan-head">
        <div className="pf-plan-copy">
          <h2 className="pf-plan-label">{s.subscription}</h2>
          <div className="pf-plan-row">
            <span className="pf-plan-name">{subscription.planName || 'Premium'}</span>
            <span className="pf-status"><i />{subscription.status === 'trialing' ? s.trial : s.active}</span>
          </div>
          {(price || renew) && <p className="pf-plan-meta">{price && `${price} / ${interval}`}{price && renew && ' · '}{renew && s.renews(renew)}</p>}
        </div>
        <span className="pf-plan-mark"><img src="logo-mark-white.svg" alt="" width="22" height="22" /></span>
      </div>

      <a className="pf-plan-btn" href={subscription.manageUrl || waManageSubscriptionUrl}>
        {s.manageSubscription}
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M7 17 17 7M8 7h9v9" />
        </svg>
      </a>
    </section>
  );
}

function PrivacyFooter() {
  return (
    <p className="pf-privacy">
      Your data is private and encrypted.<br />
      Send <b>"delete me"</b> in the chat to wipe everything.
    </p>
  );
}

function SaveBar({ onClick, saving, missing = [] }) {
  const s = useCopy();
  const blocked = missing.length > 0;
  return (
    <div className="pf-savebar">
      <div className="pf-savebar-inner">
        {blocked && <p className="pf-missing">{s.stillNeeded} <b>{missing.join(', ')}</b></p>}
        <button type="button" className="pf-save" onClick={onClick} disabled={saving || blocked} aria-busy={saving}>
          <WhatsappGlyph />
          {saving ? s.saving : s.save}
        </button>
      </div>
    </div>
  );
}

function Page({ profile, subscription, set, setUnit, save, saving, error, waManageSubscriptionUrl, isOnboarding }) {
  const s = useCopy();
  return (
    <div className="lp pf">
      <Nav />
      <Hero profile={profile} />
      <main className="pf-main">
        <AboutSection profile={profile} set={set} isOnboarding={isOnboarding} />
        <GoalSection profile={profile} set={set} setUnit={setUnit} isOnboarding={isOnboarding} />
        {/* Language, timezone and check-in time: inferred at signup or
            asked in chat later. Not worth a section on first contact. */}
        {!isOnboarding && <PreferencesSection profile={profile} set={set} />}
        {!isOnboarding && (
          <SubscriptionSection subscription={subscription} waManageSubscriptionUrl={waManageSubscriptionUrl} />
        )}
        {/* <PrivacyFooter /> */}
        {error && <p className="pf-error" role="alert">{error}</p>}
      </main>
      <SaveBar onClick={save} saving={saving} missing={isOnboarding ? missingOnboardingFields(profile, s) : []} />
    </div>
  );
}

// Two pages: /profile, from the signed link in the chat, and /start, the
// profile questions before the chat exists (start.html).
const START_PAGE = /^\/start\/?$/.test(window.location.pathname);

ReactDOM.createRoot(document.getElementById('root')).render(START_PAGE ? <StartApp /> : <App />);
