const { useState, useEffect, useMemo, useRef, useContext, createContext } = React;

/* =========================================================
   Supabase + Premium config
   ========================================================= */
const SUPABASE_URL = "https://tydoceznnmhgqzyutmmd.supabase.co";
const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InR5ZG9jZXpubm1oZ3F6eXV0bW1kIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODcxMzE2NzgsImV4cCI6MjEwMjcwNzY3OH0.J47y_E7Mds1GpX4NUnJiodKjV5wOerTQBQbPs31ONK8";
let supabase = null;
function waitForSupabaseAndBoot(timeoutMs) {
  return new Promise((resolve, reject) => {
    let poll = null;
    let onReady = null;
    function cleanup() {
      if (poll) clearInterval(poll);
      if (onReady) window.removeEventListener("supabase-ready", onReady);
    }
    function tryResolve() {
      if (window.__supabaseCreateClient) {
        supabase = window.__supabaseCreateClient(SUPABASE_URL, SUPABASE_ANON_KEY);
        cleanup();
        resolve();
        return true;
      }
      return false;
    }
    if (tryResolve()) return;
    onReady = function () { tryResolve(); };
    window.addEventListener("supabase-ready", onReady);
    const start = Date.now();
    poll = setInterval(() => {
      if (tryResolve()) return;
      if (Date.now() - start > timeoutMs) {
        cleanup();
        reject(new Error("Supabase library timed out loading (esm.sh)."));
      }
    }, 50);
  });
}

const PRICING_CONFIG = {
  yearly: { amount: 29.99, currency: "EUR", stripePaymentLink: "https://buy.stripe.com/test_4gM5kF9TH9B4faUdg1eIw00" },
  monthly: { amount: 4.99, currency: "EUR", stripePaymentLink: "https://buy.stripe.com/test_4gMeVf6HveVoe6Qfo9eIw01" },
};
const FREE_GOAL_LIMIT = 1;

const SHIFT_TYPES = [
  { code: "fruh", color: "#4A90D9", labelKey: "shiftFruh", letter: "F" },
  { code: "spat", color: "#B8FF00", labelKey: "shiftSpat", letter: "S" },
  { code: "tag", color: "#E0A64E", labelKey: "shiftTag", letter: "T" },
  { code: "nacht", color: "#6B5CA5", labelKey: "shiftNacht", letter: "N" },
  { code: "frei", color: "#3FAE7A", labelKey: "shiftFrei", letter: "R" },
  { code: "urlaub", color: "#2FB5C4", labelKey: "shiftUrlaub", letter: "U" },
  { code: "krank", color: "#E0645A", labelKey: "shiftKrank", letter: "K" },
];
// Some workplaces run a simple two-shift system (Tagdienst/Nachtdienst)
// instead of the three-way Früh/Spät/Nacht split — this resolves which
// of the two "working shift" sets should be offered in the Dienstplan
// picker, legend, and pay calculator, based on settings.shiftCalc.shiftSystem.
// "frei"/"urlaub"/"krank" are always available regardless of the system.
function workingShiftCodes(shiftSystem) {
  return shiftSystem === "2-shift" ? ["tag", "nacht"] : ["fruh", "spat", "nacht"];
}
function visibleShiftTypes(shiftSystem) {
  const working = workingShiftCodes(shiftSystem);
  return SHIFT_TYPES.filter((s) => working.includes(s.code) || ["frei", "urlaub", "krank"].includes(s.code));
}
const GERMAN_STATES = [
  { id: "baden-wuerttemberg", name: "Baden-Württemberg" },
  { id: "bayern", name: "Bayern" },
  { id: "berlin", name: "Berlin" },
  { id: "brandenburg", name: "Brandenburg" },
  { id: "bremen", name: "Bremen" },
  { id: "hamburg", name: "Hamburg" },
  { id: "hessen", name: "Hessen" },
  { id: "mecklenburg-vorpommern", name: "Mecklenburg-Vorpommern" },
  { id: "niedersachsen", name: "Niedersachsen" },
  { id: "nordrhein-westfalen", name: "Nordrhein-Westfalen" },
  { id: "rheinland-pfalz", name: "Rheinland-Pfalz" },
  { id: "saarland", name: "Saarland" },
  { id: "sachsen", name: "Sachsen" },
  { id: "sachsen-anhalt", name: "Sachsen-Anhalt" },
  { id: "schleswig-holstein", name: "Schleswig-Holstein" },
  { id: "thueringen", name: "Thüringen" },
];
const CURRENCIES = [
  { code: "EUR", name: "Euro (EUR)" },
  { code: "GBP", name: "British Pound (GBP)" },
  { code: "CHF", name: "Swiss Franc (CHF)" },
  { code: "SEK", name: "Swedish Krona (SEK)" },
  { code: "NOK", name: "Norwegian Krone (NOK)" },
  { code: "DKK", name: "Danish Krone (DKK)" },
  { code: "PLN", name: "Polish Zloty (PLN)" },
  { code: "CZK", name: "Czech Koruna (CZK)" },
  { code: "TRY", name: "Turkish Lira (TRY)" },
  { code: "MAD", name: "Moroccan Dirham (MAD)" },
  { code: "TND", name: "Tunisian Dinar (TND)" },
  { code: "DZD", name: "Algerian Dinar (DZD)" },
  { code: "EGP", name: "Egyptian Pound (EGP)" },
  { code: "SAR", name: "Saudi Riyal (SAR)" },
  { code: "AED", name: "UAE Dirham (AED)" },
  { code: "QAR", name: "Qatari Riyal (QAR)" },
  { code: "KWD", name: "Kuwaiti Dinar (KWD)" },
  { code: "BHD", name: "Bahraini Dinar (BHD)" },
  { code: "OMR", name: "Omani Rial (OMR)" },
  { code: "JPY", name: "Japanese Yen (JPY)" },
  { code: "CNY", name: "Chinese Yuan (CNY)" },
  { code: "INR", name: "Indian Rupee (INR)" },
  { code: "KRW", name: "South Korean Won (KRW)" },
  { code: "IDR", name: "Indonesian Rupiah (IDR)" },
  { code: "MYR", name: "Malaysian Ringgit (MYR)" },
  { code: "PHP", name: "Philippine Peso (PHP)" },
  { code: "THB", name: "Thai Baht (THB)" },
  { code: "PKR", name: "Pakistani Rupee (PKR)" },
  { code: "USD", name: "US Dollar (USD)" },
  { code: "CAD", name: "Canadian Dollar (CAD)" },
  { code: "MXN", name: "Mexican Peso (MXN)" },
  { code: "BRL", name: "Brazilian Real (BRL)" },
  { code: "ARS", name: "Argentine Peso (ARS)" },
  { code: "COP", name: "Colombian Peso (COP)" },
  { code: "CLP", name: "Chilean Peso (CLP)" },
  { code: "ZAR", name: "South African Rand (ZAR)" },
  { code: "NGN", name: "Nigerian Naira (NGN)" },
];
const DIENSTPLAN_TAGS = [
  { id: "training", icon: "education", labelKey: "tagTraining" },
  { id: "meeting", icon: "users", labelKey: "tagMeeting" },
  { id: "overtime", icon: "clock", labelKey: "tagOvertime" },
  { id: "shiftSwap", icon: "repeat", labelKey: "tagShiftSwap" },
  { id: "holiday", icon: "flag", labelKey: "tagHoliday" },
  { id: "note", icon: "file", labelKey: "tagNote" },
  { id: "fruit", icon: "apple", labelKey: "tagFruit" },
  { id: "snack", icon: "cookie", labelKey: "tagSnack" },
  { id: "drink", icon: "cup", labelKey: "tagDrink" },
  { id: "food", icon: "utensils", labelKey: "tagFood" },
];
function shiftColor(code) {
  const s = SHIFT_TYPES.find((s) => s.code === code);
  return s ? s.color : "#3FAE7A";
}
function shiftLetter(code) {
  const s = SHIFT_TYPES.find((s) => s.code === code);
  return s ? s.letter : "R";
}
const SUPPORT_EMAIL = "paraplaner.support@proton.me";

let deferredInstallPrompt = null;
let currentCurrency = "EUR";
window.addEventListener("beforeinstallprompt", (e) => {
  e.preventDefault();
  deferredInstallPrompt = e;
  window.dispatchEvent(new Event("pwa-install-available"));
});
window.addEventListener("appinstalled", () => {
  deferredInstallPrompt = null;
  window.dispatchEvent(new Event("pwa-install-available"));
});

/* =========================================================
   i18n
   ========================================================= */
const LANGS = ["ar", "en", "fr", "de"];
const LOCALE_MAP = { ar: "ar", en: "en-US", fr: "fr-FR", de: "de-DE" };
const DIR_MAP = { ar: "rtl", en: "ltr", fr: "ltr", de: "ltr" };
const LANG_NAMES = { ar: "العربية", en: "English", fr: "Français", de: "Deutsch" };
const LANG_KEY = "salary-planner:lang:v1";

const STRINGS = {
  appName: { ar: "PARAPLANER", en: "PARAPLANER", fr: "PARAPLANER", de: "PARAPLANER" },
  tagline: { ar: "مخطط الراتب والمصروفات", en: "Salary & expense planner", fr: "Planificateur salaire et dépenses", de: "Gehalts- und Ausgabenplaner" },
  authTaglineFull: { ar: "مخطط راتبك ومصروفاتك", en: "Your salary & expense planner", fr: "Votre planificateur salaire et dépenses", de: "Ihr Gehalts- und Ausgabenplaner" },
  loginTab: { ar: "تسجيل الدخول", en: "Log in", fr: "Connexion", de: "Anmelden" },
  registerTab: { ar: "حساب جديد", en: "New account", fr: "Nouveau compte", de: "Neues Konto" },
  nameLabel: { ar: "الاسم", en: "Name", fr: "Nom", de: "Name" },
  namePlaceholder: { ar: "اسمك", en: "Your name", fr: "Votre nom", de: "Ihr Name" },
  emailLabel: { ar: "البريد الإلكتروني", en: "Email", fr: "E-mail", de: "E-Mail" },
  passwordLabel: { ar: "كلمة السر", en: "Password", fr: "Mot de passe", de: "Passwort" },
  registerSubmit: { ar: "إنشاء الحساب", en: "Create account", fr: "Créer le compte", de: "Konto erstellen" },
  loginSubmit: { ar: "دخول", en: "Log in", fr: "Se connecter", de: "Anmelden" },
  authHint: { ar: "بياناتك محفوظة بأمان تام، مشفّرة ومتزامنة بين جميع أجهزتك. يمكنك تسجيل الدخول إلى حسابك من أي هاتف دون أن تفقد أي شيء.", en: "Your data is stored 100% securely, encrypted and synced across all your devices. You can log in from any device without losing anything.", fr: "Vos données sont stockées en toute sécurité à 100 %, chiffrées et synchronisées sur tous vos appareils. Vous pouvez vous connecter depuis n'importe quel appareil sans rien perdre.", de: "Ihre Daten werden zu 100 % sicher gespeichert, verschlüsselt und über alle Ihre Geräte synchronisiert. Sie können sich von jedem Gerät aus anmelden, ohne etwas zu verlieren." },
  errFillFields: { ar: "عمر البريد وكلمة السر", en: "Fill in email and password", fr: "Remplissez l'e-mail et le mot de passe", de: "E-Mail und Passwort ausfüllen" },
  errEmailTaken: { ar: "هذا البريد مسجل من قبل — سجّل الدخول بدلاً من إنشاء حساب جديد", en: "This email is already registered — log in instead", fr: "Cet e-mail est déjà enregistré — connectez-vous plutôt", de: "Diese E-Mail ist bereits registriert — bitte anmelden" },
  errInvalidCreds: { ar: "البريد الإلكتروني أو كلمة السر غير صحيحة", en: "Incorrect email or password", fr: "E-mail ou mot de passe incorrect", de: "E-Mail oder Passwort falsch" },
  greeting: { ar: "مرحبا،", en: "Hi,", fr: "Bonjour,", de: "Hallo," },
  logout: { ar: "خروج", en: "Log out", fr: "Déconnexion", de: "Abmelden" },
  prevMonth: { ar: "الشهر السابق", en: "Previous month", fr: "Mois précédent", de: "Vorheriger Monat" },
  nextMonth: { ar: "الشهر الجاي", en: "Next month", fr: "Mois suivant", de: "Nächster Monat" },
  incomeLabel: { ar: "الدخل", en: "Income", fr: "Revenus", de: "Einkommen" },
  expenseLabel: { ar: "المصاريف", en: "Expenses", fr: "Dépenses", de: "Ausgaben" },
  balanceLabel: { ar: "الرصيد", en: "Balance", fr: "Solde", de: "Saldo" },
  balanceBeforeSavings: { ar: "الرصيد قبل الادخار", en: "Balance before savings", fr: "Solde avant épargne", de: "Saldo vor dem Sparen" },
  cashAfterSavings: { ar: "الفلوس الباقية بعد الادخار", en: "Cash remaining after savings", fr: "Liquidités restantes après épargne", de: "Verbleibendes Geld nach dem Sparen" },
  savingsRatioLabel: { ar: "نسبة الادخار", en: "Savings rate", fr: "Taux d'épargne", de: "Sparquote" },
  addEntryTitle: { ar: "إضافة حركة جديدة", en: "Add a new entry", fr: "Ajouter une opération", de: "Neuen Eintrag hinzufügen" },
  quickAddAria: { ar: "مصروف مؤقت سريع", en: "Quick temporary expense", fr: "Dépense temporaire rapide", de: "Schnelle temporäre Ausgabe" },
  categoryBreakdownTitle: { ar: "توزيع المصاريف حسب الفئة", en: "Expenses by category", fr: "Dépenses par catégorie", de: "Ausgaben nach Kategorie" },
  termBreakdownTitle: { ar: "توزيع المصاريف حسب المدى", en: "Expenses by term", fr: "Dépenses par échéance", de: "Ausgaben nach Zeithorizont" },
  transactionsTitlePrefix: { ar: "حركات", en: "Entries for", fr: "Opérations de", de: "Einträge für" },
  expenseTab: { ar: "مصروف", en: "Expense", fr: "Dépense", de: "Ausgabe" },
  incomeTab: { ar: "دخل", en: "Income", fr: "Revenu", de: "Einnahme" },
  dateLabel: { ar: "التاريخ", en: "Date", fr: "Date", de: "Datum" },
  categoryLabel: { ar: "الفئة", en: "Category", fr: "Catégorie", de: "Kategorie" },
  sourceLabel: { ar: "المصدر", en: "Source", fr: "Source", de: "Quelle" },
  termLabel: { ar: "المدى", en: "Term", fr: "Terme", de: "Zeithorizont" },
  termDay: { ar: "يومي", en: "Daily", fr: "Quotidien", de: "Täglich" },
  termMonth: { ar: "شهري", en: "Monthly", fr: "Mensuel", de: "Monatlich" },
  termYear: { ar: "سنوي", en: "Yearly", fr: "Annuel", de: "Jährlich" },
  descLabel: { ar: "وصف (اختياري)", en: "Description (optional)", fr: "Description (optionnel)", de: "Beschreibung (optional)" },
  descPlaceholderExpense: { ar: "مثلاً: تسوق الأسبوع", en: "e.g. weekly groceries", fr: "ex. courses de la semaine", de: "z. B. wöchentlicher Einkauf" },
  descPlaceholderIncome: { ar: "مثلاً: راتب شهر أغسطس", en: "e.g. August salary", fr: "ex. salaire d'août", de: "z. B. Gehalt August" },
  amountLabel: { ar: "المبلغ", en: "Amount", fr: "Montant", de: "Betrag" },
  commonExpenseLabel: { ar: "اختر المصروف لإضافة السعر فقط", en: "Choose an expense to enter only the price", fr: "Choisissez une dépense pour saisir uniquement le prix", de: "Ausgabe auswählen und nur den Preis eingeben" },
  commonExpenseToggle: { ar: "اختيارات المصاريف", en: "Expense shortcuts", fr: "Raccourcis de dépenses", de: "Ausgaben-Auswahl" },
  commonRent: { ar: "الكراء", en: "Rent", fr: "Loyer", de: "Miete" },
  commonGroceries: { ar: "المواد الغذائية", en: "Groceries", fr: "Lebensmittel", de: "Lebensmittel" },
  commonPhone: { ar: "الهاتف والإنترنت", en: "Phone & internet", fr: "Téléphone et internet", de: "Telefon & Internet" },
  commonNetflix: { ar: "Netflix", en: "Netflix", fr: "Netflix", de: "Netflix" },
  commonSpotify: { ar: "Spotify", en: "Spotify", fr: "Spotify", de: "Spotify" },
  commonTransport: { ar: "النقل", en: "Transport", fr: "Transport", de: "Transport" },
  commonFuel: { ar: "المازوط / البنزين", en: "Fuel", fr: "Carburant", de: "Benzin" },
  commonGym: { ar: "القاعة الرياضية", en: "Gym", fr: "Salle de sport", de: "Fitnessstudio" },
  commonInsurance: { ar: "التأمين", en: "Insurance", fr: "Assurance", de: "Versicherung" },
  commonDebt: { ar: "الدين / التقسيط", en: "Debt / installment", fr: "Dette / mensualité", de: "Schuld / Rate" },
  commonElectricity: { ar: "الضو", en: "Electricity", fr: "Électricité", de: "Strom" },
  commonWater: { ar: "الما", en: "Water", fr: "Eau", de: "Wasser" },
  commonHeating: { ar: "التدفئة / الغاز", en: "Heating / gas", fr: "Chauffage / gaz", de: "Heizung / Gas" },
  commonCarInsurance: { ar: "تأمين الطوموبيل", en: "Car insurance", fr: "Assurance auto", de: "Kfz-Versicherung" },
  commonHealthInsurance: { ar: "التأمين الصحي", en: "Health insurance", fr: "Assurance santé", de: "Krankenversicherung" },
  commonAmazon: { ar: "Amazon Prime", en: "Amazon Prime", fr: "Amazon Prime", de: "Amazon Prime" },
  commonDisney: { ar: "Disney+", en: "Disney+", fr: "Disney+", de: "Disney+" },
  commonYoutube: { ar: "YouTube Premium", en: "YouTube Premium", fr: "YouTube Premium", de: "YouTube Premium" },
  commonCloud: { ar: "التخزين السحابي", en: "Cloud storage", fr: "Stockage cloud", de: "Cloud-Speicher" },
  commonSchool: { ar: "المدرسة / القراية", en: "School / education", fr: "École / études", de: "Schule / Ausbildung" },
  commonPharmacy: { ar: "الصيدلية", en: "Pharmacy", fr: "Pharmacie", de: "Apotheke" },
  commonRestaurant: { ar: "المطاعم", en: "Restaurants", fr: "Restaurants", de: "Restaurants" },
  commonDelivery: { ar: "توصيل الماكلة", en: "Food delivery", fr: "Livraison de repas", de: "Essenslieferung" },
  commonCoffee: { ar: "القهوة", en: "Coffee", fr: "Café", de: "Kaffee" },
  commonParking: { ar: "الباركينغ", en: "Parking", fr: "Parking", de: "Parken" },
  commonTaxi: { ar: "طاكسي / Uber", en: "Taxi / Uber", fr: "Taxi / Uber", de: "Taxi / Uber" },
  commonFlight: { ar: "الطيارة / السفر", en: "Flights / travel", fr: "Vols / voyage", de: "Flüge / Reisen" },
  commonRemittance: { ar: "تحويل للعائلة", en: "Family transfer", fr: "Virement familial", de: "Überweisung an Familie" },
  commonCharity: { ar: "الصدقة / الزكاة", en: "Charity / Zakat", fr: "Charité / Zakat", de: "Spende / Zakat" },
  errAmount: { ar: "دخل مبلغ صحيح أكبر من صفر", en: "Enter a valid amount greater than zero", fr: "Entrez un montant valide supérieur à zéro", de: "Bitte einen gültigen Betrag größer als null eingeben" },
  errDate: { ar: "اختار التاريخ", en: "Choose a date", fr: "Choisissez une date", de: "Datum auswählen" },
  addExpenseBtn: { ar: "إضافة المصروف", en: "Add expense", fr: "Ajouter la dépense", de: "Ausgabe hinzufügen" },
  addIncomeBtn: { ar: "إضافة الدخل", en: "Add income", fr: "Ajouter le revenu", de: "Einnahme hinzufügen" },
  emptyCategoryHint: { ar: "لم تُضِف أي مصروف بعد — بمجرد إضافة أول مصروف سيظهر التوزيع هنا.", en: "No expenses yet — add one and the breakdown will appear here.", fr: "Pas encore de dépense — ajoutez-en une et la répartition apparaîtra ici.", de: "Noch keine Ausgaben — fügen Sie eine hinzu, und die Aufschlüsselung erscheint hier." },
  emptyTransactionsHint: { ar: "لا توجد حركات هذا الشهر بعد. أضِف أول حركة من النموذج أعلاه.", en: "No entries this month yet. Add your first one from the form above.", fr: "Aucune opération ce mois-ci. Ajoutez-en une depuis le formulaire ci-dessus.", de: "Diesen Monat noch keine Einträge. Fügen Sie oben Ihren ersten hinzu." },
  footerNote: { ar: "بياناتك محفوظة بأمان تام ومتزامنة بين أجهزتك — يمكنك دائماً أخذ نسخة احتياطية إضافية من الإعدادات.", en: "Your data is stored 100% securely and synced across your devices — you can still take an extra backup from Settings.", fr: "Vos données sont stockées en toute sécurité à 100 % et synchronisées entre vos appareils — vous pouvez toujours faire une sauvegarde supplémentaire depuis les paramètres.", de: "Ihre Daten werden zu 100 % sicher gespeichert und über Ihre Geräte synchronisiert — Sie können jederzeit ein zusätzliches Backup in den Einstellungen erstellen." },
  exportBtn: { ar: "تحميل نسخة احتياطية", en: "Download backup", fr: "Télécharger la sauvegarde", de: "Backup herunterladen" },
  importBtn: { ar: "استرجاع نسخة", en: "Restore backup", fr: "Restaurer la sauvegarde", de: "Backup wiederherstellen" },
  importBankBtn: { ar: "استيراد كشف حساب", en: "Import bank statement", fr: "Importer un relevé", de: "Kontoauszug importieren" },
  importMenuBtn: { ar: "استيراد بيانات", en: "Import data", fr: "Importer des données", de: "Daten importieren" },
  importMenuTitle: { ar: "ماذا تريد أن تضيف؟", en: "What would you like to add?", fr: "Que souhaitez-vous ajouter ?", de: "Was möchten Sie hinzufügen?" },
  importMenuBankHint: { ar: "من ملف CSV أو PDF خاص بالبنك", en: "From a bank CSV or PDF file", fr: "À partir d'un fichier CSV ou PDF bancaire", de: "Aus einer Bank-CSV- oder PDF-Datei" },
  importMenuBackupHint: { ar: "رجع نسخة احتياطية حفظتيها من قبل", en: "Restore a backup you saved before", fr: "Restaurer une sauvegarde précédente", de: "Ein zuvor gespeichertes Backup wiederherstellen" },
  importBankTitle: { ar: "استيراد كشف حساب (CSV)", en: "Import bank statement (CSV)", fr: "Importer un relevé (CSV)", de: "Kontoauszug importieren (CSV)" },
  importBankDesc: { ar: "ارفع ملف CSV مُصدَّراً من تطبيق بنكك، وسيقترح التطبيق تلقائياً فئة لكل حركة. يمكنك المراجعة والتعديل قبل التأكيد.", en: "Upload a CSV file exported from your bank's app, and categories will be suggested automatically for each transaction. You can review and adjust before confirming.", fr: "Téléchargez un fichier CSV exporté depuis votre banque, les catégories seront suggérées automatiquement. Vous pouvez vérifier avant de confirmer.", de: "Laden Sie eine von Ihrer Bank exportierte CSV-Datei hoch — Kategorien werden automatisch vorgeschlagen. Sie können vor der Bestätigung alles überprüfen." },
  importBankChoose: { ar: "اختار ملف CSV", en: "Choose CSV file", fr: "Choisir un fichier CSV", de: "CSV-Datei auswählen" },
  importBankParseError: { ar: "تعذّرت قراءة الملف. تأكد من أنه ملف CSV صحيح يحتوي على أعمدة التاريخ والوصف والمبلغ.", en: "Couldn't read the file. Make sure it's a valid CSV with date, description, and amount columns.", fr: "Impossible de lire le fichier. Vérifiez qu'il s'agit d'un CSV valide avec des colonnes date, description et montant.", de: "Datei konnte nicht gelesen werden. Stellen Sie sicher, dass es sich um eine gültige CSV mit Datums-, Beschreibungs- und Betragsspalten handelt." },
  importBankChoosePdf: { ar: "اختار ملف PDF", en: "Choose PDF file", fr: "Choisir un fichier PDF", de: "PDF-Datei auswählen" },
  importBankPdfLoading: { ar: "جارٍ قراءة الملف...", en: "Reading the file...", fr: "Lecture du fichier...", de: "Datei wird gelesen..." },
  importBankPdfHint: { ar: "تتم قراءة ملف PDF مباشرة في متصفحك، دون ذكاء اصطناعي ودون أي تكلفة. يعمل بشكل أفضل مع الكشوفات الرقمية (وليس الصور الممسوحة ضوئياً) — راجع النتيجة دائماً قبل التأكيد.", en: "PDFs are read directly in your browser, no AI and no cost. Works best with digitally-generated statements (not scanned images) — always review the results before confirming.", fr: "Les PDF sont lus directement dans votre navigateur, sans IA et sans coût. Fonctionne mieux avec des relevés numériques (pas des images scannées) — vérifiez toujours les résultats avant de confirmer.", de: "PDFs werden direkt in Ihrem Browser gelesen, ohne KI und ohne Kosten. Funktioniert am besten mit digital erstellten Kontoauszügen (keine gescannten Bilder) — überprüfen Sie die Ergebnisse immer vor der Bestätigung." },
  importBankPdfError: { ar: "تعذّرت قراءة ملف PDF. قد يكون صورة ممسوحة ضوئياً بدلاً من نص حقيقي — جرّب ملف CSV بدلاً منه إن كان متوفراً.", en: "Couldn't read the PDF. It might be a scanned image instead of real text — try the CSV file instead if you have one.", fr: "Impossible de lire le PDF. Il s'agit peut-être d'une image scannée plutôt que de texte réel — essayez le fichier CSV si vous en avez un.", de: "PDF konnte nicht gelesen werden. Es könnte sich um ein gescanntes Bild statt echten Text handeln — versuchen Sie stattdessen die CSV-Datei, falls vorhanden." },
  importBankNoRows: { ar: "لم يتم العثور على حركات صالحة في هذا الملف.", en: "No valid transactions found in this file.", fr: "Aucune opération valide trouvée dans ce fichier.", de: "Keine gültigen Transaktionen in dieser Datei gefunden." },
  importBankReviewTitle: { ar: "راجع قبل الاستيراد", en: "Review before importing", fr: "Vérifier avant d'importer", de: "Vor dem Import prüfen" },
  importBankInclude: { ar: "تضمين", en: "Include", fr: "Inclure", de: "Einschließen" },
  importBankConfirmBtn: { ar: "استيراد", en: "Import", fr: "Importer", de: "Importieren" },
  importBankBack: { ar: "رجوع", en: "Back", fr: "Retour", de: "Zurück" },
  importBankSuccess: { ar: "تم استيراد الحركات بنجاح.", en: "Transactions imported successfully.", fr: "Opérations importées avec succès.", de: "Transaktionen erfolgreich importiert." },
  importError: { ar: "الملف مامقروءش، تأكد بلي هو نسخة احتياطية صحيحة", en: "Couldn't read the file — make sure it's a valid backup", fr: "Impossible de lire le fichier — vérifiez qu'il s'agit d'une sauvegarde valide", de: "Datei konnte nicht gelesen werden — prüfen Sie, ob es ein gültiges Backup ist" },
  langSwitchAria: { ar: "تبديل اللغة", en: "Switch language", fr: "Changer de langue", de: "Sprache wechseln" },
  quickTitle: { ar: "مصروف مؤقت سريع", en: "Quick temporary expense", fr: "Dépense temporaire rapide", de: "Schnelle temporäre Ausgabe" },
  quickDesc: { ar: "يُسجَّل فوراً ويُحذف تلقائياً بعد المدة التي تختارها — مناسب للتذكيرات المؤقتة.", en: "Recorded instantly and auto-deletes after the duration you pick — handy for short-lived reminders.", fr: "Enregistrée instantanément et supprimée automatiquement après la durée choisie — pratique pour les rappels courts.", de: "Wird sofort erfasst und nach der gewählten Dauer automatisch gelöscht — praktisch für kurzfristige Erinnerungen." },
  quickDuration: { ar: "يتمسح بعد", en: "Auto-delete after", fr: "Suppression après", de: "Löschen nach" },
  quickDay: { ar: "يوم واحد", en: "1 day", fr: "1 jour", de: "1 Tag" },
  quickWeek: { ar: "سيمانة", en: "1 week", fr: "1 semaine", de: "1 Woche" },
  quickAddBtn: { ar: "إضافة المصروف المؤقت", en: "Add temporary expense", fr: "Ajouter la dépense temporaire", de: "Temporäre Ausgabe hinzufügen" },
  quickCancel: { ar: "إلغاء", en: "Cancel", fr: "Annuler", de: "Abbrechen" },
  quickBadge: { ar: "مؤقت", en: "Temp", fr: "Temp.", de: "Temp." },
  quickRemaining: { ar: "باقي", en: "left", fr: "restant", de: "übrig" },
  recurringLabel: { ar: "يتكرر كل شهر", en: "Repeats every month", fr: "Se répète chaque mois", de: "Wiederholt sich monatlich" },
  recurringHint: { ar: "سيظهر تلقائياً في كل الأشهر القادمة، دون الحاجة لإدخاله مجدداً.", en: "It will automatically appear in every future month, no need to re-enter it.", fr: "Elle apparaîtra automatiquement chaque mois suivant, sans avoir à la ressaisir.", de: "Er erscheint automatisch in jedem folgenden Monat, ohne dass Sie ihn erneut eingeben müssen." },
  recurringBadge: { ar: "متكرر", en: "Recurring", fr: "Récurrent", de: "Wiederkehrend" },
  editAria: { ar: "تعديل", en: "Edit", fr: "Modifier", de: "Bearbeiten" },
  deleteAria: { ar: "حذف", en: "Delete", fr: "Supprimer", de: "Löschen" },
  editEntryTitle: { ar: "تعديل الحركة", en: "Edit entry", fr: "Modifier l'opération", de: "Eintrag bearbeiten" },
  saveBtn: { ar: "حفظ", en: "Save", fr: "Enregistrer", de: "Speichern" },
  cancelBtn: { ar: "إلغاء", en: "Cancel", fr: "Annuler", de: "Abbrechen" },
  editScopeTitle: { ar: "ماذا تريد أن تعدّل؟", en: "What would you like to edit?", fr: "Que souhaitez-vous modifier ?", de: "Was möchten Sie bearbeiten?" },
  deleteScopeTitle: { ar: "ماذا تريد أن تحذف؟", en: "What would you like to delete?", fr: "Que souhaitez-vous supprimer ?", de: "Was möchten Sie löschen?" },
  scopeThis: { ar: "هذا الشهر فقط", en: "This month only", fr: "Ce mois-ci seulement", de: "Nur diesen Monat" },
  scopeFuture: { ar: "هذا الشهر وما بعده", en: "This month and future ones", fr: "Ce mois-ci et les suivants", de: "Diesen und zukünftige Monate" },
  scopeAll: { ar: "كل الشهور (مسح نهائي)", en: "All months (delete entirely)", fr: "Tous les mois (suppression totale)", de: "Alle Monate (vollständig löschen)" },
  settingsAria: { ar: "الإعدادات", en: "Settings", fr: "Paramètres", de: "Einstellungen" },
  settingsTitle: { ar: "الإعدادات", en: "Settings", fr: "Paramètres", de: "Einstellungen" },
  settingsLanguageSection: { ar: "اللغة", en: "Language", fr: "Langue", de: "Sprache" },
  settingsCurrencySection: { ar: "العملة", en: "Currency", fr: "Devise", de: "Währung" },
  settingsProfileSection: { ar: "الملف الشخصي", en: "Profile", fr: "Profil", de: "Profil" },
  settingsNotifSection: { ar: "الإشعارات", en: "Notifications", fr: "Notifications", de: "Benachrichtigungen" },
  settingsPlanerSection: { ar: "المساعد الصوتي", en: "Voice assistant", fr: "Assistant vocal", de: "Sprachassistent" },
  planerEnableLabel: { ar: "فعل Planer", en: "Enable Planer", fr: "Activer Planer", de: "Planer aktivieren" },
  planerEnableHint: { ar: "بمجرد التفعيل، سيظهر زر صغير فوق شريط التبويب للتحدث معه — أضِف مصروفاً، احذف حركة، أو اسأل أي سؤال عن ميزانيتك.", en: "Once enabled, a small button appears above the tab bar to talk to it — add an expense, delete an entry, or ask anything about your budget.", fr: "Une fois activé, un petit bouton apparaît au-dessus de la barre d'onglets — ajoutez une dépense, supprimez une opération, ou posez une question sur votre budget.", de: "Nach der Aktivierung erscheint eine kleine Schaltfläche über der Tab-Leiste — fügen Sie eine Ausgabe hinzu, löschen Sie einen Eintrag oder stellen Sie eine Frage zu Ihrem Budget." },
  settingsDataSection: { ar: "البيانات", en: "Data", fr: "Données", de: "Daten" },
  settingsTranslateSection: { ar: "الترجمة التلقائية", en: "Auto-translate", fr: "Traduction automatique", de: "Automatische Übersetzung" },
  autoTranslateLabel: { ar: "ترجم النصوص لي كتبتها عند تبديل اللغة", en: "Translate my own text when I switch language", fr: "Traduire mon propre texte quand je change de langue", de: "Meinen eigenen Text bei Sprachwechsel übersetzen" },
  autoTranslateHint: { ar: "كي تفعلها، النصوص لي كتبتيها بيدك (تسمية المصاريف، أسماء الأهداف، ملاحظات Dienstplan) غادي تتصيفط لخدمة ترجمة خارجية (عبر Groq) باش تترجمها للغة الجديدة. هادشي كيخرج من Supabase، عكس باقي بياناتك.", en: "When on, text you typed yourself (expense labels, goal names, Dienstplan notes) is sent to an external translation service (via Groq) to translate it into the new language. This leaves Supabase, unlike the rest of your data.", fr: "Une fois activé, le texte que vous avez saisi vous-même (libellés de dépenses, noms d'objectifs, notes Dienstplan) est envoyé à un service de traduction externe (via Groq) pour le traduire dans la nouvelle langue. Cela quitte Supabase, contrairement au reste de vos données.", de: "Wenn aktiviert, wird von Ihnen eingegebener Text (Ausgabenbezeichnungen, Zielnamen, Dienstplan-Notizen) an einen externen Übersetzungsdienst (über Groq) gesendet, um ihn in die neue Sprache zu übersetzen. Dies verlässt Supabase, im Gegensatz zu Ihren übrigen Daten." },
  settingsReceiptScanSection: { ar: "مسح البونات", en: "Receipt scanning", fr: "Numérisation des reçus", de: "Beleg-Scan" },
  receiptScanLabel: { ar: "زيد مصروف بتصوير البون", en: "Add expense by photographing a receipt", fr: "Ajouter une dépense en photographiant un reçu", de: "Ausgabe durch Fotografieren eines Belegs hinzufügen" },
  receiptScanHint: { ar: "كي تفعلها، تقدر تصور البون وتطلع ليك اسم المحل والتمن تلقائيا (تراجعهم قبل ما تأكد). الصورة كتتصيفط لخدمة خارجية (عبر Groq) باش تقراها — هادشي كيخرج من Supabase، عكس باقي بياناتك.", en: "When on, you can photograph a receipt and the merchant name and amount fill in automatically (you review them before confirming). The photo is sent to an external service (via Groq) to read it — this leaves Supabase, unlike the rest of your data.", fr: "Une fois activé, vous pouvez photographier un reçu et le nom du commerçant et le montant se remplissent automatiquement (vous les vérifiez avant de confirmer). La photo est envoyée à un service externe (via Groq) pour la lire — cela quitte Supabase, contrairement au reste de vos données.", de: "Wenn aktiviert, können Sie einen Beleg fotografieren, und Händlername sowie Betrag werden automatisch ausgefüllt (Sie prüfen sie vor der Bestätigung). Das Foto wird zum Lesen an einen externen Dienst (über Groq) gesendet — dies verlässt Supabase, im Gegensatz zu Ihren übrigen Daten." },
  receiptScanTitle: { ar: "امسح البون", en: "Scan receipt", fr: "Numériser le reçu", de: "Beleg scannen" },
  receiptScanDesc: { ar: "صور البون وتطلع ليك التفاصيل تلقائيا — تقدر تبدلها قبل ما تأكد.", en: "Photograph the receipt and the details fill in automatically — you can edit them before confirming.", fr: "Photographiez le reçu et les détails se remplissent automatiquement — vous pouvez les modifier avant de confirmer.", de: "Fotografieren Sie den Beleg, und die Details werden automatisch ausgefüllt — Sie können sie vor der Bestätigung bearbeiten." },
  receiptScanTakePhotoBtn: { ar: "صور البون", en: "Take photo", fr: "Prendre une photo", de: "Foto aufnehmen" },
  receiptScanReading: { ar: "كنقرا البون...", en: "Reading the receipt...", fr: "Lecture du reçu...", de: "Beleg wird gelesen..." },
  receiptScanFailed: { ar: "ماقدرناش نقراو البون. جرب تصورها مرة أخرى بضو أحسن.", en: "Couldn't read the receipt. Try photographing it again with better lighting.", fr: "Impossible de lire le reçu. Essayez de le photographier à nouveau avec un meilleur éclairage.", de: "Beleg konnte nicht gelesen werden. Versuchen Sie es erneut mit besserer Beleuchtung." },
  receiptScanReviewDesc: { ar: "راجع المعطيات قبل ما تأكد:", en: "Review the details before confirming:", fr: "Vérifiez les détails avant de confirmer :", de: "Überprüfen Sie die Details vor der Bestätigung:" },
  receiptScanMerchant: { ar: "اسم المحل", en: "Merchant", fr: "Commerçant", de: "Händler" },
  receiptScanConfirmBtn: { ar: "زيدها كمصروف", en: "Add as expense", fr: "Ajouter comme dépense", de: "Als Ausgabe hinzufügen" },
  receiptScanRetakeBtn: { ar: "صور من جديد", en: "Retake photo", fr: "Reprendre la photo", de: "Foto erneut aufnehmen" },
  settingsDienstplanScanSection: { ar: "مسح جدول المناوبات", en: "Shift schedule scanning", fr: "Numérisation du planning", de: "Dienstplan-Scan" },
  dienstplanScanLabel: { ar: "عمر الجدول بتصوير الورقة", en: "Fill the schedule by photographing it", fr: "Remplir le planning en le photographiant", de: "Dienstplan durch Fotografieren ausfüllen" },
  dienstplanScanHint: {
    ar: "كي تفعلها، تقدر تصور جدول المناوبات (الورقة لي معطيك إياها الخدمة) وتقرا اسمك بوحدها وتعمر الشهر تلقائيا — تراجعه قبل ما تأكد. الصورة كتتصيفط لخدمة خارجية (عبر Groq) باش تقراها — هادشي كيخرج من Supabase، عكس باقي بياناتك.",
    en: "When on, you can photograph the shift schedule sheet from work, and it reads your name and fills in the month automatically — you review it before confirming. The photo is sent to an external service (via Groq) to read it — this leaves Supabase, unlike the rest of your data.",
    fr: "Une fois activé, vous pouvez photographier la feuille de planning du travail, et votre nom est repéré pour remplir le mois automatiquement — vous vérifiez avant de confirmer. La photo est envoyée à un service externe (via Groq) — cela quitte Supabase, contrairement au reste de vos données.",
    de: "Wenn aktiviert, können Sie den Dienstplan von der Arbeit fotografieren — Ihr Name wird erkannt und der Monat automatisch ausgefüllt, Sie prüfen ihn vor der Bestätigung. Das Foto wird an einen externen Dienst (über Groq) gesendet — dies verlässt Supabase, im Gegensatz zu Ihren übrigen Daten.",
  },
  dienstplanScanBtn: { ar: "امسح الجدول بالصورة", en: "Scan schedule photo", fr: "Numériser le planning", de: "Dienstplan scannen" },
  dienstplanScanTitle: { ar: "امسح جدول المناوبات", en: "Scan shift schedule", fr: "Numériser le planning", de: "Dienstplan scannen" },
  dienstplanScanNameLabel: { ar: "اسمك بالضبط كيفما مكتوب فالجدول", en: "Your name exactly as it appears on the schedule", fr: "Votre nom tel qu'il apparaît sur le planning", de: "Ihr Name genau wie im Dienstplan geschrieben" },
  dienstplanScanNamePlaceholder: { ar: "مثلا: Khalid", en: "e.g. Khalid", fr: "ex. Khalid", de: "z. B. Khalid" },
  dienstplanScanTakePhotoBtn: { ar: "صور الورقة", en: "Take photo", fr: "Prendre une photo", de: "Foto aufnehmen" },
  dienstplanScanChooseBtn: { ar: "اختار من الصور", en: "Choose from photos", fr: "Choisir dans les photos", de: "Aus Fotos wählen" },
  cameraCaptureBtn: { ar: "امسح", en: "Scan", fr: "Numériser", de: "Scannen" },
  cameraScanBtn: { ar: "امسح مباشرة", en: "Scan directly", fr: "Numériser directement", de: "Direkt scannen" },
  cameraAccessError: { ar: "ماقدرناش نوصلو للكاميرا. تأكد بلي عطيتي الإذن من إعدادات الهاتف، أو جرب \"صور البون\" العادي بدالها.", en: "Couldn't access the camera. Make sure you've granted permission in your phone settings, or try the regular \"Take photo\" instead.", fr: "Impossible d'accéder à la caméra. Vérifiez que la permission est accordée dans les réglages, ou essayez « Prendre une photo » à la place.", de: "Kein Kamerazugriff möglich. Stellen Sie sicher, dass die Berechtigung erteilt wurde, oder versuchen Sie stattdessen „Foto aufnehmen\"." },
  dienstplanScanReading: { ar: "كنقرا الجدول...", en: "Reading the schedule...", fr: "Lecture du planning...", de: "Dienstplan wird gelesen..." },
  dienstplanScanFailed: { ar: "ماقدرناش نقراو الجدول. جرب تصورو مرة أخرى بضو أحسن وتأكد بلي الاسم مكتوب بالضبط.", en: "Couldn't read the schedule. Try photographing it again with better lighting, and make sure the name matches exactly.", fr: "Impossible de lire le planning. Réessayez avec un meilleur éclairage et vérifiez que le nom correspond exactement.", de: "Dienstplan konnte nicht gelesen werden. Versuchen Sie es mit besserer Beleuchtung und prüfen Sie, ob der Name genau übereinstimmt." },
  dienstplanScanNotFound: { ar: "ماقدرناش نلقاو هاد الاسم فالجدول. تأكد بلي كتب بحال لي مكتوب فالورقة بالضبط.", en: "Couldn't find that name on the schedule. Make sure it's spelled exactly as it appears on the sheet.", fr: "Ce nom n'a pas été trouvé sur le planning. Vérifiez l'orthographe exacte sur la feuille.", de: "Dieser Name wurde im Dienstplan nicht gefunden. Prüfen Sie die genaue Schreibweise auf dem Blatt." },
  dienstplanScanReviewDesc: { ar: "راجع وصلح كل يوم قبل ما تأكد — الذكاء الاصطناعي يقدر يغلط:", en: "Review and correct each day before confirming — the AI can make mistakes:", fr: "Vérifiez et corrigez chaque jour avant de confirmer — l'IA peut se tromper :", de: "Überprüfen und korrigieren Sie jeden Tag vor der Bestätigung — die KI kann Fehler machen:" },
  dienstplanScanUnreadDays: { ar: "أيام ماقدرش يقراها (خلاتهم راحة، بدلهم إلا خاص)", en: "Days it couldn't read (left as off — change if needed)", fr: "Jours illisibles (laissés en repos — modifiez si besoin)", de: "Nicht lesbare Tage (als frei belassen — bei Bedarf ändern)" },
  dienstplanScanConfirmBtn: { ar: "أكد وعمر الجدول", en: "Confirm and fill schedule", fr: "Confirmer et remplir le planning", de: "Bestätigen und Dienstplan füllen" },
  dienstplanScanRetakeBtn: { ar: "صور من جديد", en: "Retake photo", fr: "Reprendre la photo", de: "Foto erneut aufnehmen" },
  notifEnableLabel: { ar: "فعل الإشعارات", en: "Enable notifications", fr: "Activer les notifications", de: "Benachrichtigungen aktivieren" },
  notifEnableHint: { ar: "سيطلب منك المتصفح الإذن. تعمل الإشعارات فقط عندما يكون التطبيق مفتوحاً (في الخلفية أو الواجهة) — لا يوجد خادم يرسلها عند إغلاق التطبيق بالكامل.", en: "Your browser will ask permission. Notifications only fire while the app is open (foreground or background tab) — there's no server to deliver them when it's fully closed.", fr: "Le navigateur vous demandera la permission. Les notifications ne se déclenchent que lorsque l'application est ouverte — aucun serveur ne les délivre lorsqu'elle est complètement fermée.", de: "Der Browser fragt um Erlaubnis. Benachrichtigungen funktionieren nur, während die App geöffnet ist — es gibt keinen Server, der sie bei vollständig geschlossener App zustellt." },
  notifPermissionDenied: { ar: "تم رفض الإذن من إعدادات المتصفح. فعّله من إعدادات الهاتف > Safari أو إعدادات الموقع.", en: "Permission was denied in browser settings. Enable it from your phone's Settings > Safari or the site settings.", fr: "La permission a été refusée dans les paramètres du navigateur. Activez-la depuis Réglages > Safari ou les paramètres du site.", de: "Die Berechtigung wurde in den Browsereinstellungen verweigert. Aktivieren Sie sie unter Einstellungen > Safari oder den Website-Einstellungen." },
  notifNeedsInstall: { ar: "لا تعمل الإشعارات من داخل Safari مباشرة. يجب فتح التطبيق من الأيقونة التي أضفتها إلى الشاشة الرئيسية (Add to Home Screen)، ثم العودة لفتح هذه الإعدادات وتفعيلها من هناك.", en: "Notifications don't work from inside Safari directly. Open the app from the icon you added to your Home Screen, then come back to Settings and enable them from there.", fr: "Les notifications ne fonctionnent pas directement depuis Safari. Ouvrez l'application depuis l'icône ajoutée à l'écran d'accueil, puis revenez ici pour les activer.", de: "Benachrichtigungen funktionieren nicht direkt in Safari. Öffnen Sie die App über das Symbol auf Ihrem Startbildschirm und aktivieren Sie sie dann hier." },
  notifUnsupported: { ar: "متصفحك لا يدعم الإشعارات في هذا السياق.", en: "Your browser doesn't support notifications in this context.", fr: "Votre navigateur ne prend pas en charge les notifications dans ce contexte.", de: "Ihr Browser unterstützt Benachrichtigungen in diesem Kontext nicht." },
  budgetAlertLabel: { ar: "نبهني ملي المصاريف توصل لـ", en: "Alert me when expenses reach", fr: "M'alerter quand les dépenses atteignent", de: "Benachrichtigen, wenn Ausgaben erreichen" },
  budgetAlertOff: { ar: "معطل", en: "Off", fr: "Désactivé", de: "Aus" },
  recurringReminderLabel: { ar: "ذكرني بالمصاريف المتكررة يوم أدائها", en: "Remind me on the due day of recurring expenses", fr: "Me rappeler le jour d'échéance des dépenses récurrentes", de: "An den Fälligkeitstag wiederkehrender Ausgaben erinnern" },
  displayNameLabel: { ar: "الاسم المعروض", en: "Display name", fr: "Nom affiché", de: "Anzeigename" },
  clearDataBtn: { ar: "مسح كل البيانات", en: "Clear all data", fr: "Effacer toutes les données", de: "Alle Daten löschen" },
  clearDataConfirm: { ar: "سيؤدي هذا إلى حذف جميع حركاتك نهائياً من هذا الجهاز. هل أنت متأكد؟", en: "This will permanently erase all your entries from this device. Are you sure?", fr: "Cela effacera définitivement toutes vos opérations de cet appareil. Êtes-vous sûr ?", de: "Dadurch werden alle Ihre Einträge dauerhaft von diesem Gerät gelöscht. Sind Sie sicher?" },
  clearDataConfirmBtn: { ar: "نعم، احذف كل شيء", en: "Yes, delete everything", fr: "Oui, tout supprimer", de: "Ja, alles löschen" },
  doneBtn: { ar: "تم", en: "Done", fr: "Terminé", de: "Fertig" },
  notifBudgetTitle: { ar: "تنبيه ميزانية", en: "Budget alert", fr: "Alerte budget", de: "Budget-Warnung" },
  notifBudgetBody: { ar: "وصلت مصاريف هذا الشهر إلى", en: "This month's expenses have reached", fr: "Les dépenses de ce mois ont atteint", de: "Die Ausgaben dieses Monats haben erreicht" },
  notifRecurringTitle: { ar: "مصروف متكرر اليوم", en: "Recurring expense today", fr: "Dépense récurrente aujourd'hui", de: "Wiederkehrende Ausgabe heute" },
  notifShiftTitle: { ar: "مناوبة اليوم", en: "Today's shift", fr: "Service du jour", de: "Heutige Schicht" },
  notifShiftBody: { ar: "مناوبتك اليوم", en: "Your shift today", fr: "Votre service aujourd'hui", de: "Ihre heutige Schicht" },
  notifExpiringTitle: { ar: "مصروف مؤقت سيُحذف قريباً", en: "Temporary expense expiring soon", fr: "Dépense temporaire bientôt expirée", de: "Temporäre Ausgabe läuft bald ab" },
  searchAria: { ar: "بحث فالحركات", en: "Search entries", fr: "Rechercher des opérations", de: "Einträge durchsuchen" },
  notificationsTitle: { ar: "الإشعارات", en: "Notifications", fr: "Notifications", de: "Benachrichtigungen" },
  notificationsEmpty: { ar: "ماكاينش إشعارات بعد.", en: "No notifications yet.", fr: "Aucune notification pour l'instant.", de: "Noch keine Benachrichtigungen." },
  notificationsClearAll: { ar: "مسح الكل", en: "Clear all", fr: "Tout effacer", de: "Alle löschen" },
  notifJustNow: { ar: "الآن", en: "just now", fr: "à l'instant", de: "gerade eben" },
  notifMinutesAgo: { ar: "د", en: "min ago", fr: "min", de: "Min." },
  notifHoursAgo: { ar: "سا", en: "hr ago", fr: "h", de: "Std." },
  notifDaysAgo: { ar: "يوم", en: "d ago", fr: "j", de: "Tg." },
  sponsoredLabel: { ar: "إعلان", en: "Sponsored", fr: "Sponsorisé", de: "Anzeige" },
  sponsoredDefaultCta: { ar: "اعرف أكثر", en: "Learn more", fr: "En savoir plus", de: "Mehr erfahren" },
  searchTitle: { ar: "البحث فالحركات", en: "Search entries", fr: "Rechercher des opérations", de: "Einträge durchsuchen" },
  searchPlaceholder: { ar: "ابحث بالاسم أو الفئة...", en: "Search by name or category...", fr: "Rechercher par nom ou catégorie...", de: "Nach Name oder Kategorie suchen..." },
  searchFilterAll: { ar: "الكل", en: "All", fr: "Tout", de: "Alle" },
  searchNoResults: { ar: "لا توجد نتائج. جرّب كلمة أخرى.", en: "No results. Try a different search.", fr: "Aucun résultat. Essayez une autre recherche.", de: "Keine Ergebnisse. Versuchen Sie eine andere Suche." },
  searchResultsFound: { ar: "حركة", en: "entries found", fr: "opérations trouvées", de: "Einträge gefunden" },
  searchNet: { ar: "الصافي", en: "Net", fr: "Net", de: "Netto" },
  safeToSpendTitle: { ar: "المتاح للإنفاق اليوم", en: "Safe to spend today", fr: "Vous pouvez dépenser aujourd'hui", de: "Sicher ausgebbar heute" },
  safeToSpendPerDay: { ar: "في اليوم", en: "per day", fr: "par jour", de: "pro Tag" },
  safeToSpendHowLink: { ar: "كيف يُحسب هذا؟", en: "How is this calculated?", fr: "Comment est-ce calculé ?", de: "Wie wird das berechnet?" },
  safeToSpendBreakdownTitle: { ar: "كيف يُحسب هذا الرقم", en: "How this is calculated", fr: "Comment ce montant est calculé", de: "So wird der Betrag berechnet" },
  safeToSpendBalance: { ar: "الرصيد الحالي (هذا الشهر)", en: "Current balance (this month)", fr: "Solde actuel (ce mois)", de: "Aktueller Saldo (dieser Monat)" },
  safeToSpendUpcoming: { ar: "ناقص: مصاريف قادمة", en: "Minus: upcoming expenses", fr: "Moins : dépenses à venir", de: "Minus: bevorstehende Ausgaben" },
  safeToSpendSavings: { ar: "ناقص: هدف ادخار شهري", en: "Minus: monthly goal target", fr: "Moins : objectif d'épargne mensuel", de: "Minus: monatliches Sparziel" },
  safeToSpendActualSavings: { ar: "ناقص: ادخار فعلي هذا الشهر", en: "Minus: actual savings this month", fr: "Moins : épargne réelle ce mois", de: "Minus: tatsächliches Sparen diesen Monat" },
  safeToSpendBuffer: { ar: "احتياطي الطوارئ (هدف، غير مخصوم)", en: "Emergency buffer (target, not deducted)", fr: "Réserve d'urgence (cible, non déduite)", de: "Notfallpuffer (Ziel, nicht abgezogen)" },
  safeToSpendAvailable: { ar: "المتاح", en: "Available", fr: "Disponible", de: "Verfügbar" },
  safeToSpendDays: { ar: "الأيام المتبقية في الشهر", en: "Days left in month", fr: "Jours restants ce mois", de: "Verbleibende Tage im Monat" },
  safeToSpendNegative: { ar: "المصاريف القادمة والالتزامات أكبر من المتاح لديك هذا الشهر — راجع مصاريفك المتكررة.", en: "Upcoming expenses and commitments exceed what's available this month — review your recurring expenses.", fr: "Les dépenses à venir dépassent ce qui est disponible ce mois — vérifiez vos dépenses récurrentes.", de: "Bevorstehende Ausgaben übersteigen das Verfügbare diesen Monat — prüfen Sie Ihre wiederkehrenden Ausgaben." },
  safeToSpendBelowBuffer: { ar: "الرصيد الحالي أقل من هدف احتياطي الطوارئ الخاص بك.", en: "Your current balance is below your emergency buffer target.", fr: "Votre solde actuel est en dessous de votre objectif de réserve d'urgence.", de: "Ihr aktueller Saldo liegt unter Ihrem Notfallpuffer-Ziel." },
  emergencyBufferLabel: { ar: "احتياطي الطوارئ", en: "Emergency buffer", fr: "Réserve d'urgence", de: "Notfallpuffer" },
  emergencyBufferHint: { ar: "مبلغ تريد أن يبقى دائماً جانباً، لا يُحتسب ضمن 'المتاح للإنفاق اليوم'.", en: "An amount you always want kept aside — excluded from your daily safe-to-spend.", fr: "Un montant que vous voulez toujours garder de côté — exclu du montant disponible par jour.", de: "Ein Betrag, den Sie immer zurückhalten möchten — vom täglichen Ausgabebudget ausgeschlossen." },
  upcomingExpensesTitle: { ar: "المصاريف القادمة", en: "Upcoming expenses", fr: "Dépenses à venir", de: "Bevorstehende Ausgaben" },
  upcomingIn7Days: { ar: "خلال 7 أيام", en: "Next 7 days", fr: "7 prochains jours", de: "Nächste 7 Tage" },
  upcomingIn30Days: { ar: "خلال 30 يوم", en: "Next 30 days", fr: "30 prochains jours", de: "Nächste 30 Tage" },
  upcomingInDays: { ar: "بعد", en: "in", fr: "dans", de: "in" },
  upcomingDaysUnit: { ar: "أيام", en: "days", fr: "jours", de: "Tage" },
  upcomingDayUnit: { ar: "يوم", en: "day", fr: "jour", de: "Tag" },
  upcomingEmpty: { ar: "لا توجد مصاريف متوقعة في الأيام القادمة.", en: "No expenses expected in the coming days.", fr: "Aucune dépense prévue dans les prochains jours.", de: "Keine Ausgaben in den kommenden Tagen erwartet." },
  goalsTitle: { ar: "أهدافي", en: "My goals", fr: "Mes objectifs", de: "Meine Ziele" },
  goalsAddBtn: { ar: "هدف جديد", en: "New goal", fr: "Nouvel objectif", de: "Neues Ziel" },
  goalsEmpty: { ar: "لم تُنشئ أي هدف بعد. أضِف هدفاً وتابع تقدّمك.", en: "No goals yet. Add one and track your progress.", fr: "Pas encore d'objectif. Ajoutez-en un et suivez votre progression.", de: "Noch keine Ziele. Fügen Sie eines hinzu und verfolgen Sie Ihren Fortschritt." },
  goalNameLabel: { ar: "اسم الهدف", en: "Goal name", fr: "Nom de l'objectif", de: "Zielname" },
  goalNamePlaceholder: { ar: "مثلا: سيارة", en: "e.g. Car", fr: "ex. Voiture", de: "z. B. Auto" },
  goalTargetLabel: { ar: "المبلغ المستهدف", en: "Target amount", fr: "Montant cible", de: "Zielbetrag" },
  goalCurrentLabel: { ar: "المبلغ الحالي", en: "Current amount", fr: "Montant actuel", de: "Aktueller Betrag" },
  goalMonthlyLabel: { ar: "الادخار الشهري", en: "Monthly saving", fr: "Épargne mensuelle", de: "Monatliches Sparen" },
  goalSaveBtn: { ar: "حفظ الهدف", en: "Save goal", fr: "Enregistrer l'objectif", de: "Ziel speichern" },
  goalDeleteConfirm: { ar: "هل أنت متأكد أنك تريد حذف هذا الهدف؟", en: "Are you sure you want to delete this goal?", fr: "Voulez-vous vraiment supprimer cet objectif ?", de: "Möchten Sie dieses Ziel wirklich löschen?" },
  goalExpected: { ar: "الوصول المتوقع", en: "Expected by", fr: "Atteint vers", de: "Erwartet bis" },
  goalExpectedUnknown: { ar: "أضِف ادخاراً شهرياً لنحسبه", en: "Add a monthly saving to estimate", fr: "Ajoutez une épargne mensuelle pour estimer", de: "Monatliches Sparen hinzufügen zur Schätzung" },
  goalReached: { ar: "الهدف تحقق", en: "Goal reached", fr: "Objectif atteint", de: "Ziel erreicht" },
  goalPerMonth: { ar: "شهريا", en: "/ month", fr: "/ mois", de: "/ Monat" },
  goalsAria: { ar: "أهداف الادخار", en: "Savings goals", fr: "Objectifs d'épargne", de: "Sparziele" },
  creditsTitle: { ar: "الكريدي والقروض", en: "Credits & Loans", fr: "Crédits et prêts", de: "Kredite & Darlehen" },
  creditsEmpty: { ar: "لم تُضِف أي قرض بعد. أضِف واحداً وتابع ما تبقّى عليك.", en: "No credits yet. Add one and track what's left to pay.", fr: "Aucun crédit pour l'instant. Ajoutez-en un et suivez ce qu'il vous reste à payer.", de: "Noch keine Kredite. Fügen Sie einen hinzu und verfolgen Sie, was noch zu zahlen ist." },
  creditAddTitle: { ar: "كريدي جديد", en: "New credit", fr: "Nouveau crédit", de: "Neuer Kredit" },
  creditEditTitle: { ar: "تعديل الكريدي", en: "Edit credit", fr: "Modifier le crédit", de: "Kredit bearbeiten" },
  creditDesc: { ar: "أدخل الاسم والسعر الكامل، ثم اختر: إما إعطاء المدة (وسنحسب لك القسط الشهري)، أو إعطاء القسط الشهري (وسنحسب لك المدة).", en: "Enter the name and total price, then choose: give us the duration (we'll calculate the monthly payment), or give us the monthly payment (we'll calculate the duration).", fr: "Entrez le nom et le prix total, puis choisissez : indiquez la durée (nous calculons la mensualité), ou indiquez la mensualité (nous calculons la durée).", de: "Geben Sie Name und Gesamtpreis ein, dann wählen Sie: Dauer angeben (wir berechnen die Monatsrate) oder Monatsrate angeben (wir berechnen die Dauer)." },
  creditNameLabel: { ar: "اسم الكريدي", en: "Credit name", fr: "Nom du crédit", de: "Kreditname" },
  creditNamePlaceholder: { ar: "مثلا: السيارة", en: "e.g. Car", fr: "ex. Voiture", de: "z. B. Auto" },
  creditTotalLabel: { ar: "الثمن الكامل", en: "Total price", fr: "Prix total", de: "Gesamtpreis" },
  creditModeDuration: { ar: "نعطي المدة", en: "I know the duration", fr: "Je connais la durée", de: "Ich kenne die Dauer" },
  creditModeMonthly: { ar: "نعطي شحال نخلص فالشهر", en: "I know the monthly payment", fr: "Je connais la mensualité", de: "Ich kenne die Monatsrate" },
  creditDurationLabel: { ar: "المدة (بالشهور)", en: "Duration (months)", fr: "Durée (mois)", de: "Dauer (Monate)" },
  creditDurationPlaceholder: { ar: "مثلا: 12", en: "e.g. 12", fr: "ex. 12", de: "z. B. 12" },
  creditMonthlyLabel: { ar: "شحال نقدر نخلص فالشهر", en: "Monthly payment", fr: "Mensualité", de: "Monatliche Rate" },
  creditPreviewMonthly: { ar: "الشهرية المحسوبة", en: "Calculated monthly payment", fr: "Mensualité calculée", de: "Berechnete Monatsrate" },
  creditPreviewDuration: { ar: "المدة المحسوبة", en: "Calculated duration", fr: "Durée calculée", de: "Berechnete Dauer" },
  creditSaveBtn: { ar: "حفظ الكريدي", en: "Save credit", fr: "Enregistrer le crédit", de: "Kredit speichern" },
  creditErrNeedOne: { ar: "دخل المدة أو الشهرية باش نقدرو نحسبو الباقي", en: "Enter either the duration or the monthly payment so we can calculate the rest", fr: "Entrez la durée ou la mensualité pour que nous puissions calculer le reste", de: "Geben Sie entweder die Dauer oder die Monatsrate ein, damit wir den Rest berechnen können" },
  creditRemainingLabel: { ar: "باقي", en: "remaining", fr: "restant", de: "verbleibend" },
  creditMonthsLeft: { ar: "شهر باقي", en: "months left", fr: "mois restants", de: "Monate übrig" },
  whatIfTitle: { ar: "ماذا لو؟", en: "What if?", fr: "Et si ?", de: "Was wäre wenn?" },
  whatIfTriggerBtn: { ar: "جرب سيناريو", en: "Try a scenario", fr: "Essayer un scénario", de: "Szenario testen" },
  whatIfDesc: { ar: "أدخل مبلغاً وشاهد كيف سيؤثر على ميزانيتك قبل أن تنفقه.", en: "Enter an amount to see how it would affect your budget before you spend it.", fr: "Entrez un montant pour voir son impact sur votre budget avant de le dépenser.", de: "Geben Sie einen Betrag ein, um die Auswirkung auf Ihr Budget zu sehen, bevor Sie ausgeben." },
  affordLauncherTitle: { ar: "واش نقدر نشريها؟", en: "Can I afford this?", fr: "Puis-je me le permettre ?", de: "Kann ich mir das leisten?" },
  affordLauncherSub: { ar: "شوف قبل ما تشري", en: "Check before you buy", fr: "Vérifiez avant d'acheter", de: "Vor dem Kauf prüfen" },
  affordTitle: { ar: "واش نقدر نشريها؟", en: "Can I afford this?", fr: "Puis-je me le permettre ?", de: "Kann ich mir das leisten?" },
  affordDesc: { ar: "دخل الحاجة لي بغيتي تشري، وشوف تأثيرها على الميزانية ديالك قبل ما تقرر.", en: "Enter what you want to buy, and see the impact on your budget before you decide.", fr: "Indiquez ce que vous voulez acheter et voyez l'impact sur votre budget avant de décider.", de: "Geben Sie ein, was Sie kaufen möchten, und sehen Sie die Auswirkung auf Ihr Budget, bevor Sie entscheiden." },
  affordItemLabel: { ar: "شنو بغيتي تشري", en: "What do you want to buy", fr: "Que voulez-vous acheter", de: "Was möchten Sie kaufen" },
  affordItemPlaceholder: { ar: "مثلا: سماعات جديدة", en: "e.g. new headphones", fr: "ex. nouveaux écouteurs", de: "z. B. neue Kopfhörer" },
  affordUrgency_need: { ar: "محتاجها", en: "Need it", fr: "J'en ai besoin", de: "Brauche ich" },
  affordUrgency_useful: { ar: "غادي تنفعني", en: "Useful", fr: "Utile", de: "Nützlich" },
  affordUrgency_want: { ar: "غير بغيتها", en: "Just want it", fr: "J'ai juste envie", de: "Möchte es nur" },
  affordCalcBtn: { ar: "شوف النتيجة", en: "See the result", fr: "Voir le résultat", de: "Ergebnis anzeigen" },
  affordGood: { ar: "تقدر تشريها بلا مشكل", en: "You can buy it comfortably", fr: "Vous pouvez l'acheter sans problème", de: "Sie können es sich problemlos leisten" },
  affordOk: { ar: "تقدر تشريها، ولكن دير حساب", en: "You can buy it, but adjust something", fr: "Vous pouvez l'acheter, mais ajustez quelque chose", de: "Sie können es kaufen, aber passen Sie etwas an" },
  affordBad: { ar: "أحسن تستنى شوية", en: "Better wait", fr: "Mieux vaut attendre", de: "Besser warten" },
  affordDaysEquivalent: { ar: "كتساوي", en: "Equals about", fr: "Équivaut à environ", de: "Entspricht etwa" },
  affordDaysUnit: { ar: "يوم من الميزانية اليومية", en: "days of your daily budget", fr: "jours de votre budget quotidien", de: "Tagen Ihres Tagesbudgets" },
  affordPerDayAfter: { ar: "الباقي فاليوم بعد الشراء", en: "Left per day after buying", fr: "Reste par jour après l'achat", de: "Verbleibend pro Tag nach dem Kauf" },
  affordSuggestionPrefix: { ar: "تقدر تنقص من", en: "You could cut back on", fr: "Vous pourriez réduire", de: "Sie könnten kürzen bei" },
  affordSuggestionBy: { ar: "بـ", en: "by", fr: "de", de: "um" },
  affordSuggestionSuffix: { ar: "هاد السيمانة باش تعوض.", en: "this week to make up for it.", fr: "cette semaine pour compenser.", de: "diese Woche, um es auszugleichen." },
  affordBuyNowBtn: { ar: "شريتها، زيدها كمصروف", en: "Bought it — add as expense", fr: "Acheté — ajouter comme dépense", de: "Gekauft — als Ausgabe hinzufügen" },
  affordDecideLaterBtn: { ar: "نقرر من بعد (24 سا)", en: "Decide later (24h)", fr: "Décider plus tard (24h)", de: "Später entscheiden (24 Std.)" },
  affordSavedForLaterNote: { ar: "تسجلات — غادي نذكروك من بعد 24 ساعة.", en: "Saved — we'll remind you in 24 hours.", fr: "Enregistré — nous vous le rappellerons dans 24 heures.", de: "Gespeichert — wir erinnern Sie in 24 Stunden." },
  pendingPurchasesTitle: { ar: "قرارات معلقة", en: "Pending decisions", fr: "Décisions en attente", de: "Ausstehende Entscheidungen" },
  pendingSavedSoFar: { ar: "وفرتي حتى الآن", en: "You've saved so far", fr: "Vous avez économisé jusqu'à présent", de: "Sie haben bisher gespart" },
  pendingReviewIn: { ar: "غادي نسولوك من بعد", en: "We'll ask again in", fr: "Nous vous redemanderons dans", de: "Wir fragen erneut in" },
  pendingStillWant: { ar: "مازال باغيها؟", en: "Still want it?", fr: "Toujours envie ?", de: "Immer noch gewünscht?" },
  pendingBoughtBtn: { ar: "شريتها", en: "Bought it", fr: "Acheté", de: "Gekauft" },
  pendingSkippedBtn: { ar: "تخليت عليها", en: "Skipped it", fr: "Abandonné", de: "Übersprungen" },
  pendingKeepWaitingBtn: { ar: "مازال كنفكر", en: "Keep waiting", fr: "Continuer à attendre", de: "Weiter warten" },
  whatIfAmountLabel: { ar: "المبلغ الذي تريد إنفاقه", en: "Amount you want to spend", fr: "Montant à dépenser", de: "Geplanter Ausgabebetrag" },
  whatIfCalcBtn: { ar: "احسب", en: "Calculate", fr: "Calculer", de: "Berechnen" },
  whatIfBefore: { ar: "قبل", en: "Before", fr: "Avant", de: "Vorher" },
  whatIfAfter: { ar: "بعد", en: "After", fr: "Après", de: "Nachher" },
  verdictGood: { ar: "مناسب", en: "Affordable", fr: "Abordable", de: "Leistbar" },
  verdictOk: { ar: "ممكن، لكنه سيؤثر", en: "Possible, but it will have an impact", fr: "Possible, mais aura un impact", de: "Möglich, aber mit Auswirkung" },
  verdictBad: { ar: "غير مناسب حاليا", en: "Not advisable right now", fr: "Pas conseillé actuellement", de: "Momentan nicht ratsam" },
  goalDelayMsg: { ar: "هذا المصروف سيؤخر هدف", en: "This expense would delay your goal", fr: "Cette dépense retarderait votre objectif", de: "Diese Ausgabe würde Ihr Ziel verzögern" },
  goalDelayMonths: { ar: "بحوالي", en: "by about", fr: "d'environ", de: "um etwa" },
  monthsUnit: { ar: "شهر", en: "months", fr: "mois", de: "Monate" },
  commitmentsTitle: { ar: "الالتزامات الشهرية", en: "Monthly commitments", fr: "Engagements mensuels", de: "Monatliche Verpflichtungen" },
  commitmentsMonthly: { ar: "الإجمالي الشهري", en: "Total monthly", fr: "Total mensuel", de: "Gesamt monatlich" },
  commitmentsYearly: { ar: "الإجمالي السنوي", en: "Total yearly", fr: "Total annuel", de: "Gesamt jährlich" },
  commitmentsPctIncome: { ar: "الالتزامات الثابتة كتستهلك", en: "Fixed commitments take up", fr: "Les engagements fixes représentent", de: "Feste Verpflichtungen beanspruchen" },
  commitmentsOfIncome: { ar: "من دخلك", en: "of your income", fr: "de vos revenus", de: "Ihres Einkommens" },
  commitmentsRecurringSavings: { ar: "منها ادخار واستثمار متكرر", en: "Of which recurring savings", fr: "Dont épargne récurrente", de: "Davon wiederkehrendes Sparen" },
  commitmentsEmpty: { ar: "لا توجد التزامات متكررة حالياً.", en: "No recurring commitments right now.", fr: "Aucun engagement récurrent pour le moment.", de: "Derzeit keine wiederkehrenden Verpflichtungen." },
  reportTitle: { ar: "التقرير الشهري", en: "Monthly report", fr: "Rapport mensuel", de: "Monatsbericht" },
  reportSavingsRate: { ar: "نسبة الادخار", en: "Savings rate", fr: "Taux d'épargne", de: "Sparquote" },
  reportSavingsAllocated: { ar: "الادخار والاستثمار", en: "Savings & investments", fr: "Épargne et investissements", de: "Sparen & Investitionen" },
  yearSummaryTitle: { ar: "الملخص السنوي", en: "Year summary", fr: "Résumé annuel", de: "Jahresübersicht" },
  yearNoData: { ar: "لا توجد بيانات لهذه السنة بعد.", en: "No data for this year yet.", fr: "Aucune donnée pour cette année pour l'instant.", de: "Noch keine Daten für dieses Jahr." },
  yearFutureHint: { ar: "هذه سنة قادمة — لا توجد بيانات بعد.", en: "This is a future year — no data yet.", fr: "Il s'agit d'une année future — pas encore de données.", de: "Dies ist ein zukünftiges Jahr — noch keine Daten." },
  yearProjectionTitle: { ar: "توقّع نهاية السنة", en: "Year-end projection", fr: "Projection de fin d'année", de: "Jahresendprognose" },
  yearProjectionText1: { ar: "إذا استمررت بهذه الوتيرة، بحلول نهاية", en: "If you continue at this pace, by the end of", fr: "Si vous continuez à ce rythme, d'ici la fin de", de: "Wenn Sie in diesem Tempo weitermachen, könnten Sie bis Ende" },
  yearProjectionText2: { ar: "يمكن أن يكون لديك حوالي", en: "you could have about", fr: ", vous pourriez avoir environ", de: "etwa" },
  yearProjectionText3: { ar: "مدخرة ومستثمرة، ورصيد متوقع قدره", en: "saved and invested, and a projected balance of", fr: "épargnés et investis, avec un solde prévu de", de: "gespart und investiert haben, mit einem prognostizierten Saldo von" },
  yearRegularIncome: { ar: "الدخل العادي", en: "Regular income", fr: "Revenu régulier", de: "Reguläres Einkommen" },
  yearBonusIncome: { ar: "دخل إضافي / علاوات", en: "Bonus / extra income", fr: "Primes / revenus exceptionnels", de: "Bonus / Sonderzahlungen" },
  yearProjectionBasis: { ar: "مبني على متوسط دخلك العادي:", en: "Based on your average regular income:", fr: "Basé sur votre revenu régulier moyen :", de: "Basierend auf Ihrem durchschnittlichen regulären Einkommen:" },
  yearProjectionBonusNote: { ar: "الدخل الإضافي ماكيتحسبش فالتوقع القادم", en: "extra income isn't assumed to repeat in the projection", fr: "les revenus exceptionnels ne sont pas supposés se répéter dans la projection", de: "Sonderzahlungen werden in der Prognose nicht als wiederkehrend angenommen" },
  reportTopCategory: { ar: "أكثر فئة صرف فيها", en: "Top spending category", fr: "Catégorie la plus dépensée", de: "Höchste Ausgabenkategorie" },
  reportBiggestExpense: { ar: "أكبر مصروف", en: "Biggest expense", fr: "Dépense la plus élevée", de: "Größte Ausgabe" },
  reportBiggestChange: { ar: "أكبر تغيير مقارنة بالشهر السابق", en: "Biggest change vs last month", fr: "Plus grand changement vs le mois dernier", de: "Größte Änderung ggü. letztem Monat" },
  reportVsLastMonth: { ar: "مقارنة بالشهر السابق", en: "vs last month", fr: "vs mois dernier", de: "ggü. letztem Monat" },
  reportNoData: { ar: "لا توجد بيانات كافية لإجراء مقارنة.", en: "Not enough data yet for a comparison.", fr: "Pas assez de données pour une comparaison.", de: "Noch nicht genug Daten für einen Vergleich." },
  reportNew: { ar: "جديد", en: "new", fr: "nouveau", de: "neu" },
  smartAlertsSection: { ar: "تنبيهات ذكية", en: "Smart alerts", fr: "Alertes intelligentes", de: "Intelligente Benachrichtigungen" },
  spendingSpikeLabel: { ar: "نبّهني إذا زادت المصاريف بشكل كبير عن الشهر الماضي", en: "Alert me if spending jumps sharply vs last month", fr: "M'alerter si les dépenses augmentent fortement vs le mois dernier", de: "Benachrichtigen bei starkem Ausgabenanstieg ggü. letztem Monat" },
  notifSpikeTitle: { ar: "زيادة فالمصاريف", en: "Spending increase", fr: "Hausse des dépenses", de: "Ausgabenanstieg" },
  notifSpikeBody: { ar: "زادت مصاريفك هذا الشهر بحوالي", en: "Your spending this month is up by about", fr: "Vos dépenses ce mois ont augmenté d'environ", de: "Ihre Ausgaben sind diesen Monat um etwa gestiegen" },
  notifNegativeTitle: { ar: "تنبيه: الرصيد المتوقع سالب", en: "Alert: projected balance is negative", fr: "Alerte : solde prévu négatif", de: "Warnung: prognostizierter Saldo negativ" },
  notifNegativeBody: { ar: "المصاريف القادمة والالتزامات أكبر من المتاح لديك هذا الشهر.", en: "Upcoming expenses and commitments exceed what's available this month.", fr: "Les dépenses à venir dépassent ce qui est disponible ce mois.", de: "Bevorstehende Ausgaben übersteigen das Verfügbare diesen Monat." },
  notifDueSoonTitle: { ar: "مصروف قريب", en: "Expense coming up", fr: "Dépense à venir", de: "Bevorstehende Ausgabe" },
  perMonthUnit: { ar: "شهريا", en: "monthly", fr: "mensuel", de: "monatlich" },
  viewAllUpcomingBtn: { ar: "عرض جميع المصاريف", en: "View all expenses", fr: "Voir toutes les dépenses", de: "Alle Ausgaben anzeigen" },
  viewAllCommitmentsBtn: { ar: "عرض الكل", en: "View all", fr: "Voir tout", de: "Alle anzeigen" },
  viewAllCategoriesBtn: { ar: "عرض جميع الفئات", en: "View all categories", fr: "Voir toutes les catégories", de: "Alle Kategorien anzeigen" },
  viewAllTransactionsBtn: { ar: "عرض جميع الحركات", en: "View all entries", fr: "Voir toutes les opérations", de: "Alle Einträge anzeigen" },
  showLessBtn: { ar: "أخف", en: "Show less", fr: "Réduire", de: "Weniger anzeigen" },
  topCommitmentsLabel: { ar: "أكبر الالتزامات", en: "Top commitments", fr: "Principaux engagements", de: "Größte Verpflichtungen" },
  loadingLabel: { ar: "كيتحمل...", en: "Loading...", fr: "Chargement...", de: "Wird geladen..." },
  forgotPasswordLink: { ar: "نسيت كلمة السر؟", en: "Forgot password?", fr: "Mot de passe oublié ?", de: "Passwort vergessen?" },
  backToLoginLink: { ar: "رجوع لتسجيل الدخول", en: "Back to login", fr: "Retour à la connexion", de: "Zurück zum Login" },
  sendResetLink: { ar: "صيفط رابط إعادة التعيين", en: "Send reset link", fr: "Envoyer le lien de réinitialisation", de: "Reset-Link senden" },
  forgotPasswordSent: { ar: "إذا كان هذا البريد مسجلاً، ستصلك رسالة تحتوي على رابط لإعادة تعيين كلمة السر.", en: "If that email is registered, you'll receive a link to reset your password.", fr: "Si cet e-mail est enregistré, vous recevrez un lien pour réinitialiser votre mot de passe.", de: "Falls diese E-Mail registriert ist, erhalten Sie einen Link zum Zurücksetzen des Passworts." },
  registerConfirmEmail: { ar: "أرسلنا لك رسالة تأكيد إلى بريدك الإلكتروني — افتحها لتفعيل الحساب.", en: "We've sent a confirmation email — open it to activate your account.", fr: "Nous avons envoyé un e-mail de confirmation — ouvrez-le pour activer votre compte.", de: "Wir haben eine Bestätigungs-E-Mail gesendet — öffnen Sie sie, um Ihr Konto zu aktivieren." },
  settingsPlanSection: { ar: "الاشتراك", en: "Plan", fr: "Abonnement", de: "Abo" },
  shiftEstimateLabel: { ar: "الدخل المتوقع من المناوبات", en: "Estimated income from shifts", fr: "Revenu estimé des services", de: "Geschätztes Einkommen aus Schichten" },
  shiftEstimateAddBtn: { ar: "أضف كدخل لهاد الشهر", en: "Add as income for this month", fr: "Ajouter comme revenu ce mois", de: "Als Einkommen für diesen Monat hinzufügen" },
  shiftEstimateAdded: { ar: "تمت الإضافة", en: "Added", fr: "Ajouté", de: "Hinzugefügt" },
  shiftEstimateSetup: { ar: "دوس هنا باش تحسب الدخل من المناوبات", en: "Tap here to calculate income from shifts", fr: "Appuyez ici pour calculer le revenu des services", de: "Hier tippen, um das Einkommen aus Schichten zu berechnen" },
  shiftEstimateDays: { ar: "يوم خدمة", en: "days worked", fr: "jours travaillés", de: "Arbeitstage" },
  shiftEstimateHours: { ar: "سا", en: "h", fr: "h", de: "Std" },
  shiftEstimateBase: { ar: "أساسي:", en: "Base:", fr: "Base :", de: "Grundlohn:" },
  shiftEstimateSurcharge: { ar: "زيادات:", en: "Surcharges:", fr: "Suppléments :", de: "Zuschläge:" },
  shiftCalcTitle: { ar: "حاسبة أجرة المناوبات", en: "Shift Pay Calculator", fr: "Calculateur de paie des services", de: "Schichtlohn-Rechner" },
  shiftCalcDesc: { ar: "دخل المعطيات هاد مرة وحدة، والتطبيق غادي يحسب ليك الدخل أوتوماتيك كل شهر من الـDienstplan.", en: "Enter this once — the app will then calculate your income automatically each month from the Dienstplan.", fr: "Renseignez ceci une seule fois — l'application calculera ensuite automatiquement vos revenus chaque mois à partir du Dienstplan.", de: "Geben Sie dies einmalig ein — die App berechnet dann automatisch jeden Monat Ihr Einkommen aus dem Dienstplan." },
  shiftCalcHourlyRate: { ar: "أجرة الساعة", en: "Hourly rate", fr: "Taux horaire", de: "Stundenlohn" },
  shiftCalcHoursPerShift: { ar: "ساعات كل مناوبة", en: "Hours per shift", fr: "Heures par service", de: "Stunden pro Schicht" },
  shiftCalcShiftTimesLabel: { ar: "توقيت كل مناوبة", en: "Shift start & end times", fr: "Horaires de chaque service", de: "Schichtzeiten" },
  shiftCalcLeaveHours: { ar: "ساعات العطلة/المرض في اليوم", en: "Hours per day (vacation / sick)", fr: "Heures par jour (congé / maladie)", de: "Stunden pro Tag (Urlaub / Krank)" },
  shiftCalcAnnualVacation: { ar: "عدد أيام العطلة المسموحة فالسنة", en: "Total vacation days allowed per year", fr: "Total des jours de congé par an", de: "Urlaubstage pro Jahr" },
  vacationDaysRemainingLabel: { ar: "أيام العطلة المتبقية", en: "Vacation days remaining", fr: "Jours de congé restants", de: "Verbleibende Urlaubstage" },
  vacationDaysUsedOf: { ar: "استعملتي", en: "used", fr: "utilisés", de: "genutzt" },
  vacationDaysOfLabel: { ar: "من", en: "of", fr: "sur", de: "von" },
  shiftCalcNightWindowLabel: { ar: "نافذة زيادة الليل — من الساعة لتال الساعة", en: "Night surcharge window — from / to", fr: "Plage du supplément de nuit — de / à", de: "Nachtzuschlag-Zeitraum — von / bis" },
  shiftCalcNightWindowHint: { ar: "الزيادة غادي تتحسب غير على الساعات لي كاينة فداخل هاد النافذة، ماشي على المناوبة كاملة.", en: "The surcharge is calculated only for the hours that fall inside this window, not the whole shift.", fr: "Le supplément n'est calculé que pour les heures comprises dans cette plage, pas pour tout le service.", de: "Der Zuschlag wird nur für die Stunden berechnet, die in diesen Zeitraum fallen — nicht für die ganze Schicht." },
  shiftCalcNightLabel: { ar: "زيادة الليل (Nachtzuschlag)", en: "Night surcharge", fr: "Supplément de nuit", de: "Nachtzuschlag" },
  shiftCalcWeekendLabel: { ar: "زيادة نهاية الأسبوع (Wochenendzuschlag)", en: "Weekend surcharge", fr: "Supplément week-end", de: "Wochenendzuschlag" },
  shiftCalcHolidayLabel: { ar: "زيادة الأعياد (Feiertagszuschlag)", en: "Holiday surcharge", fr: "Supplément jour férié", de: "Feiertagszuschlag" },
  shiftCalcHolidayHint: { ar: "باش تفعلها فيوم معين، حط عليه tag \"عيد\" فالـDienstplan.", en: "To apply it on a specific day, add the \"Holiday\" tag to that day in the Dienstplan.", fr: "Pour l'appliquer un jour précis, ajoutez l'étiquette « Jour férié » à ce jour dans le Dienstplan.", de: "Um es an einem bestimmten Tag anzuwenden, fügen Sie diesem Tag im Dienstplan den Tag „Feiertag\" hinzu." },
  tagHoliday: { ar: "عيد", en: "Holiday", fr: "Jour férié", de: "Feiertag" },
  shiftCalcNettoSection: { ar: "الراتب الصافي (Netto)", en: "Net salary (Netto)", fr: "Salaire net (Netto)", de: "Nettogehalt" },
  shiftCalcNettoDesc: { ar: "دخل هاد المعطيات مرة وحدة باش نقدرو نحسبو ليك الصافي من الإجمالي — تقدير غير رسمي، بلاصة الفيش ديال الأجرة الحقيقي.", en: "Enter this once so we can estimate your net salary from the gross — an unofficial estimate, not a substitute for your real payslip.", fr: "Renseignez ceci une fois pour estimer votre salaire net à partir du brut — une estimation non officielle, ne remplace pas votre vraie fiche de paie.", de: "Geben Sie dies einmalig ein, damit wir Ihr Nettogehalt aus dem Brutto schätzen können — eine inoffizielle Schätzung, kein Ersatz für Ihre echte Gehaltsabrechnung." },
  shiftCalcTaxYear: { ar: "سنة الضريبة", en: "Tax year", fr: "Année fiscale", de: "Steuerjahr" },
  shiftCalcSteuerklasse: { ar: "فئة الضريبة (Steuerklasse)", en: "Tax class (Steuerklasse)", fr: "Classe d'impôt (Steuerklasse)", de: "Steuerklasse" },
  shiftCalcKlasse: { ar: "فئة", en: "Class", fr: "Classe", de: "Klasse" },
  shiftCalcBundesland: { ar: "الولاية (Bundesland)", en: "Federal state (Bundesland)", fr: "Land (Bundesland)", de: "Bundesland" },
  shiftCalcAge: { ar: "العمر", en: "Age", fr: "Âge", de: "Alter" },
  shiftCalcChildren: { ar: "عدد الأولاد", en: "Number of children", fr: "Nombre d'enfants", de: "Anzahl Kinder" },
  shiftCalcChurchTax: { ar: "ضريبة الكنيسة (Kirchensteuer)", en: "Church tax (Kirchensteuer)", fr: "Impôt d'église (Kirchensteuer)", de: "Kirchensteuer" },
  shiftCalcKvPublic: { ar: "تأمين صحي عمومي", en: "Public health insurance", fr: "Assurance maladie publique", de: "Gesetzliche Krankenversicherung" },
  shiftCalcKvPrivate: { ar: "تأمين صحي خاص", en: "Private health insurance", fr: "Assurance maladie privée", de: "Private Krankenversicherung" },
  shiftCalcKvZusatz: { ar: "الاشتراك الإضافي (Zusatzbeitrag %)", en: "Additional contribution (Zusatzbeitrag %)", fr: "Cotisation supplémentaire (Zusatzbeitrag %)", de: "Zusatzbeitrag (%)" },
  shiftCalcKvPrivateHint: { ar: "التأمين الخاص عندو قسط ثابت بحساب مختلف — ماشي مبني على الراتب. هاد الحاسبة كتفترض بلا اشتراكات تأمين صحي/عناية فهاد الحالة؛ زيد القسط الحقيقي ديالك يدويا فمكان آخر.", en: "Private insurance has a flat premium calculated differently — not based on salary. This calculator assumes no KV/PV contributions in this case; add your real premium manually elsewhere.", fr: "L'assurance privée a une prime fixe calculée différemment — pas basée sur le salaire. Ce calculateur suppose aucune cotisation KV/PV dans ce cas ; ajoutez votre prime réelle manuellement ailleurs.", de: "Die private Versicherung hat eine Pauschalprämie, die anders berechnet wird — nicht gehaltsabhängig. Dieser Rechner geht in diesem Fall von keinen KV/PV-Beiträgen aus; fügen Sie Ihre echte Prämie manuell an anderer Stelle hinzu." },
  shiftCalcScopeLabel: { ar: "هاد التعديل يطبق على:", en: "Apply this change to:", fr: "Appliquer ce changement à :", de: "Diese Änderung anwenden auf:" },
  shiftCalcScopeThis: { ar: "هاد الشهر بوحدو", en: "This month only", fr: "Ce mois-ci seulement", de: "Nur diesen Monat" },
  shiftCalcScopeFuture: { ar: "من دابا وصاعد", en: "From now on", fr: "À partir de maintenant", de: "Ab jetzt" },
  shiftNettoLabel: { ar: "الراتب الصافي المقدر", en: "Estimated net salary", fr: "Salaire net estimé", de: "Geschätztes Nettogehalt" },
  shiftNettoTaxes: { ar: "الضرائب:", en: "Taxes:", fr: "Impôts :", de: "Steuern:" },
  shiftNettoSocial: { ar: "التأمينات الاجتماعية:", en: "Social contributions:", fr: "Cotisations sociales :", de: "Sozialabgaben:" },
  shiftNettoDisclaimer: { ar: "تقدير غير رسمي مبني على معطيات 2025/2026 — ماشي بديل عن الفيش الحقيقي ديال الأجرة.", en: "Unofficial estimate based on 2025/2026 parameters — not a substitute for your real payslip.", fr: "Estimation non officielle basée sur les paramètres 2025/2026 — ne remplace pas votre vraie fiche de paie.", de: "Inoffizielle Schätzung basierend auf den 2025/2026-Parametern — kein Ersatz für Ihre echte Gehaltsabrechnung." },
  allFeaturesFreeNotice: { ar: "كل الميزات متاحة مجانا دابا", en: "All features are free right now", fr: "Toutes les fonctionnalités sont gratuites pour le moment", de: "Alle Funktionen sind aktuell kostenlos" },
  planFree: { ar: "مجاني", en: "Free", fr: "Gratuit", de: "Kostenlos" },
  planPremium: { ar: "Premium", en: "Premium", fr: "Premium", de: "Premium" },
  planRenewsOn: { ar: "التجديد فـ", en: "Renews on", fr: "Renouvellement le", de: "Verlängerung am" },
  manageSubscriptionBtn: { ar: "إدارة الاشتراك", en: "Manage subscription", fr: "Gérer l'abonnement", de: "Abo verwalten" },
  upgradeToPremiumBtn: { ar: "الترقية لـ Premium", en: "Upgrade to Premium", fr: "Passer à Premium", de: "Auf Premium upgraden" },
  paywallTitle: { ar: "ميزة Premium", en: "Premium feature", fr: "Fonctionnalité Premium", de: "Premium-Funktion" },
  paywallBody: { ar: "هذه الميزة متاحة مع Premium — قم بترقية حسابك للاستفادة منها ومن ميزات أخرى.", en: "This feature is available with Premium — upgrade to unlock it and more.", fr: "Cette fonctionnalité est disponible avec Premium — passez à Premium pour la débloquer.", de: "Diese Funktion ist mit Premium verfügbar — upgraden Sie, um sie freizuschalten." },
  planYearlyLabel: { ar: "سنوي", en: "Yearly", fr: "Annuel", de: "Jährlich" },
  planMonthlyLabel: { ar: "شهري", en: "Monthly", fr: "Mensuel", de: "Monatlich" },
  paymentComingSoon: { ar: "الدفع غير مفعّل هنا بعد — سيتم تفعيله قريباً.", en: "Payment isn't wired up yet here — coming soon.", fr: "Le paiement n'est pas encore actif ici — bientôt disponible.", de: "Die Zahlung ist hier noch nicht eingerichtet — bald verfügbar." },
  premiumBadge: { ar: "Premium", en: "Premium", fr: "Premium", de: "Premium" },
  goalLimitReached: { ar: "تسمح الخطة المجانية بهدف واحد فقط. رقِّ إلى Premium لإضافة أهداف غير محدودة.", en: "The free plan allows a single goal. Upgrade to Premium for unlimited goals.", fr: "Le plan gratuit permet un seul objectif. Passez à Premium pour des objectifs illimités.", de: "Der kostenlose Plan erlaubt nur ein Ziel. Upgraden Sie auf Premium für unbegrenzte Ziele." },
  settingsInstallSection: { ar: "تثبيت التطبيق", en: "Install app", fr: "Installer l'application", de: "App installieren" },
  installAlreadyInstalled: { ar: "التطبيق مثبت ديجا وكيخدم كتطبيق مستقل.", en: "The app is already installed and running standalone.", fr: "L'application est déjà installée et fonctionne en mode autonome.", de: "Die App ist bereits installiert und läuft eigenständig." },
  installNowBtn: { ar: "ثبّت التطبيق الآن", en: "Install now", fr: "Installer maintenant", de: "Jetzt installieren" },
  installStepsIOS: { ar: "اضغط زر المشاركة في Safari، ثم \"أضف إلى الشاشة الرئيسية\".", en: "Tap the Share button in Safari, then \"Add to Home Screen\".", fr: "Appuyez sur le bouton Partager dans Safari, puis « Sur l'écran d'accueil ».", de: "Tippen Sie in Safari auf Teilen, dann auf „Zum Home-Bildschirm\"." },
  installStepsGeneric: { ar: "من قائمة المتصفح، اختار \"تثبيت التطبيق\" أو \"إضافة إلى الشاشة الرئيسية\".", en: "From your browser menu, choose \"Install app\" or \"Add to Home Screen\".", fr: "Dans le menu du navigateur, choisissez « Installer l'application ».", de: "Wählen Sie im Browsermenü „App installieren\"." },
  syncBlockedTitle: { ar: "تعذّر التأكد من بياناتك", en: "Couldn't confirm your data", fr: "Impossible de confirmer vos données", de: "Ihre Daten konnten nicht bestätigt werden" },
  syncBlockedBody: { ar: "أنت تشاهد نسخة محفوظة محلياً. لن تُسجَّل التعديلات حتى نتأكد من الاتصال بالخادم — هذا لضمان عدم فقدان أي معلومة.", en: "You're viewing a locally cached copy. Changes won't be saved until we confirm the connection to the server — this is to make sure nothing gets lost.", fr: "Vous consultez une copie enregistrée localement. Les modifications ne seront pas sauvegardées tant que la connexion au serveur n'est pas confirmée — ceci pour éviter toute perte de données.", de: "Sie sehen eine lokal zwischengespeicherte Kopie. Änderungen werden erst gespeichert, wenn die Serververbindung bestätigt ist — so geht nichts verloren." },
  syncBlockedRetryBtn: { ar: "إعادة المحاولة", en: "Retry", fr: "Réessayer", de: "Erneut versuchen" },
  showPasswordAria: { ar: "وري كلمة السر", en: "Show password", fr: "Afficher le mot de passe", de: "Passwort anzeigen" },
  hidePasswordAria: { ar: "خبي كلمة السر", en: "Hide password", fr: "Masquer le mot de passe", de: "Passwort verbergen" },
  settingsAppearanceSection: { ar: "المظهر", en: "Appearance", fr: "Apparence", de: "Erscheinungsbild" },
  themeDarkLabel: { ar: "غامق", en: "Dark", fr: "Sombre", de: "Dunkel" },
  themeLightLabel: { ar: "فاتح", en: "Light", fr: "Clair", de: "Hell" },
  premiumWelcomeTitle: { ar: "مرحبا بيك فـ Premium", en: "Welcome to Premium", fr: "Bienvenue dans Premium", de: "Willkommen bei Premium" },
  premiumWelcomeBody: { ar: "أصبح لديك الآن وصول كامل لجميع الميزات — المتاح للإنفاق اليوم، أهداف غير محدودة، وتنبيهات ذكية.", en: "You now have full access to every feature — Safe to Spend, unlimited goals, and smart alerts.", fr: "Vous avez maintenant accès à toutes les fonctionnalités — budget quotidien, objectifs illimités et alertes intelligentes.", de: "Sie haben jetzt vollen Zugriff auf alle Funktionen — Safe to Spend, unbegrenzte Ziele und intelligente Benachrichtigungen." },
  premiumWelcomeBtn: { ar: "هيا بنا", en: "Let's go", fr: "C'est parti", de: "Los geht's" },
  settingsLegalSection: { ar: "قانوني ومساعدة", en: "Legal & Help", fr: "Légal et aide", de: "Rechtliches & Hilfe" },
  legalPrivacyTitle: { ar: "سياسة الخصوصية", en: "Privacy Policy", fr: "Politique de confidentialité", de: "Datenschutzerklärung" },
  legalTermsTitle: { ar: "شروط الاستخدام", en: "Terms of Service", fr: "Conditions d'utilisation", de: "Nutzungsbedingungen" },
  legalImpressumTitle: { ar: "بيانات الشركة (Impressum)", en: "Legal Notice (Impressum)", fr: "Mentions légales", de: "Impressum" },
  legalHelpBtn: { ar: "مساعدة ودعم", en: "Help & Support", fr: "Aide et support", de: "Hilfe & Support" },
  legalContactBtn: { ar: "تواصل معنا", en: "Contact us", fr: "Nous contacter", de: "Kontaktiere uns" },
  legalFeedbackBtn: { ar: "اقترح تحسين", en: "Send feedback", fr: "Envoyer un avis", de: "Feedback senden" },
  legalHelpMailSubject: { ar: "مساعدة - PARAPLANER", en: "Help - PARAPLANER", fr: "Aide - PARAPLANER", de: "Hilfe - PARAPLANER" },
  legalFeedbackMailSubject: { ar: "اقتراح - PARAPLANER", en: "Feedback - PARAPLANER", fr: "Avis - PARAPLANER", de: "Feedback - PARAPLANER" },
  dienstplanTitle: { ar: "جدول المناوبات (Dienstplan)", en: "Shift Schedule (Dienstplan)", fr: "Planning des services (Dienstplan)", de: "Dienstplan" },
  dienstplanStartBtn: { ar: "ابدا عمر الجدول", en: "Start filling in the schedule", fr: "Commencer à remplir le planning", de: "Dienstplan ausfüllen" },
  dienstplanEmptyHint: { ar: "اضغط على كل يوم في التقويم وحدد نوع المناوبة — Früh، Spät، Nacht، راحة، عطلة، أو مرض.", en: "Tap each day on the calendar and pick the shift type — Früh, Spät, Nacht, off, vacation, or sick.", fr: "Touchez chaque jour du calendrier et choisissez le type de service — Früh, Spät, Nacht, repos, congé ou maladie.", de: "Tippen Sie auf jeden Tag im Kalender und wählen Sie die Schicht — Früh, Spät, Nacht, frei, Urlaub oder krank." },
  dienstplanTapToEdit: { ar: "اضغط على أي يوم لتعديله", en: "Tap any day to change it", fr: "Touchez un jour pour le modifier", de: "Tippen Sie auf einen Tag, um ihn zu ändern" },
  dienstplanDayLabel: { ar: "اليوم", en: "Day", fr: "Jour", de: "Tag" },
  dienstplanToday: { ar: "مناوبة اليوم", en: "Today's shift", fr: "Service du jour", de: "Heutige Schicht" },
  dienstplanTomorrow: { ar: "مناوبة غدا", en: "Tomorrow's shift", fr: "Service de demain", de: "Morgige Schicht" },
  dienstplanReminder: { ar: "تذكير بالمناوبة الجاية", en: "Reminder for your next shift", fr: "Rappel de votre prochain service", de: "Erinnerung an Ihre nächste Schicht" },
  dienstplanNoteLabel: { ar: "ملاحظة اليوم", en: "Day note", fr: "Note du jour", de: "Notiz für den Tag" },
  dienstplanNotePlaceholder: { ar: "اكتب هنا ما تريد فعله في هذا اليوم...", en: "Write what you want to do on this day...", fr: "Écrivez ce que vous voulez faire ce jour-là...", de: "Schreiben Sie, was Sie an diesem Tag machen möchten..." },
  dienstplanTagsLabel: { ar: "علامات", en: "Tags", fr: "Étiquettes", de: "Tags" },
  tagTraining: { ar: "تكوين", en: "Training", fr: "Formation", de: "Fortbildung" },
  tagMeeting: { ar: "اجتماع", en: "Meeting", fr: "Réunion", de: "Meeting" },
  tagOvertime: { ar: "ساعات إضافية", en: "Overtime", fr: "Heures sup.", de: "Überstunden" },
  tagShiftSwap: { ar: "تبديل مناوبة", en: "Shift swap", fr: "Échange de service", de: "Diensttausch" },
  tagNote: { ar: "ملاحظة", en: "Note", fr: "Note", de: "Notiz" },
  tagFruit: { ar: "فواكه", en: "Fruit", fr: "Fruits", de: "Obst" },
  tagSnack: { ar: "وجبة خفيفة", en: "Snack", fr: "Collation", de: "Snack" },
  tagDrink: { ar: "مشروب", en: "Drink", fr: "Boisson", de: "Getränk" },
  tagFood: { ar: "أكل", en: "Food", fr: "Repas", de: "Essen" },
  tabHome: { ar: "الرئيسية", en: "Home", fr: "Accueil", de: "Start" },
  tabStats: { ar: "إحصائيات", en: "Stats", fr: "Stats", de: "Statistik" },
  tabGoals: { ar: "الأهداف", en: "Goals", fr: "Objectifs", de: "Ziele" },
  tabTransactions: { ar: "الحركات", en: "Activity", fr: "Activité", de: "Aktivität" },
  trendChartTitle: { ar: "الاتجاه الشهري (آخر 6 أشهر)", en: "Monthly trend (last 6 months)", fr: "Tendance mensuelle (6 derniers mois)", de: "Monatstrend (letzte 6 Monate)" },
  shiftFruh: { ar: "صباحية", en: "Morning", fr: "Matin", de: "Früh" },
  shiftSpat: { ar: "مسائية", en: "Afternoon", fr: "Après-midi", de: "Spät" },
  shiftNacht: { ar: "ليلية", en: "Night", fr: "Nuit", de: "Nacht" },
  shiftTag: { ar: "نهارية", en: "Day", fr: "Jour", de: "Tag" },
  shiftSystemLabel: { ar: "نظام المناوبات فالخدمة", en: "Your workplace's shift system", fr: "Système de service de votre travail", de: "Schichtsystem Ihres Arbeitsplatzes" },
  shiftSystem3: { ar: "3 مناوبات (صباحية، مسائية، ليلية)", en: "3 shifts (Früh, Spät, Nacht)", fr: "3 services (matin, après-midi, nuit)", de: "3 Schichten (Früh, Spät, Nacht)" },
  shiftSystem2: { ar: "مناوبتين (نهارية، ليلية)", en: "2 shifts (Tag, Nacht)", fr: "2 services (jour, nuit)", de: "2 Schichten (Tag, Nacht)" },
  shiftFrei: { ar: "راحة", en: "Off", fr: "Repos", de: "Frei" },
  shiftUrlaub: { ar: "عطلة", en: "Vacation", fr: "Congé", de: "Urlaub" },
  shiftKrank: { ar: "مرض", en: "Sick", fr: "Maladie", de: "Krank" },
};

function t(key, lang) {
  const entry = STRINGS[key];
  if (!entry) return key;
  return entry[lang] || entry.en || key;
}

const LangContext = createContext(null);
function useLang() { return useContext(LangContext); }

/* =========================================================
   Icons — plain SVG line icons (no emoji anywhere in the UI)
   ========================================================= */
const ICONS = {
  home: '<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
  cart: '<circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/>',
  utensils: '<path d="M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2"/><path d="M7 2v20"/><path d="M21 15V2a5 5 0 0 0-5 5v6c0 1.1.9 2 2 2h3Zm0 0v7"/>',
  car: '<path d="M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4c0 .6.4 1 1 1h2"/><circle cx="7" cy="17" r="2"/><path d="M9 17h6"/><circle cx="17" cy="17" r="2"/>',
  fuel: '<path d="M3 22h12"/><path d="M4 9h10"/><path d="M14 22V4a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v18"/><path d="M14 13h2a2 2 0 0 1 2 2v2a2 2 0 0 0 2 2a2 2 0 0 0 2-2V9.83a2 2 0 0 0-.59-1.42L18 5"/>',
  bulb: '<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/>',
  wifi: '<path d="M12 20h.01"/><path d="M2 8.82a15 15 0 0 1 20 0"/><path d="M5 12.86a10 10 0 0 1 14 0"/><path d="M8.5 16.43a5 5 0 0 1 7 0"/>',
  medical: '<path d="M11 2v2"/><path d="M5 2v2"/><path d="M5 3H4a2 2 0 0 0-2 2v4a6 6 0 0 0 12 0V5a2 2 0 0 0-2-2h-1"/><path d="M8 15a6 6 0 0 0 12 0v-3"/><circle cx="20" cy="10" r="2"/>',
  education: '<path d="M21.42 10.92a1 1 0 0 0-.02-1.83L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.83l8.57 3.91a2 2 0 0 0 1.66 0z"/><path d="M22 10v6"/><path d="M6 12.5V16a6 3 0 0 0 12 0v-3.5"/>',
  film: '<rect x="2" y="4" width="20" height="16" rx="2"/><path d="M7 4v16M17 4v16M2 9h5M17 9h5M2 15h5M17 15h5"/>',
  repeat: '<path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/>',
  shirt: '<path d="M12.6 2.4a1 1 0 0 0-1.2 0L3 8.5a1 1 0 0 0 .3 1.7l2.5 1a1 1 0 0 1 .6.9V21a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1v-8.9a1 1 0 0 1 .6-.9l2.5-1a1 1 0 0 0 .3-1.7z"/>',
  card: '<rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20"/>',
  piggy: '<path d="M19 5c-1.5 0-2.8 1.4-3 2-3.5-1.5-11-.3-11 5 0 1.8.5 3 2 4.5V20h4v-2h3v2h4v-4c1-.5 1.7-1 2-2h2v-4h-2c-.2-1-.8-1.7-1.5-2.3z"/><path d="M2 9v1c0 1.1.9 2 2 2h1"/><path d="M16 11h.01"/>',
  plane: '<path d="M2 12l20-8-8 20-2-8-8-2z"/><path d="M12 12l10-8"/>',
  gift: '<rect x="3" y="8" width="18" height="13" rx="1"/><path d="M3 8h18"/><path d="M12 8v13"/><path d="M12 8c-1.6-4-6-4-6-1.3S9 8 12 8z"/><path d="M12 8c1.6-4 6-4 6-1.3S15 8 12 8z"/>',
  paw: '<circle cx="6.2" cy="8.2" r="1.6"/><circle cx="10.6" cy="5.2" r="1.6"/><circle cx="15.4" cy="5.2" r="1.6"/><circle cx="19.8" cy="8.2" r="1.6"/><path d="M6 15c0-3 3-4 6-4s6 1 6 4-2.6 5-6 5-6-2-6-5z"/>',
  baby: '<path d="M9 12h.01"/><path d="M15 12h.01"/><path d="M10 16c.5.3 1.2.5 2 .5s1.5-.2 2-.5"/><path d="M19 6.3a9 9 0 0 1 1.8 3.9 2 2 0 0 1 0 3.6 9 9 0 0 1-17.6 0 2 2 0 0 1 0-3.6A9 9 0 0 1 12 3c2 0 3.5 1.1 3.5 2.5s-.9 2.5-2 2.5c-.8 0-1.5-.4-1.5-1"/>',
  dumbbell: '<path d="M4 9v6"/><path d="M2 8v8"/><path d="M20 9v6"/><path d="M22 8v8"/><path d="M6 12h12"/>',
  heart: '<path d="M2 9.5a5.5 5.5 0 0 1 9.591-3.676.6.6 0 0 0 .818 0A5.5 5.5 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 0L5 15c-1.5-1.5-3-3.21-3-5.5"/>',
  receipt: '<path d="M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"/><path d="M16 8h-6"/><path d="M14 12H8"/>',
  barChart: '<path d="M3 3v18h18"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/>',
  briefcase: '<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>',
  laptop: '<path d="M18 5a2 2 0 0 1 2 2v8.53a2 2 0 0 0 .21.9l1.07 2.12a1 1 0 0 1-.9 1.45H3.62a1 1 0 0 1-.9-1.45l1.07-2.12A2 2 0 0 0 4 15.53V7a2 2 0 0 1 2-2z"/>',
  trending: '<path d="M16 7h6v6"/><path d="m22 7-8.5 8.5-5-5L2 17"/>',
  dollar: '<path d="M12 2v20"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>',
  box: '<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>',
  clock: '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
  calendar: '<path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/>',
  flag: '<path d="M5 21V4"/><path d="M5 4h13l-2.2 4.3L18 12.5H5"/>',
  globe: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3c2.8 3 2.8 15 0 18"/><path d="M12 3c-2.8 3-2.8 15 0 18"/>',
  bolt: '<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>',
  edit: '<path d="M21.17 6.81a1 1 0 0 0-3.99-3.99L3.84 16.17a2 2 0 0 0-.5.83l-1.32 4.35a.5.5 0 0 0 .62.62l4.35-1.32a2 2 0 0 0 .83-.5z"/><path d="m15 5 4 4"/>',
  trash: '<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6M14 11v6"/>',
  sliders: '<path d="M21 4h-7M10 4H3"/><path d="M21 12h-9M8 12H3"/><path d="M21 20h-6M11 20H3"/><path d="M14 2v4M8 10v4M16 18v4"/>',
  bell: '<path d="M10.27 21a2 2 0 0 0 3.46 0"/><path d="M3.26 15.33A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.67C19.41 13.96 18 12.5 18 8A6 6 0 0 0 6 8c0 4.5-1.41 5.96-2.74 7.33"/>',
  upload: '<path d="M12 3v12"/><path d="m17 8-5-5-5 5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>',
  search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
  shield: '<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.79 17 5 19 5a1 1 0 0 1 1 1z"/>',
  target: '<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1" fill="currentColor" stroke="none"/>',
  calculator: '<rect width="16" height="20" x="4" y="2" rx="2"/><path d="M8 6h8"/><path d="M16 10h.01"/><path d="M12 10h.01"/><path d="M8 10h.01"/><path d="M12 14h.01"/><path d="M8 14h.01"/><path d="M12 18h.01"/><path d="M8 18h.01"/><path d="M16 14v4"/>',
  crown: '<path d="m2 4 3 12h14l3-12-6 7-4-7-4 7-6-7zm3 16h14"/>',
  lock: '<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
  eye: '<path d="M2.06 12.35a1 1 0 0 1 0-.7 10.75 10.75 0 0 1 19.88 0 1 1 0 0 1 0 .7 10.75 10.75 0 0 1-19.88 0"/><circle cx="12" cy="12" r="3"/>',
  eyeOff: '<path d="M10.73 5.08A10.74 10.74 0 0 1 21.94 11.65a1 1 0 0 1 0 .7 10.75 10.75 0 0 1-1.44 2.49"/><path d="M14.08 14.16a3 3 0 0 1-4.24-4.24"/><path d="M17.48 17.5A10.75 10.75 0 0 1 2.06 12.35a1 1 0 0 1 0-.7 10.75 10.75 0 0 1 4.45-5.14"/><path d="m2 2 20 20"/>',
  moon: '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>',
  sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
  file: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>',
  building: '<path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z"/><path d="M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2"/><path d="M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2"/><path d="M10 6h4M10 10h4M10 14h4M10 18h4"/>',
  helpCircle: '<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>',
  mail: '<path d="m22 7-8.99 5.73a2 2 0 0 1-2.01 0L2 7"/><rect x="2" y="4" width="20" height="16" rx="2"/>',
  star: '<path d="M11.53 2.3a.53.53 0 0 1 .95 0l2.31 4.68a2.12 2.12 0 0 0 1.6 1.16l5.16.75a.53.53 0 0 1 .3.91l-3.74 3.64a2.12 2.12 0 0 0-.61 1.88l.88 5.14a.53.53 0 0 1-.77.56l-4.62-2.43a2.12 2.12 0 0 0-1.97 0L6.4 21a.53.53 0 0 1-.77-.55l.88-5.14a2.12 2.12 0 0 0-.61-1.88L2.16 9.8a.53.53 0 0 1 .3-.9l5.16-.76a2.12 2.12 0 0 0 1.6-1.16z"/>',
  chevronRight: '<path d="m9 18 6-6-6-6"/>',
  calendarGrid: '<path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/><path d="M8 14h.01"/><path d="M12 14h.01"/><path d="M16 14h.01"/><path d="M8 18h.01"/><path d="M12 18h.01"/><path d="M16 18h.01"/>',
  camera: '<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/>',
  moneyBox: '<path d="M4 10l1.5-4h13L20 10"/><rect x="4" y="10" width="16" height="9" rx="1.5"/><circle cx="12" cy="5" r="2.2"/>',
  users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
  apple: '<path d="M12 20c-4 0-7-3.5-7-8.5C5 8 7 6 9.5 6c1 0 1.8.3 2.5.9.7-.6 1.5-.9 2.5-.9C17 6 19 8 19 11.5c0 5-3 8.5-7 8.5z"/><path d="M12 6c0-1.7 1-3 2.5-3.5"/>',
  cookie: '<path d="M12 2a10 10 0 1 0 10 10 4 4 0 0 1-5-5 4 4 0 0 1-5-5"/><path d="M8.5 8.5v.01"/><path d="M16 15.5v.01"/><path d="M12 12v.01"/><path d="M11 17v.01"/><path d="M7 14v.01"/>',
  cup: '<path d="M17 8h1a4 4 0 1 1 0 8h-1"/><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4z"/><path d="M6 2v4M10 2v4M14 2v4"/>',
  mic: '<path d="M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><path d="M12 19v3"/>',
  'mic-off': '<path d="M12 2a3 3 0 0 0-3 3v5.5"/><path d="M9 9v1a3 3 0 0 0 6 0v-1"/><path d="M19 10v2a7 7 0 0 1-3 5.5"/><path d="M5 10v2a7 7 0 0 0 5 6.5"/><path d="M19 21l-2-2"/><path d="M5 5l2 2"/>',
  x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
  arrowLeft: '<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>',
};

function Icon({ name, size = 18, className }) {
  const html = ICONS[name] || ICONS.box;
  return (
    <svg
      width={size} height={size} viewBox="0 0 24 24"
      fill="none" stroke="currentColor" strokeWidth="1.7"
      strokeLinecap="round" strokeLinejoin="round"
      style={{ flexShrink: 0 }}
      className={className}
      dangerouslySetInnerHTML={{ __html: html }}
    />
  );
}

function EmptyState({ icon, text }) {
  return (
    <div className="empty-state">
      <div className="empty-state-icon"><Icon name={icon} size={22} /></div>
      <p>{text}</p>
    </div>
  );
}


/* =========================================================
   Categories
   ========================================================= */
const EXPENSE_CATEGORIES = [
  { id: "housing", icon: "home", ar: "السكن (كراء/قرض)", en: "Housing (rent/loan)", fr: "Logement (loyer/prêt)", de: "Wohnen (Miete/Kredit)" },
  { id: "groceries", icon: "cart", ar: "البقالة والسوق", en: "Groceries", fr: "Courses", de: "Lebensmittel" },
  { id: "dining", icon: "utensils", ar: "مطاعم وطعام خارج المنزل", en: "Dining out", fr: "Restaurants", de: "Auswärts essen" },
  { id: "transport", icon: "car", ar: "النقل والمواصلات", en: "Transport", fr: "Transport", de: "Transport" },
  { id: "fuel", icon: "fuel", ar: "الوقود", en: "Fuel", fr: "Carburant", de: "Kraftstoff" },
  { id: "utilities", icon: "bulb", ar: "الفواتير (كهرباء/ماء)", en: "Utilities (electricity/water)", fr: "Charges (électricité/eau)", de: "Nebenkosten (Strom/Wasser)" },
  { id: "internet", icon: "wifi", ar: "الإنترنت والهاتف", en: "Internet & phone", fr: "Internet et téléphone", de: "Internet & Telefon" },
  { id: "health", icon: "medical", ar: "الصحة والتأمين", en: "Health & insurance", fr: "Santé et assurance", de: "Gesundheit & Versicherung" },
  { id: "education", icon: "education", ar: "التعليم", en: "Education", fr: "Éducation", de: "Bildung" },
  { id: "entertainment", icon: "film", ar: "الترفيه والخروجات", en: "Entertainment", fr: "Loisirs", de: "Unterhaltung" },
  { id: "subscriptions", icon: "repeat", ar: "الاشتراكات", en: "Subscriptions", fr: "Abonnements", de: "Abonnements" },
  { id: "clothing", icon: "shirt", ar: "ملابس وتسوق", en: "Clothing & shopping", fr: "Vêtements et shopping", de: "Kleidung & Einkaufen" },
  { id: "debt", icon: "card", ar: "ديون وتقسيط", en: "Debt & installments", fr: "Dettes et mensualités", de: "Schulden & Raten" },
  { id: "savings", icon: "piggy", ar: "ادخار واستثمار", en: "Savings & investment", fr: "Épargne et investissement", de: "Sparen & Investieren" },
  { id: "travel", icon: "plane", ar: "السفر", en: "Travel", fr: "Voyage", de: "Reisen" },
  { id: "gifts", icon: "gift", ar: "هدايا", en: "Gifts", fr: "Cadeaux", de: "Geschenke" },
  { id: "pets", icon: "paw", ar: "الحيوانات الأليفة", en: "Pets", fr: "Animaux", de: "Haustiere" },
  { id: "childcare", icon: "baby", ar: "الأطفال", en: "Childcare", fr: "Enfants", de: "Kinderbetreuung" },
  { id: "fitness", icon: "dumbbell", ar: "الرياضة واللياقة", en: "Fitness", fr: "Sport et fitness", de: "Fitness" },
  { id: "charity", icon: "heart", ar: "الصدقة والتبرعات", en: "Charity & donations", fr: "Charité et dons", de: "Spenden" },
  { id: "taxes", icon: "receipt", ar: "الضرائب والرسوم", en: "Taxes & fees", fr: "Impôts et frais", de: "Steuern & Gebühren" },
  { id: "quick", icon: "clock", ar: "مصروف مؤقت", en: "Temporary expense", fr: "Dépense temporaire", de: "Temporäre Ausgabe" },
  { id: "other", icon: "box", ar: "مصاريف أخرى", en: "Other expenses", fr: "Autres dépenses", de: "Sonstige Ausgaben" },
];

const INCOME_CATEGORIES = [
  { id: "salary", icon: "briefcase", ar: "الراتب الأساسي", en: "Base salary", fr: "Salaire de base", de: "Grundgehalt" },
  { id: "bonus", icon: "gift", ar: "علاوة / دخل إضافي (مثلاً Weihnachtsgeld)", en: "Bonus / extra income (e.g. Weihnachtsgeld)", fr: "Prime / revenu exceptionnel", de: "Bonus / Sonderzahlung (z. B. Weihnachtsgeld)" },
  { id: "freelance", icon: "laptop", ar: "عمل إضافي/حر", en: "Freelance / side job", fr: "Travail indépendant", de: "Nebenjob/Freelance" },
  { id: "investment", icon: "trending", ar: "عوائد استثمار", en: "Investment returns", fr: "Revenus d'investissement", de: "Kapitalerträge" },
  { id: "gift_income", icon: "gift", ar: "هدايا ومساعدات", en: "Gifts / support", fr: "Cadeaux / aide", de: "Geschenke/Unterstützung" },
  { id: "other_income", icon: "dollar", ar: "مداخيل أخرى", en: "Other income", fr: "Autres revenus", de: "Sonstige Einnahmen" },
];

const COMMON_EXPENSES = [
  { category: "housing", labelKey: "commonRent", icon: "home" },
  { category: "groceries", labelKey: "commonGroceries", icon: "cart" },
  { category: "internet", labelKey: "commonPhone", icon: "wifi" },
  { category: "subscriptions", labelKey: "commonNetflix", icon: "repeat" },
  { category: "subscriptions", labelKey: "commonSpotify", icon: "repeat" },
  { category: "transport", labelKey: "commonTransport", icon: "car" },
  { category: "fuel", labelKey: "commonFuel", icon: "fuel" },
  { category: "fitness", labelKey: "commonGym", icon: "dumbbell" },
  { category: "health", labelKey: "commonInsurance", icon: "shield" },
  { category: "debt", labelKey: "commonDebt", icon: "card" },
  { category: "utilities", labelKey: "commonElectricity", icon: "bulb" },
  { category: "utilities", labelKey: "commonWater", icon: "bulb" },
  { category: "utilities", labelKey: "commonHeating", icon: "bulb" },
  { category: "transport", labelKey: "commonCarInsurance", icon: "car" },
  { category: "health", labelKey: "commonHealthInsurance", icon: "medical" },
  { category: "subscriptions", labelKey: "commonAmazon", icon: "repeat" },
  { category: "subscriptions", labelKey: "commonDisney", icon: "repeat" },
  { category: "subscriptions", labelKey: "commonYoutube", icon: "repeat" },
  { category: "subscriptions", labelKey: "commonCloud", icon: "repeat" },
  { category: "education", labelKey: "commonSchool", icon: "education" },
  { category: "health", labelKey: "commonPharmacy", icon: "medical" },
  { category: "dining", labelKey: "commonRestaurant", icon: "utensils" },
  { category: "dining", labelKey: "commonDelivery", icon: "utensils" },
  { category: "dining", labelKey: "commonCoffee", icon: "utensils" },
  { category: "transport", labelKey: "commonParking", icon: "car" },
  { category: "transport", labelKey: "commonTaxi", icon: "car" },
  { category: "travel", labelKey: "commonFlight", icon: "plane" },
  { category: "other", labelKey: "commonRemittance", icon: "dollar" },
  { category: "charity", labelKey: "commonCharity", icon: "heart" },
];

function findCategory(kind, id) {
  const list = kind === "expense" ? EXPENSE_CATEGORIES : INCOME_CATEGORIES;
  return list.find((c) => c.id === id) || null;
}
function categoryLabel(kind, id, lang) {
  const c = findCategory(kind, id);
  return c ? c[lang] || c.en : id;
}
function categoryIcon(kind, id) {
  const c = findCategory(kind, id);
  return c ? c.icon : "box";
}

/* =========================================================
   Storage: Supabase (source of truth) + localStorage (offline cache)
   ========================================================= */
const DEFAULT_SETTINGS = {
  notificationsEnabled: false, budgetAlertThreshold: 0, recurringReminderEnabled: false,
  emergencyBuffer: 0, spendingSpikeAlertEnabled: false, themeMode: "dark", planerEnabled: false,
  currency: "EUR",
  shiftCalc: {
    hourlyRate: 0,
    shiftTimes: {
      fruh: { start: "06:00", end: "14:00" },
      spat: { start: "14:00", end: "22:00" },
      tag: { start: "06:00", end: "18:00" },
      nacht: { start: "22:00", end: "06:00" },
    },
    // "3-shift" (Früh/Spät/Nacht) is the default, matching most existing
    // data; "2-shift" (Tag/Nacht) suits workplaces that don't split the
    // day into a morning/afternoon handover.
    shiftSystem: "3-shift",
    leaveHoursPerDay: 8,
    // The person's total contractual vacation (Urlaub) allowance for the
    // year — 30 is a very common standard in Germany, used purely as a
    // sensible default. Days actually taken are counted from the
    // Dienstplan itself (every day marked "urlaub" across the year), not
    // stored separately, so it can never drift out of sync with reality.
    annualVacationDays: 30,
    nightWindowStart: "22:00", nightWindowEnd: "06:00",
    nightSurchargePct: 0, weekendSurchargePct: 0, holidaySurchargePct: 0,
    taxYear: 2026, steuerklasse: "1", bundesland: "nordrhein-westfalen", age: 0, children: 0,
    churchTax: false, kvType: "public", kvZusatzPct: "",
  },
  shiftCalcOverrides: {},
  pendingPurchases: [],
  autoTranslateEnabled: false,
  receiptScanEnabled: false,
  // Opt-in only: when on, a "scan schedule" button appears in the
  // Dienstplan. A photo of a printed/handwritten shift schedule is sent
  // to Groq's vision model to read that employee's shifts for the open
  // month — the result only ever prefills a review calendar, it's never
  // saved without the person checking it first. Off by default (per-image
  // cost + leaves Supabase, same tradeoff as receipt scanning).
  dienstplanScanEnabled: false,
  // Remembered so the name field doesn't need retyping on every scan.
  dienstplanEmployeeName: "",
};

function cacheKeyFor(userId) {
  return `salary-planner:cloud-cache:v1:${userId}`;
}
function loadCachedUserData(userId) {
  try {
    const raw = localStorage.getItem(cacheKeyFor(userId));
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    return {
      entries: Array.isArray(parsed.entries) ? parsed.entries : [],
      goals: Array.isArray(parsed.goals) ? parsed.goals : [],
      settings: { ...DEFAULT_SETTINGS, ...(parsed.settings || {}) },
      displayName: parsed.displayName || "",
      shiftSchedules: parsed.shiftSchedules && typeof parsed.shiftSchedules === "object" ? parsed.shiftSchedules : {},
      credits: Array.isArray(parsed.credits) ? parsed.credits : [],
      planerChat: Array.isArray(parsed.planerChat) ? parsed.planerChat : [],
    };
  } catch { return null; }
}
function saveCachedUserData(userId, data) {
  try { localStorage.setItem(cacheKeyFor(userId), JSON.stringify(data)); } catch {}
}

async function fetchUserData(userId) {
  try {
    const { data, error } = await supabase
      .from("user_data")
      .select("entries, goals, settings, display_name, shift_schedules, credits, planer_chat")
      .eq("user_id", userId)
      .maybeSingle();
    if (error) return { ok: false, data: null };
    if (!data) return { ok: true, data: null };
    return {
      ok: true,
      data: {
        entries: Array.isArray(data.entries) ? data.entries : [],
        goals: Array.isArray(data.goals) ? data.goals : [],
        settings: { ...DEFAULT_SETTINGS, ...(data.settings || {}) },
        displayName: data.display_name || "",
        shiftSchedules: data.shift_schedules && typeof data.shift_schedules === "object" ? data.shift_schedules : {},
        credits: Array.isArray(data.credits) ? data.credits : [],
        planerChat: Array.isArray(data.planer_chat) ? data.planer_chat : [],
      },
    };
  } catch { return { ok: false, data: null }; }
}
async function upsertUserData(userId, payload) {
  try {
    const { error } = await supabase.from("user_data").upsert({
      user_id: userId,
      entries: payload.entries,
      goals: payload.goals,
      settings: payload.settings,
      display_name: payload.displayName,
      shift_schedules: payload.shiftSchedules || {},
      credits: payload.credits || [],
      planer_chat: payload.planerChat || [],
    });
    return !error;
  } catch { return false; }
}
async function fetchSubscription(userId) {
  try {
    const { data, error } = await supabase
      .from("subscriptions")
      .select("status, plan, current_period_end")
      .eq("user_id", userId)
      .maybeSingle();
    if (error || !data) return { status: "inactive", plan: null, current_period_end: null };
    return data;
  } catch { return { status: "inactive", plan: null, current_period_end: null }; }
}

async function fetchActiveAd() {
  try {
    const { data, error } = await supabase
      .from("sponsored_ads")
      .select("id, title, description, cta_label, link_url, starts_at, ends_at")
      .eq("active", true)
      .order("created_at", { ascending: false });
    if (error || !data || !data.length) return null;
    const today = todayISO();
    const valid = data.find((ad) => (
      (!ad.starts_at || ad.starts_at <= today) &&
      (!ad.ends_at || ad.ends_at >= today)
    ));
    return valid || null;
  } catch { return null; }
}
function dismissedAdsKeyFor(userId) {
  return `salary-planner:dismissed-ads:v1:${userId}`;
}
function loadDismissedAds(userId) {
  try {
    const raw = localStorage.getItem(dismissedAdsKeyFor(userId));
    const arr = raw ? JSON.parse(raw) : [];
    return Array.isArray(arr) ? arr : [];
  } catch { return []; }
}
function saveDismissedAd(userId, adId) {
  try {
    const list = loadDismissedAds(userId);
    if (!list.includes(adId)) list.push(adId);
    localStorage.setItem(dismissedAdsKeyFor(userId), JSON.stringify(list.slice(-20)));
  } catch {}
}
function isPremiumStatus(status) {
  return status === "active" || status === "trialing";
}

function collectLegacyLocalData(email) {
  try {
    const entriesRaw = localStorage.getItem(`salary-planner:entries:v1:${email}`);
    const goalsRaw = localStorage.getItem(`salary-planner:goals:v1:${email}`);
    const settingsRaw = localStorage.getItem(`salary-planner:settings:v1:${email}`);
    const usersRaw = localStorage.getItem("salary-planner:users:v1");
    if (!entriesRaw && !goalsRaw && !settingsRaw) return null;

    let displayName = "";
    try {
      const users = usersRaw ? JSON.parse(usersRaw) : [];
      const match = Array.isArray(users) ? users.find((u) => u.email === email) : null;
      if (match) displayName = match.name || "";
    } catch {}

    return {
      entries: entriesRaw ? JSON.parse(entriesRaw) : [],
      goals: goalsRaw ? JSON.parse(goalsRaw) : [],
      settings: settingsRaw ? { ...DEFAULT_SETTINGS, ...JSON.parse(settingsRaw) } : { ...DEFAULT_SETTINGS },
      displayName,
    };
  } catch { return null; }
}

function notifiedKeyFor(email) {
  return `salary-planner:notified:v1:${email}`;
}
function loadNotified(email) {
  try {
    const raw = localStorage.getItem(notifiedKeyFor(email));
    const arr = raw ? JSON.parse(raw) : [];
    return Array.isArray(arr) ? new Set(arr) : new Set();
  } catch { return new Set(); }
}
function saveNotified(email, set) {
  try {
    const arr = Array.from(set).slice(-200);
    localStorage.setItem(notifiedKeyFor(email), JSON.stringify(arr));
  } catch {}
}

function notificationHistoryKeyFor(email) {
  return `salary-planner:notification-history:v1:${email}`;
}
function loadNotificationHistory(email) {
  try {
    const raw = localStorage.getItem(notificationHistoryKeyFor(email));
    const arr = raw ? JSON.parse(raw) : [];
    return Array.isArray(arr) ? arr : [];
  } catch { return []; }
}
function saveNotificationHistory(email, list) {
  try {
    localStorage.setItem(notificationHistoryKeyFor(email), JSON.stringify(list.slice(0, 50)));
  } catch {}
}

function fireNotification(title, body) {
  try {
    if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
    if (navigator.serviceWorker && navigator.serviceWorker.ready) {
      navigator.serviceWorker.ready.then((reg) => {
        if (reg && reg.showNotification) reg.showNotification(title, { body, icon: "/icon-192.png" });
        else new Notification(title, { body, icon: "/icon-192.png" });
      }).catch(() => { try { new Notification(title, { body, icon: "/icon-192.png" }); } catch {} });
    } else {
      new Notification(title, { body, icon: "/icon-192.png" });
    }
  } catch {}
}
function makeId() {
  return Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
}

/* =========================================================
   Bank statement CSV import
   ========================================================= */
function parseCSVLine(line, delimiter) {
  const result = [];
  let cur = "";
  let inQuotes = false;
  for (let i = 0; i < line.length; i++) {
    const c = line[i];
    if (inQuotes) {
      if (c === '"') {
        if (line[i + 1] === '"') { cur += '"'; i++; }
        else inQuotes = false;
      } else cur += c;
    } else if (c === '"') {
      inQuotes = true;
    } else if (c === delimiter) {
      result.push(cur); cur = "";
    } else {
      cur += c;
    }
  }
  result.push(cur);
  return result.map((s) => s.trim());
}
function parseCSVText(text) {
  const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
  if (lines.length === 0) return [];
  const firstLine = lines[0];
  const delimiter = (firstLine.match(/;/g) || []).length > (firstLine.match(/,/g) || []).length ? ";" : ",";
  return lines.map((line) => parseCSVLine(line, delimiter));
}
function guessColumns(header) {
  const lower = header.map((h) => h.toLowerCase());
  const findIdx = (keywords) => lower.findIndex((h) => keywords.some((k) => h.includes(k)));
  return {
    dateIdx: findIdx(["date", "datum", "buchungstag", "valuta"]),
    descIdx: findIdx([
      "verwendungszweck", "description", "beschreibung", "payee", "empfänger", "empfaenger",
      "libellé", "libelle", "text", "purpose", "counterparty", "auftraggeber", "bezeichnung",
    ]),
    amountIdx: findIdx(["amount", "betrag", "montant", "wert", "value"]),
  };
}
function parseAmountFlexible(raw) {
  if (raw === undefined || raw === null) return NaN;
  let s = String(raw).trim().replace(/[€$£\s]/g, "");
  if (!s) return NaN;
  const hasComma = s.includes(",");
  const hasDot = s.includes(".");
  if (hasComma && hasDot) {
    if (s.lastIndexOf(",") > s.lastIndexOf(".")) s = s.replace(/\./g, "").replace(",", ".");
    else s = s.replace(/,/g, "");
  } else if (hasComma) {
    s = s.replace(",", ".");
  }
  return parseFloat(s);
}
function parseDateFlexible(raw) {
  if (!raw) return null;
  const s = String(raw).trim();
  let m = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
  if (m) return `${m[1]}-${m[2]}-${m[3]}`;
  m = s.match(/^(\d{1,2})[./](\d{1,2})[./](\d{4})/);
  if (m) return `${m[3]}-${String(m[2]).padStart(2, "0")}-${String(m[1]).padStart(2, "0")}`;
  return null;
}
const CATEGORY_KEYWORDS = [
  { category: "groceries", kind: "expense", words: ["REWE", "EDEKA", "LIDL", "ALDI", "KAUFLAND", "NETTO", "PENNY", "CARREFOUR", "MONOPRIX", "SPAR ", "TESCO", "SAINSBURY", "MARJANE"] },
  { category: "dining", kind: "expense", words: ["MCDONALD", "BURGER KING", "RESTAURANT", "STARBUCKS", "KFC", "SUBWAY", "LIEFERANDO", "UBER EATS", "DELIVEROO", "DOMINO"] },
  { category: "transport", kind: "expense", words: ["DB VERTRIEB", "DEUTSCHE BAHN", "BAHN.DE", "RATP", "MVV", "BVG", "UBER", "BOLT", "TAXI", "RYANAIR", "LUFTHANSA", "SNCF"] },
  { category: "fuel", kind: "expense", words: ["SHELL", "ARAL", "TOTAL", "ESSO", "TANKSTELLE", "STATION SERVICE"] },
  { category: "utilities", kind: "expense", words: ["STADTWERKE", "E.ON", "VATTENFALL", "ENGIE", "EDF", "WASSERWERK"] },
  { category: "internet", kind: "expense", words: ["VODAFONE", "TELEKOM", " O2 ", "ORANGE", "SFR", "1&1", "DEUTSCHE TELEKOM"] },
  { category: "subscriptions", kind: "expense", words: ["NETFLIX", "SPOTIFY", "AMAZON PRIME", "DISNEY", "APPLE.COM/BILL", "YOUTUBE PREMIUM", "ITUNES"] },
  { category: "health", kind: "expense", words: ["APOTHEKE", "PHARMACY", "PHARMACIE", "AOK", " TK ", "KRANKENKASSE", "ARZT"] },
  { category: "clothing", kind: "expense", words: ["ZARA", "H&M", "C&A", "PRIMARK", "UNIQLO"] },
  { category: "housing", kind: "expense", words: ["MIETE", " RENT", "HAUSVERWALTUNG", "VERMIETER"] },
  { category: "entertainment", kind: "expense", words: ["CINEMA", "KINO", "STEAM", "PLAYSTATION", "XBOX"] },
  { category: "fitness", kind: "expense", words: ["FITNESS", " GYM", "MCFIT", "CLEVER FIT", "BASIC FIT"] },
  { category: "salary", kind: "income", words: ["GEHALT", "LOHN", "SALARY", "PAYROLL", "SALAIRE"] },
];
function guessCategory(description, signedAmount) {
  const upper = ` ${(description || "").toUpperCase()} `;
  for (const entry of CATEGORY_KEYWORDS) {
    if (entry.words.some((w) => upper.includes(w))) {
      return { category: entry.category, kind: entry.kind };
    }
  }
  return signedAmount >= 0
    ? { category: "other_income", kind: "income" }
    : { category: "other", kind: "expense" };
}
function parseBankCSV(text) {
  const rows = parseCSVText(text);
  if (rows.length < 2) return [];
  const header = rows[0];
  const { dateIdx, descIdx, amountIdx } = guessColumns(header);
  if (dateIdx === -1 || amountIdx === -1) return [];
  const out = [];
  for (let i = 1; i < rows.length; i++) {
    const row = rows[i];
    const date = parseDateFlexible(row[dateIdx]);
    const amount = parseAmountFlexible(row[amountIdx]);
    if (!date || isNaN(amount) || amount === 0) continue;
    const description = descIdx !== -1 ? row[descIdx] : "";
    const guess = guessCategory(description, amount);
    out.push({
      date,
      label: description || guess.category,
      amount: Math.abs(amount),
      kind: guess.kind,
      category: guess.category,
      include: true,
    });
  }
  return out;
}

/* =========================================================
   Bank statement PDF import — free, client-side text
   extraction via pdf.js (no AI/OCR, no server calls, no cost).
   ========================================================= */
function loadPdfJs() {
  return new Promise((resolve, reject) => {
    if (window.pdfjsLib) return resolve(window.pdfjsLib);
    const script = document.createElement("script");
    script.src = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js";
    script.onload = () => {
      try {
        window.pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
        resolve(window.pdfjsLib);
      } catch (err) { reject(err); }
    };
    script.onerror = () => reject(new Error("pdfjs failed to load"));
    document.head.appendChild(script);
  });
}
async function extractPdfText(file) {
  const pdfjsLib = await loadPdfJs();
  const arrayBuffer = await file.arrayBuffer();
  const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
  let fullText = "";
  for (let i = 1; i <= pdf.numPages; i++) {
    const page = await pdf.getPage(i);
    const content = await page.getTextContent();
    let lastY = null;
    let line = "";
    for (const item of content.items) {
      const y = item.transform[5];
      if (lastY !== null && Math.abs(y - lastY) > 2) {
        fullText += line.trim() + "\n";
        line = "";
      }
      line += item.str + " ";
      lastY = y;
    }
    fullText += line.trim() + "\n";
  }
  return fullText;
}
function parseBankPDFText(fullText) {
  const lines = fullText.split(/\n/).map((l) => l.trim()).filter(Boolean);
  const dateRe = /(\d{4}-\d{2}-\d{2}|\d{1,2}[./]\d{1,2}[./]\d{4})/;
  const amountRe = /-?\d{1,3}(?:[.,]\d{3})*[.,]\d{2}/g;
  const out = [];
  for (const line of lines) {
    const dateMatch = line.match(dateRe);
    if (!dateMatch) continue;
    const amounts = line.match(amountRe);
    if (!amounts || amounts.length === 0) continue;
    const amountToken = amounts[amounts.length - 1];
    const amount = parseAmountFlexible(amountToken);
    const date = parseDateFlexible(dateMatch[1]);
    if (!date || isNaN(amount) || amount === 0) continue;
    const description = line
      .replace(dateMatch[0], "")
      .split(amountToken).join("")
      .replace(/€/g, "")
      .replace(/\s{2,}/g, " ")
      .trim();
    const guess = guessCategory(description, amount);
    out.push({
      date,
      label: description || guess.category,
      amount: Math.abs(amount),
      kind: guess.kind,
      category: guess.category,
      include: true,
    });
  }
  return out;
}

function timeToMinutes(hhmm) {
  if (!hhmm) return 0;
  const parts = String(hhmm).split(":");
  const h = parseInt(parts[0], 10) || 0;
  const m = parseInt(parts[1], 10) || 0;
  return h * 60 + m;
}
function shiftDurationHours(startStr, endStr) {
  let start = timeToMinutes(startStr);
  let end = timeToMinutes(endStr);
  if (end <= start) end += 24 * 60;
  return (end - start) / 60;
}
function nightOverlapHours(shiftStartStr, shiftEndStr, nightStartStr, nightEndStr) {
  let sStart = timeToMinutes(shiftStartStr);
  let sEnd = timeToMinutes(shiftEndStr);
  if (sEnd <= sStart) sEnd += 24 * 60;

  let nStart = timeToMinutes(nightStartStr);
  let nEnd = timeToMinutes(nightEndStr);
  if (nEnd <= nStart) nEnd += 24 * 60;

  let overlap = 0;
  [-24 * 60, 0, 24 * 60].forEach((offset) => {
    const ws = nStart + offset, we = nEnd + offset;
    const os = Math.max(sStart, ws), oe = Math.min(sEnd, we);
    if (oe > os) overlap += (oe - os);
  });
  return overlap / 60;
}

function todayISO() {
  return new Date().toISOString().slice(0, 10);
}
function monthKey(dateISO) {
  return dateISO.slice(0, 7);
}
function defaultDateForMonth(monthKeyStr) {
  const realToday = todayISO();
  if (monthKey(realToday) === monthKeyStr) return realToday;
  return `${monthKeyStr}-01`;
}
function monthLabel(key, lang) {
  const [y, m] = key.split("-").map(Number);
  const d = new Date(y, m - 1, 1);
  return d.toLocaleDateString(LOCALE_MAP[lang] || "en-US", { month: "long", year: "numeric" });
}
function shiftMonth(key, delta) {
  const [y, m] = key.split("-").map(Number);
  const d = new Date(y, m - 1 + delta, 1);
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
}
function adjustDateToMonth(originalDateISO, targetMonthKey) {
  const day = Number(originalDateISO.slice(8, 10));
  const [y, m] = targetMonthKey.split("-").map(Number);
  const daysInMonth = new Date(y, m, 0).getDate();
  const clampedDay = Math.min(day, daysInMonth);
  return `${targetMonthKey}-${String(clampedDay).padStart(2, "0")}`;
}
function expandEntriesForMonth(entries, targetMonthKey) {
  const result = [];
  for (const e of entries) {
    if (!e.recurring) {
      if (monthKey(e.date) === targetMonthKey) {
        result.push({ ...e, baseId: e.id, occurrenceMonth: targetMonthKey });
      }
      continue;
    }
    const startM = monthKey(e.date);
    if (targetMonthKey < startM) continue;
    if (e.endMonth && targetMonthKey > e.endMonth) continue;
    const exc = (e.exceptions || {})[targetMonthKey];
    if (exc && exc.deleted) continue;
    if (exc) {
      result.push({
        ...e, ...exc,
        id: `${e.id}::${targetMonthKey}`,
        date: exc.date || adjustDateToMonth(e.date, targetMonthKey),
        baseId: e.id, occurrenceMonth: targetMonthKey,
      });
    } else {
      result.push({
        ...e,
        id: `${e.id}::${targetMonthKey}`,
        date: adjustDateToMonth(e.date, targetMonthKey),
        baseId: e.id, occurrenceMonth: targetMonthKey,
      });
    }
  }
  return result;
}
function expandEntriesForRange(entries) {
  if (entries.length === 0) return [];
  const todayKey = monthKey(todayISO());
  let earliest = todayKey;
  entries.forEach((e) => {
    const mk = monthKey(e.date);
    if (mk < earliest) earliest = mk;
  });
  const months = [];
  let cursor = earliest;
  let guard = 0;
  while (cursor <= todayKey && guard < 240) {
    months.push(cursor);
    cursor = shiftMonth(cursor, 1);
    guard++;
  }
  const out = [];
  months.forEach((m) => out.push(...expandEntriesForMonth(entries, m)));
  return out;
}
function currencyFmt(n, lang) {
  return new Intl.NumberFormat(LOCALE_MAP[lang] || "en-US", { style: "currency", currency: currentCurrency || "EUR" }).format(n);
}

/* =========================================================
   Safe to Spend + Upcoming Expenses
   ========================================================= */
function daysInMonth(monthKeyStr) {
  const [y, m] = monthKeyStr.split("-").map(Number);
  return new Date(y, m, 0).getDate();
}
function computeSafeToSpend(entries, settings, goals) {
  const todayKey = todayISO();
  const currentMonthKey = monthKey(todayKey);
  const monthList = expandEntriesForMonth(entries, currentMonthKey);
  const isSavingsEntry = (e) => e.kind === "expense" && e.category === "savings";

  const incomeSoFar = monthList
    .filter((e) => e.kind === "income" && e.date <= todayKey)
    .reduce((s, e) => s + e.amount, 0);
  const expenseSoFar = monthList
    .filter((e) => e.kind === "expense" && !isSavingsEntry(e) && e.date <= todayKey)
    .reduce((s, e) => s + e.amount, 0);
  const actualSavingsSoFar = monthList
    .filter((e) => isSavingsEntry(e) && e.date <= todayKey)
    .reduce((s, e) => s + e.amount, 0);
  const currentBalance = incomeSoFar - expenseSoFar - actualSavingsSoFar;

  const upcomingExpenses = monthList
    .filter((e) => e.kind === "expense" && !isSavingsEntry(e) && e.date > todayKey)
    .reduce((s, e) => s + e.amount, 0);
  const upcomingActualSavings = monthList
    .filter((e) => isSavingsEntry(e) && e.date > todayKey)
    .reduce((s, e) => s + e.amount, 0);

  const plannedSavings = (goals || []).reduce((s, g) => s + (Number(g.monthlySaving) || 0), 0);
  const emergencyBuffer = Number(settings && settings.emergencyBuffer) || 0;

  const totalDays = daysInMonth(currentMonthKey);
  const todayDay = Number(todayKey.slice(8, 10));
  const remainingDays = Math.max(1, totalDays - todayDay + 1);

  const available = currentBalance - upcomingExpenses - upcomingActualSavings - plannedSavings;
  const safeToSpend = available / remainingDays;
  const belowBuffer = emergencyBuffer > 0 && currentBalance < emergencyBuffer;

  return {
    currentBalance, upcomingExpenses, upcomingActualSavings, actualSavingsSoFar,
    plannedSavings, emergencyBuffer, belowBuffer,
    remainingDays, available, safeToSpend,
  };
}

function computeAffordability(calc, goals, amount) {
  const afterAvailable = calc.available - amount;
  const afterSafeToSpend = afterAvailable / calc.remainingDays;
  const beforeSafeToSpend = calc.safeToSpend;

  let verdict = "good";
  if (afterAvailable < 0) verdict = "bad";
  else if (afterSafeToSpend < beforeSafeToSpend * 0.5) verdict = "ok";

  let goalMsg = null;
  if (afterAvailable < 0) {
    const shortfall = -afterAvailable;
    const candidateGoals = (goals || []).filter((g) => Number(g.monthlySaving) > 0);
    if (candidateGoals.length > 0) {
      const target = candidateGoals.reduce((a, b) => (Number(b.monthlySaving) > Number(a.monthlySaving) ? b : a));
      const delay = Math.max(1, Math.ceil(shortfall / Number(target.monthlySaving)));
      goalMsg = { name: target.name, delay };
    }
  }

  const daysEquivalent = beforeSafeToSpend > 0 ? amount / beforeSafeToSpend : null;

  return { amount, afterAvailable, afterSafeToSpend, beforeSafeToSpend, verdict, goalMsg, daysEquivalent };
}

const DISCRETIONARY_CATEGORIES = ["dining", "entertainment", "subscriptions", "clothing"];
function suggestSpendingAdjustment(monthEntries, shortfall) {
  const map = new Map();
  (monthEntries || [])
    .filter((e) => e.kind === "expense" && DISCRETIONARY_CATEGORIES.includes(e.category))
    .forEach((e) => map.set(e.category, (map.get(e.category) || 0) + e.amount));
  if (map.size === 0) return null;
  const [topCategory, topAmount] = Array.from(map.entries()).sort((a, b) => b[1] - a[1])[0];
  const target = shortfall > 0 ? Math.min(topAmount, shortfall) : topAmount * 0.3;
  if (target < 1) return null;
  return { category: topCategory, amount: target };
}

function getUpcomingExpenses(entries, daysAhead) {
  const todayKey = todayISO();
  const todayDate = new Date(todayKey);
  const endDate = new Date(todayDate.getTime() + daysAhead * DAY_MS);
  const endKey = endDate.toISOString().slice(0, 10);
  const startMonth = monthKey(todayKey);
  const endMonth = monthKey(endKey);
  const months = startMonth === endMonth ? [startMonth] : [startMonth, endMonth];
  let all = [];
  months.forEach((m) => { all = all.concat(expandEntriesForMonth(entries, m)); });
  return all
    .filter((e) => e.kind === "expense" && e.category !== "savings" && e.date > todayKey && e.date <= endKey)
    .sort((a, b) => a.date.localeCompare(b.date));
}
function daysFromToday(dateISO) {
  const todayKey = todayISO();
  const a = new Date(todayKey);
  const b = new Date(dateISO);
  return Math.round((b - a) / DAY_MS);
}

/* =========================================================
   Monthly Commitments
   ========================================================= */
function computeMonthlyCommitments(entries, currentIncome) {
  const currentMonthKey = monthKey(todayISO());
  const allRecurring = entries.filter((e) => (
    e.kind === "expense" && e.recurring &&
    monthKey(e.date) <= currentMonthKey &&
    (!e.endMonth || e.endMonth >= currentMonthKey)
  ));
  const list = allRecurring.filter((e) => e.category !== "savings");
  const recurringSavingsList = allRecurring.filter((e) => e.category === "savings");
  const totalMonthly = list.reduce((s, e) => s + e.amount, 0);
  const totalYearly = totalMonthly * 12;
  const recurringSavingsMonthly = recurringSavingsList.reduce((s, e) => s + e.amount, 0);
  const pctOfIncome = currentIncome > 0 ? (totalMonthly / currentIncome) * 100 : null;
  return { list, totalMonthly, totalYearly, recurringSavingsList, recurringSavingsMonthly, pctOfIncome };
}

/* =========================================================
   Monthly Report
   ========================================================= */
function computeMonthlyReport(entries, monthKeyStr) {
  const current = expandEntriesForMonth(entries, monthKeyStr);
  const prevKey = shiftMonth(monthKeyStr, -1);
  const previous = expandEntriesForMonth(entries, prevKey);

  const isSavingsEntry = (e) => e.kind === "expense" && e.category === "savings";

  const income = current.filter((e) => e.kind === "income").reduce((s, e) => s + e.amount, 0);
  const bonusIncome = current.filter((e) => e.kind === "income" && e.category === "bonus").reduce((s, e) => s + e.amount, 0);
  const expenses = current.filter((e) => e.kind === "expense" && !isSavingsEntry(e)).reduce((s, e) => s + e.amount, 0);
  const savingsAllocated = current.filter(isSavingsEntry).reduce((s, e) => s + e.amount, 0);
  const savings = income - expenses;
  const savingsRate = income > 0 ? (savingsAllocated / income) * 100 : 0;

  const prevExpenses = previous.filter((e) => e.kind === "expense" && !isSavingsEntry(e)).reduce((s, e) => s + e.amount, 0);

  function categoryTotals(list) {
    const map = new Map();
    list.filter((e) => e.kind === "expense" && !isSavingsEntry(e)).forEach((e) => map.set(e.category, (map.get(e.category) || 0) + e.amount));
    return map;
  }
  const curCats = categoryTotals(current);
  const prevCats = categoryTotals(previous);

  const categoryChanges = Array.from(curCats, ([categoryId, amount]) => {
    const prevAmount = prevCats.get(categoryId) || 0;
    const pctChange = prevAmount > 0 ? ((amount - prevAmount) / prevAmount) * 100 : null;
    return { categoryId, amount, prevAmount, pctChange };
  }).sort((a, b) => b.amount - a.amount);

  const topCategory = categoryChanges[0] || null;

  const biggestExpense = current
    .filter((e) => e.kind === "expense" && !isSavingsEntry(e))
    .sort((a, b) => b.amount - a.amount)[0] || null;

  const withChange = categoryChanges.filter((c) => c.pctChange !== null);
  const biggestChange = withChange.length
    ? withChange.reduce((a, b) => (Math.abs(b.pctChange) > Math.abs(a.pctChange) ? b : a))
    : null;

  return {
    income, bonusIncome, expenses, savings, savingsAllocated, savingsRate, prevExpenses,
    categoryChanges, topCategory, biggestExpense, biggestChange,
    hasPrevData: previous.length > 0,
  };
}

/* =========================================================
   Year Summary / Year Projection
   ========================================================= */
function computeYearSummary(entries, year) {
  const todayKey = todayISO();
  const currentYear = Number(todayKey.slice(0, 4));
  const currentMonthNum = Number(todayKey.slice(5, 7));

  const months = [];
  for (let m = 1; m <= 12; m++) months.push(`${year}-${String(m).padStart(2, "0")}`);

  let totalIncome = 0, totalRegularIncome = 0, totalBonusIncome = 0, totalExpenses = 0, totalSavings = 0;
  const monthly = months.map((mKey, idx) => {
    const list = expandEntriesForMonth(entries, mKey);
    const regularIncome = list.filter((e) => e.kind === "income" && e.category !== "bonus").reduce((s, e) => s + e.amount, 0);
    const bonusIncome = list.filter((e) => e.kind === "income" && e.category === "bonus").reduce((s, e) => s + e.amount, 0);
    const income = regularIncome + bonusIncome;
    const expenses = list.filter((e) => e.kind === "expense" && e.category !== "savings").reduce((s, e) => s + e.amount, 0);
    const savings = list.filter((e) => e.kind === "expense" && e.category === "savings").reduce((s, e) => s + e.amount, 0);
    totalIncome += income; totalRegularIncome += regularIncome; totalBonusIncome += bonusIncome;
    totalExpenses += expenses; totalSavings += savings;
    const hasData = list.length > 0;
    return { monthKey: mKey, monthNum: idx + 1, income, regularIncome, bonusIncome, expenses, savings, balance: income - expenses, hasData };
  });

  const netBalance = totalIncome - totalExpenses;
  const savingsRate = totalIncome > 0 ? (totalSavings / totalIncome) * 100 : 0;

  let projection = null;
  if (year === currentYear) {
    const elapsed = monthly.slice(0, currentMonthNum).filter((m) => m.hasData);
    if (elapsed.length > 0) {
      const n = elapsed.length;
      const avgRegularIncome = elapsed.reduce((s, m) => s + m.regularIncome, 0) / n;
      const avgExpenses = elapsed.reduce((s, m) => s + m.expenses, 0) / n;
      const avgSavings = elapsed.reduce((s, m) => s + m.savings, 0) / n;
      const remainingMonths = Math.max(0, 12 - currentMonthNum);

      const actualIncomeSoFar = elapsed.reduce((s, m) => s + m.income, 0);
      const actualExpensesSoFar = elapsed.reduce((s, m) => s + m.expenses, 0);
      const actualSavingsSoFar = elapsed.reduce((s, m) => s + m.savings, 0);

      const projectedTotalIncome = actualIncomeSoFar + avgRegularIncome * remainingMonths;
      const projectedTotalExpenses = actualExpensesSoFar + avgExpenses * remainingMonths;
      const projectedTotalSavings = actualSavingsSoFar + avgSavings * remainingMonths;

      projection = {
        monthsUsed: n,
        avgRegularIncome,
        projectedTotalIncome,
        projectedTotalExpenses,
        projectedSavings: projectedTotalSavings,
        projectedBalance: projectedTotalIncome - projectedTotalExpenses,
      };
    }
  }

  return {
    year, monthly, totalIncome, totalRegularIncome, totalBonusIncome, totalExpenses, totalSavings,
    netBalance, savingsRate, projection, isCurrentYear: year === currentYear, isFutureYear: year > currentYear,
  };
}

const DAY_MS = 24 * 60 * 60 * 1000;

function addDaysISO(dateISO, days) {
  return new Date(new Date(dateISO).getTime() + days * DAY_MS).toISOString().slice(0, 10);
}
function shortDateLabel(dateISO, lang) {
  const [y, m, d] = dateISO.split("-").map(Number);
  return new Date(y, m - 1, d).toLocaleDateString(LOCALE_MAP[lang] || "en-US", {
    weekday: "short", day: "numeric", month: "short",
  });
}

function formatRemaining(expiresAt, lang) {
  const diff = expiresAt - Date.now();
  if (diff <= 0) return "";
  const hours = Math.ceil(diff / (60 * 60 * 1000));
  if (hours <= 48) {
    const locale = LOCALE_MAP[lang] || "en-US";
    return new Intl.NumberFormat(locale).format(hours) + "h";
  }
  const days = Math.ceil(diff / DAY_MS);
  const locale = LOCALE_MAP[lang] || "en-US";
  return new Intl.NumberFormat(locale).format(days) + "d";
}

/* =========================================================
   Language switcher
   ========================================================= */
function LangSwitch({ className }) {
  const { lang, setLang } = useLang();
  return (
    <div className={`lang-switch ${className || ""}`}>
      <Icon name="globe" size={15} />
      <select value={lang} onChange={(e) => setLang(e.target.value)} aria-label={t("langSwitchAria", lang)}>
        {LANGS.map((l) => <option key={l} value={l}>{LANG_NAMES[l]}</option>)}
      </select>
    </div>
  );
}

/* =========================================================
   Savings ring
   ========================================================= */
function SavingsRing({ ratio, balance }) {
  const { lang } = useLang();
  const clamped = Math.max(0, Math.min(1, ratio));
  const negative = balance < 0;
  const size = 200, stroke = 14;
  const r = (size - stroke) / 2;
  const circumference = 2 * Math.PI * r;
  const offset = circumference * (1 - clamped);
  const color = negative ? "var(--red)" : "var(--gold)";

  return (
    <div className="ring-wrap" role="img" aria-label={`${t("savingsRatioLabel", lang)} ${Math.round(ratio * 100)}%`}>
      <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
        <circle cx={size/2} cy={size/2} r={r} fill="none" stroke="var(--surface-2)" strokeWidth={stroke} />
        <circle
          cx={size/2} cy={size/2} r={r} fill="none" stroke={color} strokeWidth={stroke}
          strokeLinecap="round" strokeDasharray={circumference} strokeDashoffset={offset}
          transform={`rotate(-90 ${size/2} ${size/2})`}
          style={{ transition: "stroke-dashoffset 0.6s ease" }}
        />
      </svg>
      <div className="ring-center">
        <span className="ring-pct">{Math.round(ratio * 100)}%</span>
        <span className="ring-label">{t("savingsRatioLabel", lang)}</span>
      </div>
    </div>
  );
}

/* =========================================================
   Category bars
   ========================================================= */
function CategoryBars({ rows, onSelectCategory }) {
  const { lang } = useLang();
  const [showAll, setShowAll] = useState(false);
  const max = Math.max(1, ...rows.map((r) => r.amount));
  if (rows.length === 0) {
    return <p className="empty-hint">{t("emptyCategoryHint", lang)}</p>;
  }
  const visible = showAll ? rows : rows.slice(0, 5);
  return (
    <div>
      <div className="bars">
        {visible.map((r) => (
          <button
            type="button"
            className="bar-row bar-row--clickable"
            key={r.categoryId}
            onClick={() => onSelectCategory && onSelectCategory(r.categoryId)}
          >
            <div className="bar-row-top">
              <span className="bar-icon"><Icon name={categoryIcon("expense", r.categoryId)} size={16} /></span>
              <span className="bar-name">{categoryLabel("expense", r.categoryId, lang)}</span>
              <span className="bar-amount">{currencyFmt(r.amount, lang)}</span>
            </div>
            <div className="bar-track">
              <div className="bar-fill" style={{ width: `${(r.amount / max) * 100}%` }} />
            </div>
          </button>
        ))}
      </div>
      {rows.length > 5 && (
        <button type="button" className="view-all-link" onClick={() => setShowAll((v) => !v)}>
          {showAll ? t("showLessBtn", lang) : t("viewAllCategoriesBtn", lang)}
        </button>
      )}
    </div>
  );
}

/* =========================================================
   Term (near/long) breakdown
   ========================================================= */
const TERM_META = {
  day: { icon: "clock", key: "termDay" },
  month: { icon: "calendar", key: "termMonth" },
  year: { icon: "flag", key: "termYear" },
};

// Shown when a category is tapped in the donut chart or the bar list —
// the underlying entries for that category were always being computed
// anyway (that's how the chart got its totals), this just surfaces them
// instead of leaving the chart as a dead end.
function CategoryDetailModal({ categoryId, monthEntries, month, lang, onEdit, onDelete, onClose }) {
  const rows = monthEntries.filter((e) => e.kind === "expense" && e.category === categoryId);
  const total = rows.reduce((s, e) => s + e.amount, 0);
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{categoryLabel("expense", categoryId, lang)}</h3>
          <button type="button" className="modal-close" onClick={onClose} aria-label={t("cancelBtn", lang)}><Icon name="x" size={16} /></button>
        </div>
        <div className="search-summary">
          <span>{monthLabel(month, lang)}</span>
          <span className="search-net-neg">{currencyFmt(total, lang)}</span>
        </div>
        {rows.length === 0 ? (
          <EmptyState icon="receipt" text={t("emptyTransactionsHint", lang)} />
        ) : (
          <EntryList entries={rows} onEdit={onEdit} onDelete={onDelete} />
        )}
      </div>
    </div>
  );
}

function TermBreakdown({ totals }) {
  const { lang } = useLang();
  const max = Math.max(1, totals.day, totals.month, totals.year);
  return (
    <div className="bars">
      {["day", "month", "year"].map((term) => (
        <div className="bar-row" key={term}>
          <div className="bar-row-top">
            <span className="bar-icon"><Icon name={TERM_META[term].icon} size={16} /></span>
            <span className="bar-name">{t(TERM_META[term].key, lang)}</span>
            <span className="bar-amount">{currencyFmt(totals[term], lang)}</span>
          </div>
          <div className="bar-track"><div className="bar-fill" style={{ width: `${(totals[term] / max) * 100}%` }} /></div>
        </div>
      ))}
    </div>
  );
}

/* =========================================================
   Monthly trend chart (income vs expense, last 6 months)
   ========================================================= */
function MonthlyTrendChart({ entries, month }) {
  const { lang } = useLang();
  const months = [];
  for (let i = 5; i >= 0; i--) months.push(shiftMonth(month, -i));

  const points = months.map((m) => {
    const list = expandEntriesForMonth(entries, m);
    const inc = list.filter((e) => e.kind === "income").reduce((s, e) => s + e.amount, 0);
    const exp = list.filter((e) => e.kind === "expense" && e.category !== "savings").reduce((s, e) => s + e.amount, 0);
    return { m, inc, exp };
  });

  const maxVal = Math.max(1, ...points.map((p) => Math.max(p.inc, p.exp)));
  const W = 320, H = 140, PAD = 8;
  const stepX = (W - PAD * 2) / (points.length - 1 || 1);
  const toY = (v) => H - PAD - (v / maxVal) * (H - PAD * 2);
  const toXY = (i, v) => [PAD + i * stepX, toY(v)];

  const incPath = points.map((p, i) => toXY(i, p.inc)).map(([x, y], i) => `${i === 0 ? "M" : "L"}${x},${y}`).join(" ");
  const expPath = points.map((p, i) => toXY(i, p.exp)).map(([x, y], i) => `${i === 0 ? "M" : "L"}${x},${y}`).join(" ");

  return (
    <div className="trend-chart">
      <div className="trend-legend">
        <span><span className="dot" style={{ background: "var(--green)" }}></span>{t("incomeLabel", lang)}</span>
        <span><span className="dot" style={{ background: "var(--red)" }}></span>{t("expenseLabel", lang)}</span>
      </div>
      <svg viewBox={`0 0 ${W} ${H}`} className="trend-svg" preserveAspectRatio="none">
        <path d={incPath} fill="none" stroke="var(--green)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
        <path d={expPath} fill="none" stroke="var(--red)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
        {points.map((p, i) => {
          const [x1, y1] = toXY(i, p.inc);
          const [x2, y2] = toXY(i, p.exp);
          return (
            <g key={i}>
              <circle cx={x1} cy={y1} r="3" fill="var(--green)" />
              <circle cx={x2} cy={y2} r="3" fill="var(--red)" />
            </g>
          );
        })}
      </svg>
      <div className="trend-axis">
        {points.map((p, i) => (
          <span key={i}>{new Date(p.m + "-01").toLocaleDateString(LOCALE_MAP[lang] || "en-US", { month: "short" })}</span>
        ))}
      </div>
    </div>
  );
}

/* =========================================================
   Category donut chart
   ========================================================= */
function CategoryDonutChart({ rows, onSelectCategory }) {
  const { lang } = useLang();
  const top = rows.slice(0, 6);
  const total = top.reduce((s, r) => s + r.amount, 0);
  const R = 60, CX = 70, CY = 70, STROKE = 22;
  const circumference = 2 * Math.PI * R;
  const palette = ["#B8FF00", "#4A90D9", "#4ADE80", "#E0A64E", "#6B5CA5", "#FF5C5C"];

  if (total <= 0) {
    return <p className="empty-hint">{t("emptyCategoryHint", lang)}</p>;
  }

  let offsetAcc = 0;
  const segments = top.map((r, i) => {
    const frac = r.amount / total;
    const len = frac * circumference;
    const seg = { ...r, color: palette[i % palette.length], len, offset: offsetAcc, frac };
    offsetAcc += len;
    return seg;
  });

  return (
    <div className="donut-wrap">
      <svg viewBox="0 0 140 140" className="donut-svg">
        <circle cx={CX} cy={CY} r={R} fill="none" stroke="var(--surface-2)" strokeWidth={STROKE} />
        {segments.map((s, i) => (
          <circle
            key={i} cx={CX} cy={CY} r={R} fill="none" stroke={s.color} strokeWidth={STROKE}
            strokeDasharray={`${s.len} ${circumference - s.len}`}
            strokeDashoffset={-s.offset}
            transform={`rotate(-90 ${CX} ${CY})`}
            style={{ cursor: onSelectCategory ? "pointer" : "default" }}
            onClick={() => onSelectCategory && onSelectCategory(s.categoryId)}
          />
        ))}
      </svg>
      <div className="donut-legend">
        {segments.map((s, i) => (
          <button
            type="button"
            className="donut-legend-item donut-legend-item--clickable"
            key={i}
            onClick={() => onSelectCategory && onSelectCategory(s.categoryId)}
          >
            <span className="dot" style={{ background: s.color }}></span>
            <span className="l">{categoryLabel("expense", s.categoryId, lang)}</span>
            <span className="v">{Math.round(s.frac * 100)}%</span>
          </button>
        ))}
      </div>
    </div>
  );
}

/* =========================================================
   Legal documents
   ========================================================= */
const LEGAL_DOCS = {
  privacy: {
    titleKey: "legalPrivacyTitle",
    sections: {
      ar: [
        { h: "1. المسؤول عن معالجة البيانات", p: "PARAPLANER تطبيق مبني ومُدار من طرف [الاسم القانوني الكامل للشركة، مثلاً Paraplaner LLC]، شركة مسجلة في [الولاية الأمريكية]، [العنوان الرسمي المسجل]. للتواصل بخصوص خصوصيتك: paraplaner.support@proton.me." },
        { h: "2. ما هي البيانات التي نجمعها", p: "البريد الإلكتروني وكلمة المرور (مشفّرة، لا نراها)، الاسم المعروض، الحركات المالية التي تُدخلها يدوياً (المبالغ، الفئات، التواريخ — دون أي اتصال بحسابك المصرفي إطلاقاً)، إعدادات التطبيق، وحالة الاشتراك (عبر Stripe)." },
        { h: "3. الأساس القانوني للمعالجة", p: "نعالج بياناتك لتقديم الخدمة إليك (تنفيذ العقد، المادة 6(1)(ب) من اللائحة العامة لحماية البيانات GDPR)، ولحماية الحساب والأمان (مصلحة مشروعة، المادة 6(1)(و))، وبموافقتك الصريحة بالنسبة للإشعارات الاختيارية." },
        { h: "4. مع من نشارك بياناتك", p: "Supabase (خدمة تخزين البيانات والمصادقة) وStripe (معالجة الدفع) — يعمل كلاهما كـ«معالِجَين» بموجب اتفاقيات معالجة بيانات متوافقة مع GDPR. تتولى Stripe معلومات البطاقة المصرفية مباشرة، ولا نراها أو نخزّنها إطلاقاً. لا نبيع بياناتك ولا نشاركها مع أي جهة إعلانية." },
        { h: "5. أين تُخزَّن البيانات ومدة الاحتفاظ بها", p: "تُخزَّن البيانات على البنية التحتية لـSupabase. تبقى محفوظة طالما كان حسابك نشطاً، وتُحذف نهائياً عند طلب حذف الحساب." },
        { h: "6. حقوقك", p: "لك الحق في الوصول إلى بياناتك، وتصحيحها، وحذفها، وتقييد معالجتها، ونقلها إلى خدمة أخرى، والاعتراض على معالجتها. كما يحق لك تقديم شكوى إلى سلطة حماية البيانات في بلدك (في ألمانيا: الجهة المختصة Landesdatenschutzbehörde)." },
        { h: "7. localStorage وملفات تعريف الارتباط", p: "يستخدم التطبيق localStorage الخاص بالمتصفح كذاكرة تخزين مؤقت محلية للعمل بسرعة ودون اتصال بالإنترنت. لا توجد ملفات تعريف ارتباط (كوكيز) للتتبع الإعلاني ولا أدوات تحليل من أطراف ثالثة." },
        { h: "8. الأطفال", p: "هذه الخدمة غير موجهة للأشخاص دون سن 16 عاماً." },
        { h: "9. التواصل", p: "لأي سؤال حول هذه السياسة أو بياناتك الشخصية: paraplaner.support@proton.me" },
      ],
      en: [
        { h: "1. Data Controller", p: "PARAPLANER is built and operated by [full legal company name, e.g. Paraplaner LLC], a company registered in [U.S. state], [registered business address]. For privacy inquiries: paraplaner.support@proton.me." },
        { h: "2. What Data We Collect", p: "Your email and password (encrypted, never visible to us), your display name, the financial entries you manually add (amounts, categories, dates — we never connect to your bank account), app settings, and subscription status (via Stripe)." },
        { h: "3. Legal Basis for Processing", p: "We process your data to provide the service (contract performance, Art. 6(1)(b) GDPR), to protect your account and security (legitimate interest, Art. 6(1)(f)), and with your explicit consent for optional notifications." },
        { h: "4. Who We Share Data With", p: "Supabase (database and authentication hosting) and Stripe (payment processing) — both act as processors under GDPR-compliant data processing agreements. Stripe handles your card details directly; we never see or store them. We never sell or share your data with advertisers." },
        { h: "5. Where Data Is Stored and Retention", p: "Data is stored on Supabase's infrastructure. It remains stored as long as your account is active, and is permanently deleted upon account deletion request." },
        { h: "6. Your Rights", p: "You have the right to access, correct, delete, restrict processing of, port, and object to processing of your data. You also have the right to lodge a complaint with your local data protection authority (in Germany: the competent Landesdatenschutzbehörde)." },
        { h: "7. localStorage and Cookies", p: "The app uses browser localStorage as a local cache for speed and offline use. There are no advertising tracking cookies or third-party analytics." },
        { h: "8. Children", p: "This service is not directed at individuals under 16." },
        { h: "9. Contact", p: "For any question about this policy or your personal data: paraplaner.support@proton.me" },
      ],
      fr: [
        { h: "1. Responsable du traitement", p: "PARAPLANER est développé et exploité par [nom légal complet de la société, ex. Paraplaner LLC], société enregistrée dans [État américain], [adresse professionnelle enregistrée]. Pour toute question relative à la confidentialité : paraplaner.support@proton.me." },
        { h: "2. Données que nous collectons", p: "Votre e-mail et mot de passe (chiffré, jamais visible pour nous), votre nom affiché, les opérations financières que vous saisissez manuellement (montants, catégories, dates — sans jamais nous connecter à votre compte bancaire), les paramètres de l'application, et le statut de l'abonnement (via Stripe)." },
        { h: "3. Base légale du traitement", p: "Nous traitons vos données pour fournir le service (exécution du contrat, art. 6(1)(b) RGPD), pour protéger votre compte et la sécurité (intérêt légitime, art. 6(1)(f)), et avec votre consentement explicite pour les notifications facultatives." },
        { h: "4. Avec qui nous partageons vos données", p: "Supabase (hébergement de la base de données et de l'authentification) et Stripe (traitement des paiements) — tous deux agissent comme sous-traitants dans le cadre d'accords conformes au RGPD. Stripe gère directement vos données de carte bancaire ; nous ne les voyons ni ne les stockons jamais. Nous ne vendons ni ne partageons jamais vos données avec des annonceurs." },
        { h: "5. Lieu de stockage et durée de conservation", p: "Les données sont stockées sur l'infrastructure de Supabase. Elles restent stockées tant que votre compte est actif, et sont définitivement supprimées sur demande de suppression de compte." },
        { h: "6. Vos droits", p: "Vous avez le droit d'accéder à vos données, de les rectifier, de les supprimer, d'en limiter le traitement, de les porter, et de vous opposer à leur traitement. Vous avez également le droit de déposer une plainte auprès de l'autorité de protection des données de votre pays (en Allemagne : la Landesdatenschutzbehörde compétente)." },
        { h: "7. localStorage et cookies", p: "L'application utilise le localStorage du navigateur comme cache local pour la rapidité et l'utilisation hors ligne. Il n'y a aucun cookie publicitaire de suivi ni d'analyse tierce." },
        { h: "8. Enfants", p: "Ce service ne s'adresse pas aux personnes de moins de 16 ans." },
        { h: "9. Contact", p: "Pour toute question concernant cette politique ou vos données personnelles : paraplaner.support@proton.me" },
      ],
      de: [
        { h: "1. Verantwortlicher für die Datenverarbeitung", p: "PARAPLANER wird betrieben von [vollständiger rechtlicher Firmenname, z.B. Paraplaner LLC], einem Unternehmen registriert in [US-Bundesstaat], [eingetragene Geschäftsadresse]. Für Fragen zum Datenschutz: paraplaner.support@proton.me." },
        { h: "2. Welche Daten wir erheben", p: "Ihre E-Mail-Adresse und Ihr Passwort (verschlüsselt, für uns nicht einsehbar), Ihr Anzeigename, die von Ihnen manuell eingegebenen Finanzeinträge (Beträge, Kategorien, Daten — ohne jegliche Verbindung zu Ihrem Bankkonto), App-Einstellungen sowie der Abonnementstatus (über Stripe)." },
        { h: "3. Rechtsgrundlage der Verarbeitung", p: "Wir verarbeiten Ihre Daten zur Erbringung der Dienstleistung (Vertragserfüllung, Art. 6(1)(b) DSGVO), zum Schutz Ihres Kontos und der Sicherheit (berechtigtes Interesse, Art. 6(1)(f)) sowie mit Ihrer ausdrücklichen Einwilligung für optionale Benachrichtigungen." },
        { h: "4. Mit wem wir Daten teilen", p: "Supabase (Datenbank- und Authentifizierungs-Hosting) und Stripe (Zahlungsabwicklung) — beide fungieren als Auftragsverarbeiter im Rahmen DSGVO-konformer Auftragsverarbeitungsverträge. Stripe verarbeitet Ihre Kartendaten direkt; wir sehen oder speichern diese niemals. Wir verkaufen oder teilen Ihre Daten niemals mit Werbetreibenden." },
        { h: "5. Speicherort und Aufbewahrungsdauer", p: "Die Daten werden auf der Infrastruktur von Supabase gespeichert. Sie bleiben gespeichert, solange Ihr Konto aktiv ist, und werden bei Löschungsantrag endgültig gelöscht." },
        { h: "6. Ihre Rechte", p: "Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung, Datenübertragbarkeit und Widerspruch gegen die Verarbeitung Ihrer Daten. Sie haben zudem das Recht, sich bei der zuständigen Landesdatenschutzbehörde zu beschweren." },
        { h: "7. localStorage und Cookies", p: "Die App nutzt den localStorage des Browsers als lokalen Cache für Geschwindigkeit und Offline-Nutzung. Es gibt keine Werbe-Tracking-Cookies oder Analyse-Tools von Drittanbietern." },
        { h: "8. Kinder", p: "Dieser Dienst richtet sich nicht an Personen unter 16 Jahren." },
        { h: "9. Kontakt", p: "Bei Fragen zu dieser Erklärung oder Ihren personenbezogenen Daten: paraplaner.support@proton.me" },
      ],
    },
  },
  terms: {
    titleKey: "legalTermsTitle",
    sections: {
      ar: [
        { h: "1. قبول الشروط", p: "باستخدامك تطبيق PARAPLANER، فإنك توافق على هذه الشروط كاملة. إذا كنت لا توافق، يجب ألا تستخدم التطبيق." },
        { h: "2. وصف الخدمة", p: "PARAPLANER أداة شخصية لتتبع الراتب والمصاريف. التطبيق ليس مستشاراً مالياً أو قانونياً أو ضريبياً — تبقى القرارات المالية مسؤوليتك الشخصية بالكامل." },
        { h: "3. الحساب", p: "يجب عليك تقديم معلومات صحيحة عند التسجيل، وتبقى مسؤولاً عن حماية كلمة المرور الخاصة بك وأي نشاط يحدث من حسابك." },
        { h: "4. الاشتراك والدفع", p: "يوفر التطبيق خطة مجانية وخطة Premium (3.99€ شهرياً أو 29.99€ سنوياً، والأسعار قابلة للتغيير). تتم عملية الدفع عبر Stripe. يتجدد الاشتراك تلقائياً حتى تقوم بإلغائه بنفسك من الإعدادات." },
        { h: "5. حق التراجع (Widerrufsrecht)", p: "وفقاً للقانون الأوروبي، لديك 14 يوماً للتراجع عن شراء محتوى رقمي. بمجرد تفعيل ميزات Premium والبدء في الاستفادة منها فوراً، فإنك توافق صراحةً على فقدان حق التراجع هذا وفقاً للمادة Art. 246a §1 Abs. 3 EGBGB." },
        { h: "6. ملكية البيانات", p: "تبقى البيانات المالية التي تُدخلها ملكاً كاملاً لك. يمكنك تصديرها (نسخة احتياطية) أو حذف حسابك في أي وقت." },
        { h: "7. الاستخدام الممنوع", p: "يُمنع الاستخدام غير القانوني، أو محاولة اختراق التطبيق أو هندسته العكسية، أو إساءة استخدام الخدمة بأي شكل." },
        { h: "8. حدود المسؤولية", p: "يُقدَّم التطبيق «كما هو». لا نضمن الدقة المطلقة للنصائح أو التوقعات المالية (مثل المتاح للإنفاق اليوم) — فهي أدوات مساعدة تعتمد على البيانات التي تُدخلها بنفسك." },
        { h: "9. الإنهاء", p: "يمكنك حذف حسابك في أي وقت. يجوز لنا تعليق أي حساب في حال خرق هذه الشروط." },
        { h: "10. القانون المطبَّق", p: "تخضع هذه الشروط لقانون جمهورية ألمانيا الاتحادية." },
        { h: "11. التواصل", p: "لأي سؤال حول هذه الشروط: paraplaner.support@proton.me" },
      ],
      en: [
        { h: "1. Acceptance of Terms", p: "By using PARAPLANER, you agree to these terms in full. If you do not agree, you must not use the app." },
        { h: "2. Service Description", p: "PARAPLANER is a personal salary and expense tracking tool. It is not a financial, legal, or tax advisor — financial decisions remain entirely your own responsibility." },
        { h: "3. Your Account", p: "You must provide accurate information when registering, and you are responsible for keeping your password secure and for any activity on your account." },
        { h: "4. Subscription and Payment", p: "The app offers a Free plan and a Premium plan (€3.99/month or €29.99/year, prices subject to change). Payment is processed via Stripe. Your subscription renews automatically until you cancel it yourself in Settings." },
        { h: "5. Right of Withdrawal (Widerrufsrecht)", p: "Under EU law, you have 14 days to withdraw from a digital content purchase. By activating Premium features and using them immediately, you explicitly consent to losing this withdrawal right, in accordance with Art. 246a §1(3) EGBGB." },
        { h: "6. Data Ownership", p: "The financial data you enter remains entirely your property. You may export it (backup) or delete your account at any time." },
        { h: "7. Prohibited Use", p: "You may not use the service unlawfully, attempt to hack or reverse-engineer the app, or misuse the service in any way." },
        { h: "8. Limitation of Liability", p: "The app is provided \"as is.\" We do not absolutely guarantee the accuracy of advice or financial projections (such as Safe to Spend) — these are aids based on the data you provide." },
        { h: "9. Termination", p: "You may delete your account at any time. We may suspend an account that breaches these terms." },
        { h: "10. Governing Law", p: "These terms are governed by the laws of the Federal Republic of Germany." },
        { h: "11. Contact", p: "For any question about these terms: paraplaner.support@proton.me" },
      ],
      fr: [
        { h: "1. Acceptation des conditions", p: "En utilisant PARAPLANER, vous acceptez intégralement ces conditions. Si vous n'êtes pas d'accord, vous ne devez pas utiliser l'application." },
        { h: "2. Description du service", p: "PARAPLANER est un outil personnel de suivi du salaire et des dépenses. Ce n'est pas un conseiller financier, juridique ou fiscal — les décisions financières relèvent entièrement de votre responsabilité." },
        { h: "3. Votre compte", p: "Vous devez fournir des informations exactes lors de l'inscription et êtes responsable de la sécurité de votre mot de passe et de toute activité sur votre compte." },
        { h: "4. Abonnement et paiement", p: "L'application propose un plan Gratuit et un plan Premium (3,99€/mois ou 29,99€/an, prix susceptibles de changer). Le paiement est traité via Stripe. Votre abonnement se renouvelle automatiquement jusqu'à ce que vous l'annuliez vous-même dans les paramètres." },
        { h: "5. Droit de rétractation (Widerrufsrecht)", p: "Selon le droit européen, vous disposez de 14 jours pour vous rétracter d'un achat de contenu numérique. En activant les fonctionnalités Premium et en les utilisant immédiatement, vous consentez expressément à perdre ce droit de rétractation, conformément à l'art. 246a §1(3) EGBGB." },
        { h: "6. Propriété des données", p: "Les données financières que vous saisissez restent entièrement votre propriété. Vous pouvez les exporter (sauvegarde) ou supprimer votre compte à tout moment." },
        { h: "7. Utilisation interdite", p: "Vous ne devez pas utiliser le service de manière illégale, tenter de le pirater ou de l'analyser par rétro-ingénierie, ni en abuser de quelque manière que ce soit." },
        { h: "8. Limitation de responsabilité", p: "L'application est fournie « telle quelle ». Nous ne garantissons pas de manière absolue l'exactitude des conseils ou projections financières (comme Safe to Spend) — ce sont des outils d'aide basés sur les données que vous fournissez." },
        { h: "9. Résiliation", p: "Vous pouvez supprimer votre compte à tout moment. Nous pouvons suspendre un compte en cas de violation de ces conditions." },
        { h: "10. Droit applicable", p: "Ces conditions sont régies par le droit de la République fédérale d'Allemagne." },
        { h: "11. Contact", p: "Pour toute question concernant ces conditions : paraplaner.support@proton.me" },
      ],
      de: [
        { h: "1. Annahme der Bedingungen", p: "Durch die Nutzung von PARAPLANER stimmen Sie diesen Bedingungen vollständig zu. Wenn Sie nicht zustimmen, dürfen Sie die App nicht nutzen." },
        { h: "2. Leistungsbeschreibung", p: "PARAPLANER ist ein persönliches Tool zur Verfolgung von Gehalt und Ausgaben. Es ist kein Finanz-, Rechts- oder Steuerberater — finanzielle Entscheidungen liegen vollständig in Ihrer eigenen Verantwortung." },
        { h: "3. Ihr Konto", p: "Sie müssen bei der Registrierung korrekte Angaben machen und sind für die Sicherheit Ihres Passworts sowie für jegliche Aktivität in Ihrem Konto verantwortlich." },
        { h: "4. Abonnement und Zahlung", p: "Die App bietet einen kostenlosen Plan und einen Premium-Plan (3,99€/Monat oder 29,99€/Jahr, Preise können sich ändern). Die Zahlung erfolgt über Stripe. Ihr Abonnement verlängert sich automatisch, bis Sie es selbst in den Einstellungen kündigen." },
        { h: "5. Widerrufsrecht", p: "Nach EU-Recht haben Sie 14 Tage Zeit, um von einem Kauf digitaler Inhalte zurückzutreten. Durch die Aktivierung von Premium-Funktionen und deren sofortige Nutzung stimmen Sie ausdrücklich dem Verlust dieses Widerrufsrechts gemäß Art. 246a §1(3) EGBGB zu." },
        { h: "6. Dateneigentum", p: "Die von Ihnen eingegebenen Finanzdaten bleiben vollständig Ihr Eigentum. Sie können sie jederzeit exportieren (Backup) oder Ihr Konto löschen." },
        { h: "7. Verbotene Nutzung", p: "Sie dürfen den Dienst nicht rechtswidrig nutzen, nicht versuchen, ihn zu hacken oder zurückzuentwickeln, und ihn in keiner Weise missbrauchen." },
        { h: "8. Haftungsbeschränkung", p: "Die App wird \"wie besehen\" bereitgestellt. Wir garantieren nicht absolut die Genauigkeit von Ratschlägen oder finanziellen Prognosen (wie Safe to Spend) — dies sind Hilfsmittel auf Basis der von Ihnen bereitgestellten Daten." },
        { h: "9. Kündigung", p: "Sie können Ihr Konto jederzeit löschen. Wir können ein Konto bei Verstoß gegen diese Bedingungen sperren." },
        { h: "10. Anwendbares Recht", p: "Diese Bedingungen unterliegen dem Recht der Bundesrepublik Deutschland." },
        { h: "11. Kontakt", p: "Bei Fragen zu diesen Bedingungen: paraplaner.support@proton.me" },
      ],
    },
  },
  impressum: {
    titleKey: "legalImpressumTitle",
    sections: {
      ar: [
        { h: "معلومات الشركة", p: "[الاسم القانوني الكامل للشركة — مثلاً: Paraplaner LLC / Paraplaner Inc.]\n[الولاية الأمريكية المسجلة فيها الشركة]\n[رقم التسجيل / EIN]\n[العنوان الرسمي المسجل]\nالولايات المتحدة الأمريكية" },
        { h: "التواصل", p: "البريد الإلكتروني: paraplaner.support@proton.me" },
        { h: "الممثل داخل الاتحاد الأوروبي (EU Representative)", p: "بما أن الشركة مسجلة خارج الاتحاد الأوروبي وتخدم مستخدمين أوروبيين، يتطلب القانون الأوروبي (المادة 27 من GDPR) تعيين ممثل قانوني داخل الاتحاد الأوروبي. [يُستكمل هذا الحقل بمعلومات الممثل عند تعيينه، قبل الإطلاق الرسمي للمستخدمين الأوروبيين]." },
        { h: "حل النزاعات", p: "توفر المفوضية الأوروبية منصة لحل النزاعات عبر الإنترنت (ODR): https://ec.europa.eu/consumers/odr/. بريدنا الإلكتروني مذكور أعلاه." },
      ],
      en: [
        { h: "Company Information", p: "[Full legal company name — e.g. Paraplaner LLC / Paraplaner Inc.]\n[U.S. state of incorporation]\n[Registration number / EIN]\n[Registered business address]\nUnited States of America" },
        { h: "Contact", p: "Email: paraplaner.support@proton.me" },
        { h: "EU Representative", p: "Because the company is registered outside the EU and serves EU users, EU law (Art. 27 GDPR) requires appointing a legal representative within the EU. [Fill in the representative's details here once appointed, before launching to EU users]." },
        { h: "Dispute Resolution", p: "The European Commission provides a platform for online dispute resolution (ODR): https://ec.europa.eu/consumers/odr/. Our email address is listed above." },
      ],
      fr: [
        { h: "Informations sur la société", p: "[Nom légal complet de la société — ex. Paraplaner LLC / Paraplaner Inc.]\n[État américain d'immatriculation]\n[Numéro d'enregistrement / EIN]\n[Adresse professionnelle enregistrée]\nÉtats-Unis d'Amérique" },
        { h: "Contact", p: "E-mail : paraplaner.support@proton.me" },
        { h: "Représentant dans l'UE", p: "La société étant enregistrée hors de l'UE et servant des utilisateurs européens, le droit européen (art. 27 RGPD) exige la désignation d'un représentant légal au sein de l'UE. [Complétez ce champ une fois le représentant désigné, avant le lancement auprès des utilisateurs de l'UE]." },
        { h: "Résolution des litiges", p: "La Commission européenne propose une plateforme de résolution des litiges en ligne (RLL) : https://ec.europa.eu/consumers/odr/. Notre adresse e-mail figure ci-dessus." },
      ],
      de: [
        { h: "Unternehmensangaben", p: "[Vollständiger rechtlicher Firmenname — z.B. Paraplaner LLC / Paraplaner Inc.]\n[US-Bundesstaat der Gründung]\n[Registrierungsnummer / EIN]\n[Eingetragene Geschäftsadresse]\nVereinigte Staaten von Amerika" },
        { h: "Kontakt", p: "E-Mail: paraplaner.support@proton.me" },
        { h: "EU-Vertreter", p: "Da das Unternehmen außerhalb der EU registriert ist und EU-Nutzer bedient, verlangt EU-Recht (Art. 27 DSGVO) die Benennung eines gesetzlichen Vertreters innerhalb der EU. [Diese Angaben ergänzen, sobald ein Vertreter benannt wurde — vor dem Start für EU-Nutzer]." },
        { h: "Streitschlichtung", p: "Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit: https://ec.europa.eu/consumers/odr/. Unsere E-Mail-Adresse finden Sie oben." },
      ],
    },
  },
};

function LegalModal({ docType, onClose }) {
  const { lang } = useLang();
  const doc = LEGAL_DOCS[docType];
  const sections = doc.sections[lang] || doc.sections.ar;
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card legal-modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{t(doc.titleKey, lang)}</h3>
          <button type="button" className="modal-close" onClick={onClose} aria-label={t("cancelBtn", lang)}><Icon name="x" size={16} /></button>
        </div>
        <div className="legal-body">
          {sections.map((s, i) => (
            <div className="legal-section" key={i}>
              <h4>{s.h}</h4>
              <p>{s.p}</p>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

/* =========================================================
   Dienstplan (shift schedule) — Premium feature
   ========================================================= */
function InfoHint({ text }) {
  const [show, setShow] = useState(false);
  return (
    <span className="info-hint-wrap">
      <button type="button" className="info-hint-btn" onClick={() => setShow((v) => !v)} aria-label="?">
        <Icon name="helpCircle" size={13} />
      </button>
      {show && <p className="recurring-hint info-hint-text">{text}</p>}
    </span>
  );
}

function PercentPicker({ value, onChange, presets }) {
  const list = presets || [25, 50, 70, 75, 85, 100];
  return (
    <div>
      <div className="percent-chip-row">
        {list.map((p) => (
          <button
            type="button" key={p}
            className={`percent-chip ${Number(value) === p ? "percent-chip--active" : ""}`}
            onClick={() => onChange(p)}
          >
            {p}%
          </button>
        ))}
      </div>
      <input
        type="text" inputMode="decimal" className="percent-custom-input"
        value={value || ""}
        onChange={(e) => {
          const v = parseFloat(String(e.target.value).replace(",", "."));
          onChange(isNaN(v) ? 0 : v);
        }}
        placeholder="0"
      />
    </div>
  );
}

function ShiftCalculatorModal({ calc, viewMonth, hasOverrideThisMonth, onSave, onClose }) {
  const { lang } = useLang();
  const [hourlyRate, setHourlyRate] = useState(calc.hourlyRate || "");
  const defaultTimes = {
    fruh: { start: "06:00", end: "14:00" }, spat: { start: "14:00", end: "22:00" },
    tag: { start: "06:00", end: "18:00" }, nacht: { start: "22:00", end: "06:00" },
  };
  const [shiftTimes, setShiftTimes] = useState({ ...defaultTimes, ...(calc.shiftTimes || {}) });
  const [shiftSystem, setShiftSystem] = useState(calc.shiftSystem || "3-shift");
  const [leaveHoursPerDay, setLeaveHoursPerDay] = useState(calc.leaveHoursPerDay || 8);
  const [annualVacationDays, setAnnualVacationDays] = useState(calc.annualVacationDays != null ? calc.annualVacationDays : 30);
  const [nightWindowStart, setNightWindowStart] = useState(calc.nightWindowStart || "22:00");
  const [nightWindowEnd, setNightWindowEnd] = useState(calc.nightWindowEnd || "06:00");
  const [nightPct, setNightPct] = useState(calc.nightSurchargePct || 0);
  const [weekendPct, setWeekendPct] = useState(calc.weekendSurchargePct || 0);
  const [holidayPct, setHolidayPct] = useState(calc.holidaySurchargePct || 0);
  const [taxYear, setTaxYear] = useState(calc.taxYear || 2026);
  const [steuerklasse, setSteuerklasse] = useState(calc.steuerklasse || "1");
  const [bundesland, setBundesland] = useState(calc.bundesland || "nordrhein-westfalen");
  const [age, setAge] = useState(calc.age || "");
  const [children, setChildren] = useState(calc.children || 0);
  const [churchTax, setChurchTax] = useState(!!calc.churchTax);
  const [kvType, setKvType] = useState(calc.kvType || "public");
  const [kvZusatzPct, setKvZusatzPct] = useState(calc.kvZusatzPct != null ? calc.kvZusatzPct : "");
  const [scope, setScope] = useState(hasOverrideThisMonth ? "this" : "future");

  function setShiftTime(code, field, value) {
    setShiftTimes((prev) => ({ ...prev, [code]: { ...prev[code], [field]: value } }));
  }

  function save() {
    onSave({
      hourlyRate: parseFloat(String(hourlyRate).replace(",", ".")) || 0,
      shiftTimes,
      shiftSystem,
      leaveHoursPerDay: parseFloat(String(leaveHoursPerDay).replace(",", ".")) || 8,
      annualVacationDays: parseInt(annualVacationDays, 10) || 0,
      nightWindowStart, nightWindowEnd,
      nightSurchargePct: Number(nightPct) || 0,
      weekendSurchargePct: Number(weekendPct) || 0,
      holidaySurchargePct: Number(holidayPct) || 0,
      taxYear: Number(taxYear) || 2026,
      steuerklasse, bundesland,
      age: parseInt(age, 10) || 0,
      children: parseInt(children, 10) || 0,
      churchTax, kvType,
      kvZusatzPct: kvZusatzPct === "" ? "" : Number(kvZusatzPct),
    }, scope);
    onClose();
  }

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{t("shiftCalcTitle", lang)}</h3>
          <button type="button" className="modal-close" onClick={onClose} aria-label={t("cancelBtn", lang)}><Icon name="x" size={16} /></button>
        </div>
        <p className="quick-desc">{t("shiftCalcDesc", lang)}</p>

        <label className="field field--wide">
          <span>{t("shiftCalcHourlyRate", lang)}</span>
          <input type="text" inputMode="decimal" value={hourlyRate} onChange={(e) => setHourlyRate(e.target.value)} placeholder="0.00" />
        </label>

        <div className="shift-times-section">
          <span className="dienstplan-tags-label">{t("shiftCalcShiftTimesLabel", lang)}</span>
          {["fruh", "spat", "tag", "nacht"].map((code) => {
            const shiftDef = SHIFT_TYPES.find((s) => s.code === code);
            return (
              <div className="shift-time-row" key={code}>
                <span className="shift-time-row-name" style={{ color: shiftDef.color }}>{t(shiftDef.labelKey, lang)}</span>
                <input type="time" value={shiftTimes[code].start} onChange={(e) => setShiftTime(code, "start", e.target.value)} />
                <span className="shift-time-row-sep">–</span>
                <input type="time" value={shiftTimes[code].end} onChange={(e) => setShiftTime(code, "end", e.target.value)} />
              </div>
            );
          })}
        </div>

        <label className="field field--wide">
          <span>{t("shiftCalcLeaveHours", lang)}</span>
          <input type="text" inputMode="decimal" value={leaveHoursPerDay} onChange={(e) => setLeaveHoursPerDay(e.target.value)} placeholder="8" />
        </label>

        <label className="field field--wide">
          <span>{t("shiftCalcAnnualVacation", lang)}</span>
          <input type="text" inputMode="numeric" value={annualVacationDays} onChange={(e) => setAnnualVacationDays(e.target.value)} placeholder="30" />
        </label>

        <div className="shift-calc-surcharge">
          <span className="dienstplan-tags-label">
            {t("shiftCalcNightWindowLabel", lang)}
            <InfoHint text={t("shiftCalcNightWindowHint", lang)} />
          </span>
          <div className="shift-time-row shift-time-row--window">
            <input type="time" value={nightWindowStart} onChange={(e) => setNightWindowStart(e.target.value)} />
            <span className="shift-time-row-sep">–</span>
            <input type="time" value={nightWindowEnd} onChange={(e) => setNightWindowEnd(e.target.value)} />
          </div>
          <PercentPicker value={nightPct} onChange={setNightPct} />
        </div>
        <div className="shift-calc-surcharge">
          <span className="dienstplan-tags-label">{t("shiftCalcWeekendLabel", lang)}</span>
          <PercentPicker value={weekendPct} onChange={setWeekendPct} />
        </div>
        <div className="shift-calc-surcharge">
          <span className="dienstplan-tags-label">
            {t("shiftCalcHolidayLabel", lang)}
            <InfoHint text={t("shiftCalcHolidayHint", lang)} />
          </span>
          <PercentPicker value={holidayPct} onChange={setHolidayPct} />
        </div>

        <div className="shift-calc-divider">
          <span>{t("shiftCalcNettoSection", lang)}</span>
        </div>
        <p className="quick-desc">{t("shiftCalcNettoDesc", lang)}</p>

        <div className="form-grid">
          <label className="field">
            <span>{t("shiftCalcTaxYear", lang)}</span>
            <select value={taxYear} onChange={(e) => setTaxYear(e.target.value)}>
              <option value={2026}>2026</option>
              <option value={2025}>2025</option>
            </select>
          </label>
          <label className="field">
            <span>{t("shiftCalcSteuerklasse", lang)}</span>
            <select value={steuerklasse} onChange={(e) => setSteuerklasse(e.target.value)}>
              {["1", "2", "3", "4", "5", "6"].map((k) => <option key={k} value={k}>{t("shiftCalcKlasse", lang)} {k}</option>)}
            </select>
          </label>
          <label className="field field--wide">
            <span>{t("shiftCalcBundesland", lang)}</span>
            <select value={bundesland} onChange={(e) => setBundesland(e.target.value)}>
              {GERMAN_STATES.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
            </select>
          </label>
          <label className="field">
            <span>{t("shiftCalcAge", lang)}</span>
            <input type="text" inputMode="numeric" value={age} onChange={(e) => setAge(e.target.value)} placeholder="30" />
          </label>
          <label className="field">
            <span>{t("shiftCalcChildren", lang)}</span>
            <input type="text" inputMode="numeric" value={children} onChange={(e) => setChildren(e.target.value)} placeholder="0" />
          </label>
        </div>

        <label className="recurring-check">
          <input type="checkbox" checked={churchTax} onChange={(e) => setChurchTax(e.target.checked)} />
          <span>{t("shiftCalcChurchTax", lang)}</span>
        </label>

        <div className="kind-toggle" style={{ margin: "16px 0 10px" }}>
          <button type="button" className={`kind-btn ${kvType === "public" ? "kind-btn--active" : ""}`} onClick={() => setKvType("public")}>
            {t("shiftCalcKvPublic", lang)}
          </button>
          <button type="button" className={`kind-btn ${kvType === "private" ? "kind-btn--active" : ""}`} onClick={() => setKvType("private")}>
            {t("shiftCalcKvPrivate", lang)}
          </button>
        </div>
        {kvType === "public" && (
          <label className="field field--wide">
            <span>{t("shiftCalcKvZusatz", lang)}</span>
            <input type="text" inputMode="decimal" value={kvZusatzPct} onChange={(e) => setKvZusatzPct(e.target.value)} placeholder="2.9" />
          </label>
        )}
        {kvType === "private" && (
          <span className="dienstplan-tags-label">
            {t("shiftCalcKvPrivate", lang)}
            <InfoHint text={t("shiftCalcKvPrivateHint", lang)} />
          </span>
        )}

        {viewMonth && (
          <div className="shift-calc-scope">
            <span className="dienstplan-tags-label">{t("shiftCalcScopeLabel", lang)}</span>
            <div className="scope-options scope-options--compact">
              <button type="button" className={`scope-option ${scope === "this" ? "scope-option--active" : ""}`} onClick={() => setScope("this")}>
                {t("shiftCalcScopeThis", lang)} ({monthLabel(viewMonth, lang)})
              </button>
              <button type="button" className={`scope-option ${scope === "future" ? "scope-option--active" : ""}`} onClick={() => setScope("future")}>
                {t("shiftCalcScopeFuture", lang)}
              </button>
            </div>
          </div>
        )}

        <button type="button" className="submit-btn" onClick={save}>{t("saveBtn", lang)}</button>
      </div>
    </div>
  );
}

function DienstplanModal({ month, shiftSchedules, onSave, onClose, shiftCalc, shiftCalcOverrides, onSaveShiftCalc, onAddIncome, scanEnabled, accessToken, employeeName, onSaveEmployeeName }) {
  const { lang } = useLang();
  const [viewMonth, setViewMonth] = useState(month);
  const [localSchedule, setLocalSchedule] = useState(() => shiftSchedules[month] || null);
  const [selectedDay, setSelectedDay] = useState(null);
  const [incomeAdded, setIncomeAdded] = useState(false);
  const [showCalc, setShowCalc] = useState(false);
  const [showScan, setShowScan] = useState(false);
  const pickerRef = useRef(null);

  useEffect(() => {
    if (selectedDay && pickerRef.current) {
      pickerRef.current.scrollIntoView({ behavior: "smooth", block: "center" });
    }
  }, [selectedDay]);

  useEffect(() => {
    setLocalSchedule(shiftSchedules[viewMonth] || null);
    setSelectedDay(null);
    setIncomeAdded(false);
  }, [viewMonth]);

  const total = daysInMonth(viewMonth);
  const [year, mo] = viewMonth.split("-").map(Number);
  const firstWeekdayRaw = new Date(year, mo - 1, 1).getDay();
  const leadingBlanks = (firstWeekdayRaw + 6) % 7;

  const weekdayLabels = {
    ar: ["إثن", "ثلا", "أرب", "خمي", "جمع", "سبت", "أحد"],
    en: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
    fr: ["Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"],
    de: ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"],
  }[lang] || ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];

  function startFilling() {
    const blank = {};
    for (let d = 1; d <= total; d++) blank[String(d)] = "frei";
    blank.notes = {};
    blank.tags = {};
    setLocalSchedule(blank);
    onSave(viewMonth, blank);
  }

  function setDayShift(day, code) {
    const updated = { ...(localSchedule || {}), [String(day)]: code };
    setLocalSchedule(updated);
    onSave(viewMonth, updated);
    setSelectedDay(null);
  }

  function setDayNote(day, note) {
    const updated = {
      ...(localSchedule || {}),
      notes: { ...((localSchedule && localSchedule.notes) || {}), [String(day)]: note },
    };
    setLocalSchedule(updated);
    onSave(viewMonth, updated);
  }

  function toggleDayTag(day, tagId) {
    const dayKey = String(day);
    const existingTags = (localSchedule && localSchedule.tags && localSchedule.tags[dayKey]) || [];
    const nextTags = existingTags.includes(tagId)
      ? existingTags.filter((id) => id !== tagId)
      : [...existingTags, tagId];
    const updated = {
      ...(localSchedule || {}),
      tags: { ...((localSchedule && localSchedule.tags) || {}), [dayKey]: nextTags },
    };
    setLocalSchedule(updated);
    onSave(viewMonth, updated);
  }

  const counts = SHIFT_TYPES.reduce((acc, s) => ({ ...acc, [s.code]: 0 }), {});
  if (localSchedule) {
    Object.keys(localSchedule)
      .filter((key) => key !== "notes" && key !== "tags")
      .forEach((key) => {
        const code = localSchedule[key];
        if (counts[code] !== undefined) counts[code]++;
      });
  }
  const effectiveCalc = resolveShiftCalc({ shiftCalc, shiftCalcOverrides }, viewMonth);
  const hasOverrideThisMonth = !!(shiftCalcOverrides && shiftCalcOverrides[viewMonth]);
  const estimate = computeShiftPayEstimate(localSchedule, effectiveCalc, viewMonth);
  const viewYear = viewMonth.slice(0, 4);
  const vacationDaysUsed = computeVacationDaysUsed(shiftSchedules, viewYear);
  const vacationDaysTotal = Number(shiftCalc && shiftCalc.annualVacationDays) || 0;
  const netEstimate = estimate.hasConfig && estimate.total > 0
    ? computeGermanNetSalary({ ...effectiveCalc, grossMonthly: estimate.total, year: effectiveCalc && effectiveCalc.taxYear })
    : null;
  function addEstimateAsIncome() {
    if (!onAddIncome || estimate.total <= 0) return;
    const amount = netEstimate ? netEstimate.netMonthly : estimate.total;
    onAddIncome({
      id: makeId(), kind: "income", category: "salary",
      label: monthLabel(viewMonth, lang), amount,
      date: `${viewMonth}-01`, term: null, recurring: false, endMonth: null, exceptions: {},
    });
    setIncomeAdded(true);
  }

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card dienstplan-modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{t("dienstplanTitle", lang)}</h3>
          <div style={{ display: "flex", alignItems: "center", gap: 4 }}>
            {scanEnabled && (
              <button type="button" className="modal-icon-btn" onClick={() => setShowScan(true)} aria-label={t("dienstplanScanBtn", lang)} title={t("dienstplanScanBtn", lang)}>
                <Icon name="camera" size={17} />
              </button>
            )}
            <button type="button" className="modal-icon-btn" onClick={() => setShowCalc(true)} aria-label={t("shiftCalcTitle", lang)} title={t("shiftCalcTitle", lang)}>
              <Icon name="calculator" size={17} />
            </button>
            <button type="button" className="modal-close" onClick={onClose} aria-label={t("cancelBtn", lang)}><Icon name="x" size={16} /></button>
          </div>
        </div>
        <div className="dienstplan-month-nav" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, margin: "4px 0 14px" }}>
          <button type="button" className="month-arrow" onClick={() => setViewMonth((current) => shiftMonth(current, -1))} aria-label={t("prevMonth", lang)}>‹</button>
          <div className="dienstplan-month-label" style={{ margin: 0 }}>{monthLabel(viewMonth, lang)}</div>
          <button type="button" className="month-arrow" onClick={() => setViewMonth((current) => shiftMonth(current, 1))} aria-label={t("nextMonth", lang)}>›</button>
        </div>

        {!localSchedule && (
          <div className="dienstplan-empty">
            <div className="dienstplan-empty-icon"><Icon name="calendarGrid" size={30} /></div>
            <p>{t("dienstplanEmptyHint", lang)}</p>
            <button type="button" className="submit-btn" onClick={startFilling}>
              {t("dienstplanStartBtn", lang)}
            </button>
          </div>
        )}

        {localSchedule && (
          <>
            <div className="dienstplan-legend">
              {SHIFT_TYPES.map((s) => (
                <div className="dienstplan-legend-item" key={s.code}>
                  <span className="dienstplan-legend-swatch" style={{ background: s.color }}>{s.letter}</span>
                  <span className="dienstplan-legend-count">{counts[s.code]}</span>
                  <span className="dienstplan-legend-label">{t(s.labelKey, lang)}</span>
                </div>
              ))}
            </div>

            {estimate.hasConfig ? (
              <div className="dienstplan-estimate-box">
                <div className="dienstplan-estimate-top">
                  <span>{t("shiftEstimateLabel", lang)}</span>
                  <strong>{currencyFmt(estimate.total, lang)}</strong>
                </div>
                <div className="shift-estimate-breakdown">
                  <span>{estimate.daysWorked} {t("shiftEstimateDays", lang)} · {estimate.totalHours}{t("shiftEstimateHours", lang)}</span>
                  <span>{t("shiftEstimateBase", lang)} {currencyFmt(estimate.basePay, lang)}</span>
                  {estimate.surchargePay > 0 && (
                    <span>{t("shiftEstimateSurcharge", lang)} +{currencyFmt(estimate.surchargePay, lang)}</span>
                  )}
                </div>
                {netEstimate && (
                  <div className="shift-netto-box">
                    <div className="shift-netto-top">
                      <span>{t("shiftNettoLabel", lang)}</span>
                      <strong>{currencyFmt(netEstimate.netMonthly, lang)}</strong>
                    </div>
                    <div className="shift-estimate-breakdown">
                      <span>{t("shiftNettoTaxes", lang)} -{currencyFmt(netEstimate.totalTaxes, lang)}</span>
                      <span>{t("shiftNettoSocial", lang)} -{currencyFmt(netEstimate.socialContributionsMonthly, lang)}</span>
                    </div>
                    <p className="shift-netto-disclaimer">{t("shiftNettoDisclaimer", lang)}</p>
                  </div>
                )}
                {estimate.total > 0 && onAddIncome && (
                  <button
                    type="button"
                    className="ghost-btn"
                    style={{ width: "100%", marginTop: 8 }}
                    onClick={addEstimateAsIncome}
                    disabled={incomeAdded}
                  >
                    {incomeAdded ? t("shiftEstimateAdded", lang) : t("shiftEstimateAddBtn", lang)}
                  </button>
                )}
              </div>
            ) : (
              <button type="button" className="dienstplan-estimate-prompt" onClick={() => setShowCalc(true)}>
                <Icon name="calculator" size={15} />
                {t("shiftEstimateSetup", lang)}
              </button>
            )}

            {vacationDaysTotal > 0 && (
              <div className="dienstplan-vacation-box">
                <span>{t("vacationDaysRemainingLabel", lang)} ({viewYear})</span>
                <strong>{Math.max(0, vacationDaysTotal - vacationDaysUsed)}</strong>
                <small>{vacationDaysUsed} {t("vacationDaysUsedOf", lang)} {t("vacationDaysOfLabel", lang)} {vacationDaysTotal}</small>
              </div>
            )}

            <div className="dienstplan-calendar">
              <div className="dienstplan-weekdays">
                {weekdayLabels.map((w, i) => <span key={i}>{w}</span>)}
              </div>
              <div className="dienstplan-grid">
                {Array.from({ length: leadingBlanks }).map((_, i) => <div key={`b${i}`} className="dienstplan-cell dienstplan-cell--blank" />)}
                {Array.from({ length: total }).map((_, i) => {
                  const day = i + 1;
                  const code = localSchedule[String(day)] || "frei";
                  const dayHasNote = localSchedule.notes && localSchedule.notes[String(day)];
                  const dayHasTags = localSchedule.tags && localSchedule.tags[String(day)] && localSchedule.tags[String(day)].length > 0;
                  return (
                    <button
                      type="button"
                      key={day}
                      className={`dienstplan-cell ${selectedDay === day ? "dienstplan-cell--selected" : ""}`}
                      style={{ background: shiftColor(code) }}
                      onClick={() => setSelectedDay(selectedDay === day ? null : day)}
                    >
                      <span className="dienstplan-cell-day">{day}</span>
                      <span className="dienstplan-cell-letter">{shiftLetter(code)}</span>
                      {(dayHasNote || dayHasTags) && (
                        <span className="dienstplan-cell-note-dot"><Icon name={dayHasTags ? "cup" : "file"} size={7} /></span>
                      )}
                    </button>
                  );
                })}
              </div>
            </div>

            {selectedDay && (
              <div className="dienstplan-picker" ref={pickerRef}>
                <div className="dienstplan-picker-title">
                  {t("dienstplanDayLabel", lang)} {selectedDay}
                </div>
                <div className="dienstplan-picker-row">
                  {SHIFT_TYPES.map((s) => (
                    <button type="button" key={s.code} className="dienstplan-picker-swatch" style={{ background: s.color }} onClick={() => setDayShift(selectedDay, s.code)}>
                      <span className="dienstplan-picker-letter">{s.letter}</span>
                      <span>{t(s.labelKey, lang)}</span>
                    </button>
                  ))}
                </div>

                <div className="dienstplan-tags-section">
                  <span className="dienstplan-tags-label">{t("dienstplanTagsLabel", lang)}</span>
                  <div className="dienstplan-tags-row">
                    {DIENSTPLAN_TAGS.map((tag) => {
                      const active = ((localSchedule.tags && localSchedule.tags[String(selectedDay)]) || []).includes(tag.id);
                      return (
                        <button
                          type="button"
                          key={tag.id}
                          className={`dienstplan-tag-chip ${active ? "dienstplan-tag-chip--active" : ""}`}
                          onClick={() => toggleDayTag(selectedDay, tag.id)}
                        >
                          <Icon name={tag.icon} size={13} />
                          {t(tag.labelKey, lang)}
                        </button>
                      );
                    })}
                  </div>
                </div>

                <label style={{ display: "block", marginTop: 12, textAlign: "start" }}>
                  <span style={{ display: "block", marginBottom: 6, fontSize: 12, opacity: .75 }}>{t("dienstplanNoteLabel", lang)}</span>
                  <textarea
                    value={(localSchedule.notes && localSchedule.notes[String(selectedDay)]) || ""}
                    onChange={(event) => setDayNote(selectedDay, event.target.value)}
                    placeholder={t("dienstplanNotePlaceholder", lang)}
                    rows={2}
                    style={{ width: "100%", resize: "vertical", boxSizing: "border-box" }}
                  />
                </label>
              </div>
            )}

            {!selectedDay && <p className="dienstplan-hint">{t("dienstplanTapToEdit", lang)}</p>}
          </>
        )}
      </div>
      {showCalc && (
        <ShiftCalculatorModal
          calc={effectiveCalc || {}}
          viewMonth={viewMonth}
          hasOverrideThisMonth={hasOverrideThisMonth}
          onSave={(newCalc, scope) => onSaveShiftCalc(newCalc, scope, viewMonth)}
          onClose={() => setShowCalc(false)}
        />
      )}
      {showScan && (
        <DienstplanScanModal
          accessToken={accessToken}
          month={viewMonth}
          employeeName={employeeName}
          onSaveEmployeeName={onSaveEmployeeName}
          onConfirm={(scheduleObj) => { setLocalSchedule(scheduleObj); onSave(viewMonth, scheduleObj); }}
          onClose={() => setShowScan(false)}
        />
      )}
    </div>
  );
}

function DienstplanCard({ month, shiftSchedules, onSaveMonth, isPremium, onPaywall, openOnMount = false, onCloseDienstplan, shiftCalc, shiftCalcOverrides, onSaveShiftCalc, onAddIncome, scanEnabled, accessToken, employeeName, onSaveEmployeeName }) {
  const { lang } = useLang();
  const [open, setOpen] = useState(false);
  const schedule = shiftSchedules[month];
  const filledCount = schedule ? Object.keys(schedule).filter((key) => key !== "notes" && key !== "tags").length : 0;
  const effectiveCalc = useMemo(
    () => resolveShiftCalc({ shiftCalc, shiftCalcOverrides }, month),
    [shiftCalc, shiftCalcOverrides, month]
  );
  const estimate = useMemo(() => computeShiftPayEstimate(schedule, effectiveCalc, month), [schedule, effectiveCalc, month]);

  useEffect(() => {
    if (openOnMount && isPremium) setOpen(true);
  }, [openOnMount, isPremium]);

  if (!isPremium) {
    return (
      <section className="panel safe-panel">
        <div className="safe-top">
          <div className="safe-icon"><Icon name="calendarGrid" size={20} /></div>
          <div className="safe-main">
            <div className="safe-label">{t("dienstplanTitle", lang)}</div>
            <div className="safe-amount safe-amount--locked"><Icon name="lock" size={18} /></div>
          </div>
          <span className="premium-badge"><Icon name="crown" size={12} /> {t("premiumBadge", lang)}</span>
        </div>
        <PremiumLockNotice onUpgrade={onPaywall} />
      </section>
    );
  }

  return (
    <section className="panel safe-panel">
      <button type="button" className="safe-toggle" onClick={() => setOpen(true)}>
        <div className="safe-icon"><Icon name="calendarGrid" size={20} /></div>
        <div className="safe-main">
          <div className="safe-label">{t("dienstplanTitle", lang)}</div>
          <div className="safe-amount" style={{ fontSize: 15 }}>
            {filledCount > 0 ? monthLabel(month, lang) : t("dienstplanStartBtn", lang)}
          </div>
          {estimate.hasConfig && estimate.total > 0 && (
            <div className="dienstplan-estimate-line">
              {t("shiftEstimateLabel", lang)}: <strong>{currencyFmt(estimate.total, lang)}</strong>
            </div>
          )}
        </div>
        <span className="safe-chevron">‹</span>
      </button>
      {open && (
        <DienstplanModal
          month={month}
          shiftSchedules={shiftSchedules}
          onSave={onSaveMonth}
          onClose={() => { setOpen(false); if (onCloseDienstplan) onCloseDienstplan(); }}
          shiftCalc={shiftCalc}
          shiftCalcOverrides={shiftCalcOverrides}
          onSaveShiftCalc={onSaveShiftCalc}
          onAddIncome={onAddIncome}
          scanEnabled={scanEnabled}
          accessToken={accessToken}
          employeeName={employeeName}
          onSaveEmployeeName={onSaveEmployeeName}
        />
      )}
    </section>
  );
}

function DienstplanReminderCard({ shiftSchedules, onOpen }) {
  const { lang } = useLang();
  const today = todayISO();
  const [activeDate, setActiveDate] = useState(today);
  function getDayDetails(date) {
    const schedule = shiftSchedules[monthKey(date)];
    const dayKey = String(Number(date.slice(8, 10)));
    const code = schedule && schedule[dayKey];
    const shift = SHIFT_TYPES.find((item) => item.code === code);
    const rawNote = schedule && schedule.notes && schedule.notes[dayKey];
    const translated = schedule && schedule.noteTranslations && schedule.noteTranslations[dayKey] && schedule.noteTranslations[dayKey][lang];
    const note = translated || rawNote;
    const tagIds = (schedule && schedule.tags && schedule.tags[dayKey]) || [];
    const tags = DIENSTPLAN_TAGS.filter((tag) => tagIds.includes(tag.id));
    return { date, shift, note, tags };
  }
  const current = getDayDetails(activeDate);
  const hasAnySchedule = Object.values(shiftSchedules).some((schedule) => (
    schedule && Object.keys(schedule).some((key) => key !== "notes" && key !== "tags")
  ));

  if (!hasAnySchedule) return null;
  function moveDay(direction) {
    setActiveDate((date) => addDaysISO(date, direction));
  }
  function handleSwipeStart(event) {
    event.currentTarget.dataset.swipeStartX = String(event.touches[0].clientX);
  }
  function handleSwipeEnd(event) {
    const startX = Number(event.currentTarget.dataset.swipeStartX);
    const endX = event.changedTouches[0].clientX;
    if (Math.abs(endX - startX) >= 40) moveDay(endX < startX ? 1 : -1);
  }

  return (
    <section
      className="panel dienstplan-reminder-card"
      role="button"
      tabIndex={0}
      title={t("dienstplanTitle", lang)}
      onClick={onOpen}
      onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); onOpen(); } }}
      style={{ border: "1px solid rgba(47,181,196,.35)", background: "linear-gradient(135deg, rgba(47,181,196,.12), rgba(74,144,217,.08))", cursor: "pointer", padding: "9px 11px" }}
    >
      <div className="panel-header-row" style={{ marginBottom: 5 }}>
        <h2 style={{ display: "flex", alignItems: "center", gap: 6, margin: 0, fontSize: 12.5, fontWeight: 700 }}>
          <Icon name="bell" size={14} />
          {t("dienstplanReminder", lang)}
        </h2>
      </div>
      <div
        onTouchStart={handleSwipeStart}
        onTouchEnd={handleSwipeEnd}
        style={{ display: "flex", alignItems: "center", gap: 8, touchAction: "pan-y" }}
      >
        <div style={{ flex: 1, minWidth: 0, display: "flex", alignItems: "center", gap: 8, padding: "7px 8px", borderRadius: 10, background: "rgba(0,0,0,.12)" }}>
          <span style={{ display: "grid", placeItems: "center", width: 26, height: 26, borderRadius: 8, color: "#fff", background: current.shift ? current.shift.color : "rgba(141,154,181,.4)", fontWeight: 800, fontSize: 11, flexShrink: 0 }}>
            {current.shift ? current.shift.letter : "—"}
          </span>
          <div style={{ minWidth: 0 }}>
            <div style={{ fontSize: 10.5, opacity: .72 }}>
              {current.date === today ? t("dienstplanToday", lang) : current.date === addDaysISO(today, 1) ? t("dienstplanTomorrow", lang) : shortDateLabel(current.date, lang)}
            </div>
            <div style={{ fontWeight: 700, fontSize: 12.5, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
              {current.shift ? t(current.shift.labelKey, lang) : "—"}
            </div>
            {current.tags.length > 0 && (
              <div className="dienstplan-reminder-tags">
                {current.tags.map((tag) => (
                  <span className="dienstplan-reminder-tag" key={tag.id} title={t(tag.labelKey, lang)}>
                    <Icon name={tag.icon} size={9} />
                  </span>
                ))}
              </div>
            )}
            {current.note && <div style={{ display: "flex", alignItems: "flex-start", gap: 4, marginTop: 2, fontSize: 10, whiteSpace: "normal", overflowWrap: "anywhere" }}><Icon name="file" size={10} />{current.note}</div>}
          </div>
        </div>
      </div>
      <div style={{ textAlign: "center", marginTop: 5, fontSize: 9.5, opacity: .55 }}>
        {lang === "ar" ? "حرك لليمين ولا لليسار باش تشوف أي نهار" : "Swipe left or right to view any day"}
      </div>
    </section>
  );
}

function PremiumWelcomeModal({ onClose }) {
  const { lang } = useLang();
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card premium-welcome-card" onClick={(e) => e.stopPropagation()}>
        <div className="premium-welcome-burst">
          <span className="confetti-dot"></span>
          <span className="confetti-dot"></span>
          <span className="confetti-dot"></span>
          <span className="confetti-dot"></span>
          <span className="confetti-dot"></span>
          <span className="confetti-dot"></span>
          <div className="premium-welcome-crown"><Icon name="crown" size={34} /></div>
        </div>
        <h3 className="premium-welcome-title">{t("premiumWelcomeTitle", lang)}</h3>
        <p className="premium-welcome-body">{t("premiumWelcomeBody", lang)}</p>
        <button type="button" className="submit-btn" onClick={onClose}>{t("premiumWelcomeBtn", lang)}</button>
      </div>
    </div>
  );
}

function PaywallModal({ session, onClose }) {
  const { lang } = useLang();
  const [plan, setPlan] = useState("yearly");
  const cfg = PRICING_CONFIG[plan];
  const hasLink = !!cfg.stripePaymentLink;
  const checkoutUrl = hasLink && session
    ? `${cfg.stripePaymentLink}?client_reference_id=${encodeURIComponent(session.user.id)}&prefilled_email=${encodeURIComponent(session.user.email)}`
    : null;

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{t("paywallTitle", lang)}</h3>
          <button type="button" className="modal-close" onClick={onClose} aria-label={t("cancelBtn", lang)}><Icon name="x" size={16} /></button>
        </div>
        <p className="quick-desc">{t("paywallBody", lang)}</p>

        <div className="kind-toggle" style={{ marginBottom: 16 }}>
          <button type="button" className={`kind-btn ${plan === "yearly" ? "kind-btn--active" : ""}`} onClick={() => setPlan("yearly")}>
            {t("planYearlyLabel", lang)} — {PRICING_CONFIG.yearly.amount}€
          </button>
          <button type="button" className={`kind-btn ${plan === "monthly" ? "kind-btn--active" : ""}`} onClick={() => setPlan("monthly")}>
            {t("planMonthlyLabel", lang)} — {PRICING_CONFIG.monthly.amount}€
          </button>
        </div>

        {hasLink ? (
          <a href={checkoutUrl} className="submit-btn" style={{ display: "block", textAlign: "center" }}>
            {t("upgradeToPremiumBtn", lang)}
          </a>
        ) : (
          <p className="form-error">{t("paymentComingSoon", lang)}</p>
        )}
      </div>
    </div>
  );
}

function PremiumLockNotice({ onUpgrade }) {
  const { lang } = useLang();
  return (
    <button type="button" className="premium-lock" onClick={onUpgrade}>
      <Icon name="lock" size={14} />
      <span>{t("paywallBody", lang)}</span>
    </button>
  );
}

/* =========================================================
   Safe to Spend
   ========================================================= */
function SafeToSpendCard({ entries, settings, goals, isPremium, onPaywall }) {
  const { lang } = useLang();
  const [expanded, setExpanded] = useState(false);
  const [showWhatIf, setShowWhatIf] = useState(false);
  const calc = useMemo(() => computeSafeToSpend(entries, settings, goals), [entries, settings, goals]);
  const perDay = Math.max(0, calc.safeToSpend);
  const negative = calc.available < 0;

  if (!isPremium) {
    return (
      <section className="panel safe-panel">
        <div className="safe-top">
          <div className="safe-icon"><Icon name="shield" size={20} /></div>
          <div className="safe-main">
            <div className="safe-label">{t("safeToSpendTitle", lang)}</div>
            <div className="safe-amount safe-amount--locked"><Icon name="lock" size={18} /></div>
          </div>
          <span className="premium-badge"><Icon name="crown" size={12} /> {t("premiumBadge", lang)}</span>
        </div>
        <PremiumLockNotice onUpgrade={onPaywall} />
      </section>
    );
  }

  return (
    <section className="panel safe-panel">
      <div className="safe-top">
        <div className="safe-icon"><Icon name="shield" size={20} /></div>
        <div className="safe-main">
          <div className="safe-label">{t("safeToSpendTitle", lang)}</div>
          <div className={`safe-amount ${negative ? "safe-amount--negative" : ""}`}>
            {currencyFmt(perDay, lang)} <span className="safe-per-day">/ {t("safeToSpendPerDay", lang)}</span>
          </div>
        </div>
      </div>

      {negative && <p className="form-error safe-warning">{t("safeToSpendNegative", lang)}</p>}
      {calc.belowBuffer && <p className="safe-buffer-note">{t("safeToSpendBelowBuffer", lang)}</p>}

      <button type="button" className="safe-how-link" onClick={() => setExpanded((v) => !v)}>
        {t("safeToSpendHowLink", lang)}
        <span className={`safe-chevron ${expanded ? "safe-chevron--open" : ""}`}>‹</span>
      </button>

      {expanded && (
        <div className="safe-breakdown">
          <div className="safe-row"><span>{t("safeToSpendBalance", lang)}</span><span>{currencyFmt(calc.currentBalance, lang)}</span></div>
          <div className="safe-row"><span>{t("safeToSpendUpcoming", lang)}</span><span>-{currencyFmt(calc.upcomingExpenses, lang)}</span></div>
          {calc.upcomingActualSavings > 0 && (
            <div className="safe-row safe-row--savings"><span>{t("safeToSpendActualSavings", lang)}</span><span>-{currencyFmt(calc.upcomingActualSavings, lang)}</span></div>
          )}
          {calc.plannedSavings > 0 && (
            <div className="safe-row"><span>{t("safeToSpendSavings", lang)}</span><span>-{currencyFmt(calc.plannedSavings, lang)}</span></div>
          )}
          <div className="safe-row safe-row--total"><span>{t("safeToSpendAvailable", lang)}</span><span>{currencyFmt(calc.available, lang)}</span></div>
          <div className="safe-row safe-row--muted"><span>{t("safeToSpendDays", lang)}</span><span>{calc.remainingDays}</span></div>
          {calc.emergencyBuffer > 0 && (
            <div className="safe-row safe-row--muted"><span>{t("safeToSpendBuffer", lang)}</span><span>{currencyFmt(calc.emergencyBuffer, lang)}</span></div>
          )}
          <button type="button" className="ghost-btn" style={{ marginTop: 12, width: "100%" }} onClick={() => setShowWhatIf(true)}>
            <Icon name="calculator" size={14} /> {t("whatIfTriggerBtn", lang)}
          </button>
        </div>
      )}

      {showWhatIf && <WhatIfModal calc={calc} goals={goals} onClose={() => setShowWhatIf(false)} />}
    </section>
  );
}

/* =========================================================
   Upcoming Expenses
   ========================================================= */
function UpcomingExpensesCard({ entries }) {
  const { lang } = useLang();
  const [showAll, setShowAll] = useState(false);
  const next7 = useMemo(() => getUpcomingExpenses(entries, 7), [entries]);
  const next30 = useMemo(() => getUpcomingExpenses(entries, 30), [entries]);
  const total7 = useMemo(() => next7.reduce((s, e) => s + e.amount, 0), [next7]);
  const total30 = useMemo(() => next30.reduce((s, e) => s + e.amount, 0), [next30]);
  const visible = showAll ? next30 : next30.slice(0, 4);

  return (
    <section className="panel">
      <h2>{t("upcomingExpensesTitle", lang)}</h2>
      <div className="upcoming-summary">
        <div className="upcoming-summary-item">
          <span className="l">{t("upcomingIn7Days", lang)}</span>
          <span className="v">{currencyFmt(total7, lang)}</span>
        </div>
        <div className="upcoming-summary-item">
          <span className="l">{t("upcomingIn30Days", lang)}</span>
          <span className="v">{currencyFmt(total30, lang)}</span>
        </div>
      </div>

      {next30.length === 0 ? (
        <EmptyState icon="clock" text={t("upcomingEmpty", lang)} />
      ) : (
        <>
          <ul className="upcoming-list">
            {visible.map((e) => {
              const d = daysFromToday(e.date);
              const unit = d === 1 ? t("upcomingDayUnit", lang) : t("upcomingDaysUnit", lang);
              return (
                <li className="upcoming-item" key={e.id}>
                  <div className="entry-icon" aria-hidden="true"><Icon name={categoryIcon(e.kind, e.category)} size={16} /></div>
                  <div className="upcoming-item-main">
                    <span className="upcoming-item-label">{e.label}</span>
                    <span className="upcoming-item-when">{t("upcomingInDays", lang)} {d} {unit}</span>
                  </div>
                  <span className="entry-amount">-{currencyFmt(e.amount, lang)}</span>
                </li>
              );
            })}
          </ul>
          {next30.length > 4 && (
            <button type="button" className="view-all-link" onClick={() => setShowAll((v) => !v)}>
              {showAll ? t("showLessBtn", lang) : t("viewAllUpcomingBtn", lang)}
            </button>
          )}
        </>
      )}
    </section>
  );
}

/* =========================================================
   Savings Goals
   ========================================================= */
function computeGoalProjection(goal, lang) {
  const target = Number(goal.targetAmount) || 0;
  const current = Number(goal.currentAmount) || 0;
  const monthly = Number(goal.monthlySaving) || 0;
  const remaining = Math.max(0, target - current);
  const progress = target > 0 ? Math.min(1, current / target) : 0;
  if (remaining === 0) return { progress, expectedLabel: t("goalReached", lang) };
  if (monthly <= 0) return { progress, expectedLabel: t("goalExpectedUnknown", lang) };
  const monthsNeeded = Math.ceil(remaining / monthly);
  const targetMonth = shiftMonth(monthKey(todayISO()), monthsNeeded);
  return { progress, expectedLabel: monthLabel(targetMonth, lang) };
}

function GoalCard({ goal, onEdit, onDelete }) {
  const { lang } = useLang();
  const proj = computeGoalProjection(goal, lang);
  return (
    <div className="goal-card">
      <div className="goal-card-top">
        <div className="goal-icon"><Icon name="target" size={16} /></div>
        <div className="goal-main">
          <div className="goal-name">{(goal.nameTranslations && goal.nameTranslations[lang]) || goal.name}</div>
          <div className="goal-amounts">{currencyFmt(Number(goal.currentAmount) || 0, lang)} / {currencyFmt(Number(goal.targetAmount) || 0, lang)}</div>
        </div>
        <div className="entry-actions">
          <button className="entry-action" onClick={() => onEdit(goal)} aria-label={t("editAria", lang)}><Icon name="edit" size={14} /></button>
          <button className="entry-action entry-action--danger" onClick={() => onDelete(goal)} aria-label={t("deleteAria", lang)}><Icon name="trash" size={14} /></button>
        </div>
      </div>
      <div className="bar-track"><div className="bar-fill" style={{ width: `${proj.progress * 100}%` }} /></div>
      <div className="goal-card-bottom">
        <span>{goal.monthlySaving ? `${currencyFmt(Number(goal.monthlySaving), lang)} ${t("goalPerMonth", lang)}` : "—"}</span>
        <span>{t("goalExpected", lang)}: {proj.expectedLabel}</span>
      </div>
    </div>
  );
}

function GoalFormModal({ initial, onSave, onClose }) {
  const { lang } = useLang();
  const [name, setName] = useState(initial ? initial.name : "");
  const [targetAmount, setTargetAmount] = useState(initial ? String(initial.targetAmount) : "");
  const [currentAmount, setCurrentAmount] = useState(initial ? String(initial.currentAmount) : "0");
  const [monthlySaving, setMonthlySaving] = useState(initial ? String(initial.monthlySaving || "") : "");
  const [error, setError] = useState("");

  function submit(e) {
    e.preventDefault();
    const target = parseFloat(String(targetAmount).replace(",", "."));
    if (!name.trim()) { setError(t("goalNameLabel", lang)); return; }
    if (!target || target <= 0) { setError(t("errAmount", lang)); return; }
    onSave({
      name: name.trim(),
      targetAmount: target,
      currentAmount: parseFloat(String(currentAmount).replace(",", ".")) || 0,
      monthlySaving: parseFloat(String(monthlySaving).replace(",", ".")) || 0,
    });
  }

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{t("goalsAddBtn", lang)}</h3>
          <button type="button" className="modal-close" onClick={onClose} aria-label={t("cancelBtn", lang)}><Icon name="x" size={16} /></button>
        </div>
        <form onSubmit={submit}>
          <div className="form-grid">
            <label className="field field--wide">
              <span>{t("goalNameLabel", lang)}</span>
              <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder={t("goalNamePlaceholder", lang)} />
            </label>
            <label className="field">
              <span>{t("goalTargetLabel", lang)}</span>
              <input type="text" inputMode="decimal" value={targetAmount} onChange={(e) => setTargetAmount(e.target.value)} placeholder="0.00" />
            </label>
            <label className="field">
              <span>{t("goalCurrentLabel", lang)}</span>
              <input type="text" inputMode="decimal" value={currentAmount} onChange={(e) => setCurrentAmount(e.target.value)} placeholder="0.00" />
            </label>
            <label className="field">
              <span>{t("goalMonthlyLabel", lang)}</span>
              <input type="text" inputMode="decimal" value={monthlySaving} onChange={(e) => setMonthlySaving(e.target.value)} placeholder="0.00" />
            </label>
          </div>
          {error && <p className="form-error">{error}</p>}
          <button type="submit" className="submit-btn">{t("goalSaveBtn", lang)}</button>
        </form>
      </div>
    </div>
  );
}

function GoalsSection({ goals, onAdd, onEdit, onDelete }) {
  const { lang } = useLang();
  return (
    <section className="panel">
      <div className="panel-header-row">
        <h2>{t("goalsTitle", lang)}</h2>
        <button type="button" className="quick-fab" onClick={onAdd} aria-label={t("goalsAddBtn", lang)} title={t("goalsAddBtn", lang)}>
          <Icon name="target" size={16} />
        </button>
      </div>
      {goals.length === 0 ? (
        <EmptyState icon="target" text={t("goalsEmpty", lang)} />
      ) : (
        <div className="goals-list">
          {goals.map((g) => <GoalCard key={g.id} goal={g} onEdit={onEdit} onDelete={onDelete} />)}
        </div>
      )}
    </section>
  );
}

/* =========================================================
   Shift income estimate
   ========================================================= */
function resolveShiftCalc(settings, monthKeyStr) {
  const overrides = (settings && settings.shiftCalcOverrides) || {};
  return overrides[monthKeyStr] || (settings && settings.shiftCalc) || {};
}

// Counts how many days across an entire year's Dienstplan are marked
// "urlaub" — scanning the real schedule directly rather than keeping a
// separate running total means this can never drift out of sync with
// what's actually on the calendar.
function computeVacationDaysUsed(shiftSchedules, year) {
  let used = 0;
  for (let m = 1; m <= 12; m++) {
    const mKey = year + "-" + String(m).padStart(2, "0");
    const sched = shiftSchedules && shiftSchedules[mKey];
    if (!sched) continue;
    Object.keys(sched).forEach((key) => {
      if (key !== "notes" && key !== "tags" && key !== "noteTranslations" && sched[key] === "urlaub") used++;
    });
  }
  return used;
}

function computeShiftPayEstimate(schedule, calc, monthKeyStr) {
  const hourlyRate = Number(calc && calc.hourlyRate) || 0;
  const shiftTimes = (calc && calc.shiftTimes) || {};
  const leaveHoursPerDay = Number(calc && calc.leaveHoursPerDay) || 8;
  const nightWindowStart = (calc && calc.nightWindowStart) || "22:00";
  const nightWindowEnd = (calc && calc.nightWindowEnd) || "06:00";
  const nightPct = Number(calc && calc.nightSurchargePct) || 0;
  const weekendPct = Number(calc && calc.weekendSurchargePct) || 0;
  const holidayPct = Number(calc && calc.holidaySurchargePct) || 0;
  const hasConfig = hourlyRate > 0;

  let daysWorked = 0, totalHours = 0, basePay = 0, surchargePay = 0;
  const [year, mo] = (monthKeyStr || todayISO().slice(0, 7)).split("-").map(Number);

  if (schedule && hasConfig) {
    const workedCodes = { fruh: true, spat: true, tag: true, nacht: true, urlaub: true, krank: true };
    Object.keys(schedule)
      .filter((key) => key !== "notes" && key !== "tags")
      .forEach((dayStr) => {
        const code = schedule[dayStr];
        if (!workedCodes[code]) return;
        daysWorked += 1;

        const times = shiftTimes[code];
        const hoursThisDay = (code === "urlaub" || code === "krank" || !times)
          ? leaveHoursPerDay
          : shiftDurationHours(times.start, times.end);
        totalHours += hoursThisDay;
        const dayBase = hourlyRate * hoursThisDay;
        basePay += dayBase;

        if ((code === "fruh" || code === "spat" || code === "tag" || code === "nacht") && times) {
          let daySurcharge = 0;
          if (nightPct > 0) {
            const overlapHours = nightOverlapHours(times.start, times.end, nightWindowStart, nightWindowEnd);
            daySurcharge += hourlyRate * overlapHours * (nightPct / 100);
          }
          const dayNum = Number(dayStr);
          const dow = new Date(year, mo - 1, dayNum).getDay();
          if (dow === 0 || dow === 6) daySurcharge += dayBase * (weekendPct / 100);
          const tags = (schedule.tags && schedule.tags[dayStr]) || [];
          if (tags.includes("holiday")) daySurcharge += dayBase * (holidayPct / 100);
          surchargePay += daySurcharge;
        }
      });
  }

  return {
    hasConfig,
    daysWorked,
    totalHours,
    basePay,
    surchargePay,
    total: basePay + surchargePay,
  };
}

/* =========================================================
   German Gross → Net salary calculation
   ========================================================= */
const GERMAN_TAX_PARAMS = {
  2025: {
    grundfreibetrag: 12096,
    zone2End: 17443, zone3End: 68480,
    y1: 932.30, y2: 1400,
    z1: 176.64, z2: 2397, z3: 1015.13,
    zone4Rate: 0.42, zone4Sub: 10911.92,
    zone5Start: 277826, zone5Rate: 0.45, zone5Sub: 19246.67,
    kvPvBbgMonthly: 5512.50,
    rvAlvBbgMonthly: 8050,
    kvRate: 0.146, kvZusatzDefault: 2.5,
    pvRate: 3.6, pvChildless: 0.6,
    rvRate: 18.6, alvRate: 2.6,
    soliFreigrenzeSingle: 19950, soliFreigrenzeMarried: 39900,
  },
  2026: {
    grundfreibetrag: 12348,
    zone2End: 17799, zone3End: 69878,
    y1: 914.51, y2: 1400,
    z1: 173.10, z2: 2397, z3: 1034.87,
    zone4Rate: 0.42, zone4Sub: 11135.63,
    zone5Start: 277826, zone5Rate: 0.45, zone5Sub: 19470.41,
    kvPvBbgMonthly: 5812.50,
    rvAlvBbgMonthly: 8450,
    kvRate: 0.146, kvZusatzDefault: 2.9,
    pvRate: 3.6, pvChildless: 0.6,
    rvRate: 18.6, alvRate: 2.6,
    soliFreigrenzeSingle: 20350, soliFreigrenzeMarried: 40700,
  },
};

function calculateGermanIncomeTax(zvE, p) {
  if (zvE <= p.grundfreibetrag) return 0;
  if (zvE <= p.zone2End) {
    const y = (zvE - p.grundfreibetrag) / 10000;
    return Math.floor((p.y1 * y + p.y2) * y);
  }
  if (zvE <= p.zone3End) {
    const z = (zvE - p.zone2End) / 10000;
    return Math.floor((p.z1 * z + p.z2) * z + p.z3);
  }
  if (zvE <= p.zone5Start) {
    return Math.floor(p.zone4Rate * zvE - p.zone4Sub);
  }
  return Math.floor(p.zone5Rate * zvE - p.zone5Sub);
}

function calculateSolidaritatszuschlag(lohnsteuerAnnual, isMarried, p) {
  const freigrenze = isMarried ? p.soliFreigrenzeMarried : p.soliFreigrenzeSingle;
  if (lohnsteuerAnnual <= freigrenze) return 0;
  const full = lohnsteuerAnnual * 0.055;
  const milderung = (lohnsteuerAnnual - freigrenze) * 0.119;
  return Math.max(0, Math.min(full, milderung));
}

function computeGermanNetSalary(inputs) {
  const {
    grossMonthly, year, steuerklasse, bundesland, age,
    children, churchTax, kvType, kvZusatzPct, hasPension = true,
  } = inputs;
  const p = GERMAN_TAX_PARAMS[year] || GERMAN_TAX_PARAMS[2026];
  const gross = Number(grossMonthly) || 0;
  const grossAnnual = gross * 12;

  const isMarried = steuerklasse === "3" || steuerklasse === "5";
  const isPrivateKV = kvType === "private";

  const kvPvBase = Math.min(gross, p.kvPvBbgMonthly);
  const rvAlvBase = Math.min(gross, p.rvAlvBbgMonthly);

  let kvMonthly = 0, pvMonthly = 0;
  if (!isPrivateKV) {
    const zusatz = kvZusatzPct !== "" && kvZusatzPct != null ? Number(kvZusatzPct) : p.kvZusatzDefault;
    kvMonthly = kvPvBase * ((p.kvRate * 100 + Number(zusatz)) / 100) / 2;

    const isChildless = Number(children) === 0 && Number(age) >= 23;
    const childDiscount = Math.min(1.0, Math.max(0, Number(children) - 1) * 0.25);
    let employeePvBase = bundesland === "sachsen" ? 2.3 : p.pvRate / 2;
    employeePvBase = Math.max(0, employeePvBase - childDiscount);
    const employeeExtra = isChildless ? p.pvChildless : 0;
    pvMonthly = kvPvBase * ((employeePvBase + employeeExtra) / 100);
  }

  const rvMonthly = hasPension ? rvAlvBase * (p.rvRate / 100) / 2 : 0;
  const alvMonthly = rvAlvBase * (p.alvRate / 100) / 2;
  const socialContributionsMonthly = kvMonthly + pvMonthly + rvMonthly + alvMonthly;

  let zvEAnnual = Math.max(0, grossAnnual - socialContributionsMonthly * 12);

  let lohnsteuerAnnual;
  if (steuerklasse === "3") {
    lohnsteuerAnnual = calculateGermanIncomeTax(zvEAnnual / 2, p) * 2;
  } else if (steuerklasse === "5") {
    lohnsteuerAnnual = calculateGermanIncomeTax(zvEAnnual, p) + zvEAnnual * 0.02;
  } else if (steuerklasse === "6") {
    lohnsteuerAnnual = calculateGermanIncomeTax(zvEAnnual, p) + zvEAnnual * 0.05;
  } else {
    lohnsteuerAnnual = calculateGermanIncomeTax(zvEAnnual, p);
  }
  lohnsteuerAnnual = Math.max(0, lohnsteuerAnnual);
  const lohnsteuerMonthly = lohnsteuerAnnual / 12;

  const soliAnnual = calculateSolidaritatszuschlag(lohnsteuerAnnual, isMarried, p);
  const soliMonthly = soliAnnual / 12;

  const kirchensteuerRate = ["bayern", "baden-wuerttemberg"].includes(bundesland) ? 0.08 : 0.09;
  const kirchensteuerMonthly = churchTax ? (lohnsteuerMonthly * kirchensteuerRate) : 0;

  const totalTaxes = lohnsteuerMonthly + soliMonthly + kirchensteuerMonthly;
  const totalDeductions = totalTaxes + socialContributionsMonthly;
  const netMonthly = gross - totalDeductions;

  return {
    gross,
    lohnsteuerMonthly, soliMonthly, kirchensteuerMonthly, totalTaxes,
    kvMonthly, pvMonthly, rvMonthly, alvMonthly, socialContributionsMonthly,
    totalDeductions, netMonthly,
    isEstimate: true,
  };
}

/* =========================================================
   Credits / Loans tracker
   ========================================================= */
function computeCreditProgress(credit) {
  const total = Number(credit.totalAmount) || 0;
  const monthly = Number(credit.monthlyPayment) || 0;
  const duration = Number(credit.durationMonths) || 0;
  const todayKey = monthKey(todayISO());
  let monthsElapsed = 0;
  let cursor = credit.startMonth;
  while (cursor <= todayKey && monthsElapsed < duration) {
    monthsElapsed += 1;
    cursor = shiftMonth(cursor, 1);
  }
  const paid = Math.min(total, monthsElapsed * monthly);
  const remaining = Math.max(0, total - paid);
  const progress = total > 0 ? Math.min(1, paid / total) : 0;
  const payoffMonth = shiftMonth(credit.startMonth, Math.max(0, duration - 1));
  return { paid, remaining, progress, payoffMonth, monthsElapsed, monthsLeft: Math.max(0, duration - monthsElapsed) };
}

function CreditFormModal({ initial, defaultMonth, onSave, onClose }) {
  const { lang } = useLang();
  const [name, setName] = useState(initial ? initial.name : "");
  const [totalAmount, setTotalAmount] = useState(initial ? String(initial.totalAmount) : "");
  const [mode, setMode] = useState(initial && initial.durationMonths ? "duration" : "monthly");
  const [durationMonths, setDurationMonths] = useState(initial ? String(initial.durationMonths || "") : "");
  const [monthlyPayment, setMonthlyPayment] = useState(initial ? String(initial.monthlyPayment || "") : "");
  const [error, setError] = useState("");

  const total = parseFloat(String(totalAmount).replace(",", ".")) || 0;
  const durationNum = parseFloat(String(durationMonths).replace(",", ".")) || 0;
  const monthlyNum = parseFloat(String(monthlyPayment).replace(",", ".")) || 0;

  let computedMonthly = null, computedDuration = null;
  if (mode === "duration" && total > 0 && durationNum > 0) {
    computedMonthly = total / durationNum;
  } else if (mode === "monthly" && total > 0 && monthlyNum > 0) {
    computedDuration = Math.ceil(total / monthlyNum);
  }

  function submit(e) {
    e.preventDefault();
    if (!name.trim()) { setError(t("creditNameLabel", lang)); return; }
    if (!total || total <= 0) { setError(t("errAmount", lang)); return; }
    const finalMonthly = mode === "duration" ? computedMonthly : monthlyNum;
    const finalDuration = mode === "monthly" ? computedDuration : Math.round(durationNum);
    if (!finalMonthly || finalMonthly <= 0 || !finalDuration || finalDuration <= 0) {
      setError(t("creditErrNeedOne", lang));
      return;
    }
    onSave({
      name: name.trim(),
      totalAmount: total,
      monthlyPayment: finalMonthly,
      durationMonths: finalDuration,
      startMonth: initial ? initial.startMonth : defaultMonth,
    });
  }

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{initial ? t("creditEditTitle", lang) : t("creditAddTitle", lang)}</h3>
          <button type="button" className="modal-close" onClick={onClose} aria-label={t("cancelBtn", lang)}><Icon name="x" size={16} /></button>
        </div>
        <p className="quick-desc">{t("creditDesc", lang)}</p>
        <form onSubmit={submit}>
          <div className="form-grid">
            <label className="field field--wide">
              <span>{t("creditNameLabel", lang)}</span>
              <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder={t("creditNamePlaceholder", lang)} />
            </label>
            <label className="field field--wide">
              <span>{t("creditTotalLabel", lang)}</span>
              <input type="text" inputMode="decimal" value={totalAmount} onChange={(e) => setTotalAmount(e.target.value)} placeholder="0.00" />
            </label>
          </div>

          <div className="kind-toggle" style={{ margin: "14px 0 10px" }}>
            <button type="button" className={`kind-btn ${mode === "duration" ? "kind-btn--active" : ""}`} onClick={() => setMode("duration")}>
              {t("creditModeDuration", lang)}
            </button>
            <button type="button" className={`kind-btn ${mode === "monthly" ? "kind-btn--active" : ""}`} onClick={() => setMode("monthly")}>
              {t("creditModeMonthly", lang)}
            </button>
          </div>

          {mode === "duration" ? (
            <div className="form-grid">
              <label className="field field--wide">
                <span>{t("creditDurationLabel", lang)}</span>
                <input type="text" inputMode="decimal" value={durationMonths} onChange={(e) => setDurationMonths(e.target.value)} placeholder={t("creditDurationPlaceholder", lang)} />
              </label>
            </div>
          ) : (
            <div className="form-grid">
              <label className="field field--wide">
                <span>{t("creditMonthlyLabel", lang)}</span>
                <input type="text" inputMode="decimal" value={monthlyPayment} onChange={(e) => setMonthlyPayment(e.target.value)} placeholder="0.00" />
              </label>
            </div>
          )}

          {(computedMonthly !== null || computedDuration !== null) && (
            <p className="credit-live-preview">
              {computedMonthly !== null && <>{t("creditPreviewMonthly", lang)}: <strong>{currencyFmt(computedMonthly, lang)}</strong> / {t("perMonthUnit", lang)}</>}
              {computedDuration !== null && <>{t("creditPreviewDuration", lang)}: <strong>{computedDuration} {t("monthsUnit", lang)}</strong></>}
            </p>
          )}

          {error && <p className="form-error">{error}</p>}
          <button type="submit" className="submit-btn">{t("creditSaveBtn", lang)}</button>
        </form>
      </div>
    </div>
  );
}

function CreditCard({ credit, onEdit, onDelete }) {
  const { lang } = useLang();
  const proj = computeCreditProgress(credit);
  return (
    <div className="goal-card">
      <div className="goal-card-top">
        <div className="goal-icon"><Icon name="card" size={16} /></div>
        <div className="goal-main">
          <div className="goal-name">{(credit.nameTranslations && credit.nameTranslations[lang]) || credit.name}</div>
          <div className="goal-amounts">{currencyFmt(proj.remaining, lang)} {t("creditRemainingLabel", lang)}</div>
        </div>
        <div className="entry-actions">
          <button className="entry-action" onClick={() => onEdit(credit)} aria-label={t("editAria", lang)}><Icon name="edit" size={14} /></button>
          <button className="entry-action entry-action--danger" onClick={() => onDelete(credit)} aria-label={t("deleteAria", lang)}><Icon name="trash" size={14} /></button>
        </div>
      </div>
      <div className="bar-track"><div className="bar-fill" style={{ width: `${proj.progress * 100}%` }} /></div>
      <div className="goal-card-bottom">
        <span>{currencyFmt(credit.monthlyPayment, lang)} {t("goalPerMonth", lang)}</span>
        <span>{proj.monthsLeft > 0 ? `${proj.monthsLeft} ${t("creditMonthsLeft", lang)}` : t("goalReached", lang)}</span>
      </div>
    </div>
  );
}

function CreditsSection({ credits, isPremium, onPaywall, onAdd, onEdit, onDelete }) {
  const { lang } = useLang();
  return (
    <section className="panel">
      <div className="panel-header-row">
        <h2>{t("creditsTitle", lang)}</h2>
        <button type="button" className="quick-fab" onClick={isPremium ? onAdd : onPaywall} aria-label={t("creditAddTitle", lang)} title={t("creditAddTitle", lang)}>
          <Icon name="card" size={16} />
        </button>
      </div>
      {!isPremium ? (
        <PremiumLockNotice onUpgrade={onPaywall} />
      ) : credits.length === 0 ? (
        <EmptyState icon="card" text={t("creditsEmpty", lang)} />
      ) : (
        <div className="goals-list">
          {credits.map((c) => <CreditCard key={c.id} credit={c} onEdit={onEdit} onDelete={onDelete} />)}
        </div>
      )}
    </section>
  );
}


function WhatIfModal({ calc, goals, onClose }) {
  const { lang } = useLang();
  const [amount, setAmount] = useState("");
  const [result, setResult] = useState(null);

  function calculate(e) {
    e.preventDefault();
    const value = parseFloat(String(amount).replace(",", "."));
    if (!value || value <= 0) return;
    setResult(computeAffordability(calc, goals, value));
  }

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{t("whatIfTitle", lang)}</h3>
          <button type="button" className="modal-close" onClick={onClose} aria-label={t("cancelBtn", lang)}><Icon name="x" size={16} /></button>
        </div>
        <p className="quick-desc">{t("whatIfDesc", lang)}</p>
        <form onSubmit={calculate}>
          <label className="field field--wide">
            <span>{t("whatIfAmountLabel", lang)}</span>
            <input type="text" inputMode="decimal" value={amount} onChange={(e) => setAmount(e.target.value)} placeholder="0.00" autoFocus />
          </label>
          <button type="submit" className="submit-btn">{t("whatIfCalcBtn", lang)}</button>
        </form>

        {result && (
          <div className="whatif-result">
            <div className="whatif-compare">
              <div className="whatif-col">
                <span className="l">{t("whatIfBefore", lang)}</span>
                <span className="v">{currencyFmt(result.beforeSafeToSpend, lang)}</span>
              </div>
              <div className="whatif-arrow">→</div>
              <div className="whatif-col">
                <span className="l">{t("whatIfAfter", lang)}</span>
                <span className={`v ${result.afterSafeToSpend < 0 ? "whatif-neg" : ""}`}>{currencyFmt(result.afterSafeToSpend, lang)}</span>
              </div>
            </div>
            <div className={`whatif-verdict whatif-verdict--${result.verdict}`}>
              {result.verdict === "good" && t("verdictGood", lang)}
              {result.verdict === "ok" && t("verdictOk", lang)}
              {result.verdict === "bad" && t("verdictBad", lang)}
            </div>
            {result.goalMsg && (
              <p className="whatif-goal-msg">
                {t("goalDelayMsg", lang)} <strong>{result.goalMsg.name}</strong> {t("goalDelayMonths", lang)} {result.goalMsg.delay} {t("monthsUnit", lang)}.
              </p>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

/* =========================================================
   "Can I afford this?" + Impulse Timer
   ========================================================= */
function AffordabilityLauncherCard({ onOpen }) {
  const { lang } = useLang();
  return (
    <button type="button" className="afford-launcher" onClick={onOpen}>
      <span className="afford-launcher-icon"><Icon name="shield" size={20} /></span>
      <span className="afford-launcher-text">
        <strong>{t("affordLauncherTitle", lang)}</strong>
        <small>{t("affordLauncherSub", lang)}</small>
      </span>
      <Icon name="chevronRight" size={16} />
    </button>
  );
}

function AffordabilityModal({ entries, monthEntries, settings, goals, onAddExpense, onSaveForLater, onClose }) {
  const { lang } = useLang();
  const [name, setName] = useState("");
  const [price, setPrice] = useState("");
  const [category, setCategory] = useState(EXPENSE_CATEGORIES[0].id);
  const [urgency, setUrgency] = useState("want");
  const [result, setResult] = useState(null);
  const [savedForLater, setSavedForLater] = useState(false);

  const calc = useMemo(() => computeSafeToSpend(entries, settings, goals), [entries, settings, goals]);

  function calculate(e) {
    e.preventDefault();
    const value = parseFloat(String(price).replace(",", "."));
    if (!value || value <= 0) return;
    const affordability = computeAffordability(calc, goals, value);
    const suggestion = affordability.verdict !== "good"
      ? suggestSpendingAdjustment(monthEntries, -affordability.afterAvailable)
      : null;
    setResult({ ...affordability, suggestion });
    setSavedForLater(false);
  }

  function buyNow() {
    const value = parseFloat(String(price).replace(",", "."));
    if (!value || value <= 0) return;
    onAddExpense({
      id: makeId(), kind: "expense", category,
      label: name.trim() || categoryLabel("expense", category, lang),
      amount: value, date: todayISO(), term: "day",
      recurring: false, endMonth: null, exceptions: {},
    });
    onClose();
  }

  function decideLater() {
    const value = parseFloat(String(price).replace(",", "."));
    if (!value || value <= 0) return;
    onSaveForLater({
      id: makeId(),
      name: name.trim() || categoryLabel("expense", category, lang),
      price: value, category, urgency,
      createdAt: Date.now(),
      reviewAt: Date.now() + 24 * 60 * 60 * 1000,
      status: "pending",
    });
    setSavedForLater(true);
  }

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{t("affordTitle", lang)}</h3>
          <button type="button" className="modal-close" onClick={onClose} aria-label={t("cancelBtn", lang)}><Icon name="x" size={16} /></button>
        </div>
        <p className="quick-desc">{t("affordDesc", lang)}</p>

        <form onSubmit={calculate}>
          <div className="form-grid">
            <label className="field field--wide">
              <span>{t("affordItemLabel", lang)}</span>
              <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder={t("affordItemPlaceholder", lang)} />
            </label>
            <label className="field">
              <span>{t("amountLabel", lang)}</span>
              <input type="text" inputMode="decimal" value={price} onChange={(e) => setPrice(e.target.value)} placeholder="0.00" autoFocus />
            </label>
            <label className="field">
              <span>{t("categoryLabel", lang)}</span>
              <select value={category} onChange={(e) => setCategory(e.target.value)}>
                {EXPENSE_CATEGORIES.filter((c) => c.id !== "quick").map((c) => <option key={c.id} value={c.id}>{c[lang] || c.en}</option>)}
              </select>
            </label>
          </div>

          <div className="afford-urgency-row">
            {["need", "useful", "want"].map((u) => (
              <button
                type="button" key={u}
                className={`afford-urgency-btn ${urgency === u ? "afford-urgency-btn--active" : ""}`}
                onClick={() => setUrgency(u)}
              >
                {t(`affordUrgency_${u}`, lang)}
              </button>
            ))}
          </div>

          <button type="submit" className="submit-btn">{t("affordCalcBtn", lang)}</button>
        </form>

        {result && (
          <div className="afford-result">
            <div className={`afford-verdict afford-verdict--${result.verdict}`}>
              {result.verdict === "good" && t("affordGood", lang)}
              {result.verdict === "ok" && t("affordOk", lang)}
              {result.verdict === "bad" && t("affordBad", lang)}
            </div>

            {result.daysEquivalent != null && (
              <div className="afford-metric">
                <span>{t("affordDaysEquivalent", lang)}</span>
                <strong>{result.daysEquivalent.toFixed(1)} {t("affordDaysUnit", lang)}</strong>
              </div>
            )}
            <div className="afford-metric">
              <span>{t("affordPerDayAfter", lang)}</span>
              <strong className={result.afterSafeToSpend < 0 ? "whatif-neg" : ""}>{currencyFmt(result.afterSafeToSpend, lang)}</strong>
            </div>

            {result.goalMsg && (
              <p className="whatif-goal-msg">
                {t("goalDelayMsg", lang)} <strong>{result.goalMsg.name}</strong> {t("goalDelayMonths", lang)} {result.goalMsg.delay} {t("monthsUnit", lang)}.
              </p>
            )}
            {result.suggestion && (
              <p className="afford-suggestion">
                {t("affordSuggestionPrefix", lang)} <strong>{categoryLabel("expense", result.suggestion.category, lang)}</strong> {t("affordSuggestionBy", lang)} {currencyFmt(result.suggestion.amount, lang)} {t("affordSuggestionSuffix", lang)}
              </p>
            )}

            {savedForLater ? (
              <p className="afford-saved-note"><Icon name="clock" size={13} /> {t("affordSavedForLaterNote", lang)}</p>
            ) : (
              <div className="afford-actions">
                <button type="button" className="submit-btn" onClick={buyNow}>{t("affordBuyNowBtn", lang)}</button>
                <button type="button" className="ghost-btn" style={{ width: "100%", marginTop: 8 }} onClick={decideLater}>
                  <Icon name="clock" size={14} /> {t("affordDecideLaterBtn", lang)}
                </button>
              </div>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

function PendingPurchasesCard({ pendingPurchases, onBought, onSkipped, onKeepWaiting }) {
  const { lang } = useLang();
  const active = (pendingPurchases || []).filter((p) => p.status === "pending");
  const savedTotal = useMemo(
    () => (pendingPurchases || []).filter((p) => p.status === "skipped").reduce((s, p) => s + p.price, 0),
    [pendingPurchases]
  );

  if (active.length === 0) return null;

  return (
    <section className="panel">
      <div className="panel-header-row">
        <h2>{t("pendingPurchasesTitle", lang)}</h2>
      </div>
      {savedTotal > 0 && (
        <p className="pending-saved-note">{t("pendingSavedSoFar", lang)} <strong>{currencyFmt(savedTotal, lang)}</strong></p>
      )}
      <ul className="pending-purchase-list">
        {active.map((item) => {
          const isDue = Date.now() >= item.reviewAt;
          return (
            <li className="pending-purchase-item" key={item.id}>
              <div className="pending-purchase-top">
                <div className="entry-icon" aria-hidden="true"><Icon name={categoryIcon("expense", item.category)} size={16} /></div>
                <div className="pending-purchase-main">
                  <span className="pending-purchase-name">{item.name}</span>
                  <span className="pending-purchase-price">{currencyFmt(item.price, lang)}</span>
                </div>
              </div>
              {!isDue ? (
                <span className="pending-purchase-countdown">
                  <Icon name="clock" size={12} /> {t("pendingReviewIn", lang)} {formatRemaining(item.reviewAt, lang)}
                </span>
              ) : (
                <div className="pending-purchase-due">
                  <span className="pending-purchase-question">{t("pendingStillWant", lang)}</span>
                  <div className="pending-purchase-actions">
                    <button type="button" className="ghost-btn" onClick={() => onBought(item)}>{t("pendingBoughtBtn", lang)}</button>
                    <button type="button" className="ghost-btn ghost-btn--danger" onClick={() => onSkipped(item)}>{t("pendingSkippedBtn", lang)}</button>
                    <button type="button" className="ghost-btn" onClick={() => onKeepWaiting(item)}>{t("pendingKeepWaitingBtn", lang)}</button>
                  </div>
                </div>
              )}
            </li>
          );
        })}
      </ul>
    </section>
  );
}

/* =========================================================
   Monthly Commitments
   ========================================================= */
function MonthlyCommitmentsCard({ entries, currentIncome }) {
  const { lang } = useLang();
  const [expanded, setExpanded] = useState(false);
  const [showAll, setShowAll] = useState(false);
  const calc = useMemo(() => computeMonthlyCommitments(entries, currentIncome), [entries, currentIncome]);
  const sortedList = useMemo(() => [...calc.list].sort((a, b) => b.amount - a.amount), [calc.list]);
  const visible = showAll ? sortedList : sortedList.slice(0, 3);

  return (
    <section className="panel safe-panel">
      <button type="button" className="safe-toggle" onClick={() => setExpanded((v) => !v)}>
        <div className="safe-icon"><Icon name="receipt" size={20} /></div>
        <div className="safe-main">
          <div className="safe-label">{t("commitmentsTitle", lang)}</div>
          <div className="safe-amount">{currencyFmt(calc.totalMonthly, lang)} <span className="safe-per-day">/ {t("perMonthUnit", lang)}</span></div>
        </div>
        <span className={`safe-chevron ${expanded ? "safe-chevron--open" : ""}`}>‹</span>
      </button>

      {expanded && (
        <div className="safe-breakdown">
          <div className="safe-row"><span>{t("commitmentsYearly", lang)}</span><span>{currencyFmt(calc.totalYearly, lang)}</span></div>
          {calc.recurringSavingsMonthly > 0 && (
            <div className="safe-row safe-row--savings"><span>{t("commitmentsRecurringSavings", lang)}</span><span>{currencyFmt(calc.recurringSavingsMonthly, lang)}</span></div>
          )}
          {calc.pctOfIncome !== null && (
            <div className="safe-row safe-row--muted">
              <span>{t("commitmentsPctIncome", lang)}</span>
              <span>{calc.pctOfIncome.toFixed(0)}% {t("commitmentsOfIncome", lang)}</span>
            </div>
          )}
          {calc.list.length === 0 ? (
            <p className="empty-hint">{t("commitmentsEmpty", lang)}</p>
          ) : (
            <>
              <div className="safe-subhead">{t("topCommitmentsLabel", lang)}</div>
              <ul className="upcoming-list">
                {visible.map((e) => (
                  <li className="upcoming-item" key={e.id}>
                    <div className="entry-icon" aria-hidden="true"><Icon name={categoryIcon(e.kind, e.category)} size={16} /></div>
                    <div className="upcoming-item-main">
                      <span className="upcoming-item-label">{e.label}</span>
                    </div>
                    <span className="entry-amount">-{currencyFmt(e.amount, lang)}</span>
                  </li>
                ))}
              </ul>
              {sortedList.length > 3 && (
                <button type="button" className="view-all-link" onClick={() => setShowAll((v) => !v)}>
                  {showAll ? t("showLessBtn", lang) : t("viewAllCommitmentsBtn", lang)}
                </button>
              )}
            </>
          )}
        </div>
      )}
    </section>
  );
}

/* =========================================================
   Monthly Report
   ========================================================= */
function MonthlyReportCard({ entries, month, isPremium, onPaywall }) {
  const { lang } = useLang();
  const [expanded, setExpanded] = useState(false);
  const report = useMemo(() => computeMonthlyReport(entries, month), [entries, month]);

  return (
    <section className="panel safe-panel">
      <button type="button" className="safe-toggle" onClick={() => setExpanded((v) => !v)}>
        <div className="safe-icon"><Icon name="trending" size={20} /></div>
        <div className="safe-main">
          <div className="safe-label">{t("reportTitle", lang)} — {monthLabel(month, lang)}</div>
          <div className="safe-amount">{report.savingsRate.toFixed(0)}% <span className="safe-per-day">{t("reportSavingsRate", lang)}</span></div>
        </div>
        <span className={`safe-chevron ${expanded ? "safe-chevron--open" : ""}`}>‹</span>
      </button>

      {expanded && (
        <div className="safe-breakdown">
          <div className="safe-row"><span>{t("incomeLabel", lang)}</span><span>{currencyFmt(report.income, lang)}</span></div>
          {report.bonusIncome > 0 && (
            <div className="safe-row safe-row--muted"><span>{t("yearBonusIncome", lang)}</span><span>{currencyFmt(report.bonusIncome, lang)}</span></div>
          )}
          <div className="safe-row"><span>{t("expenseLabel", lang)}</span><span>{currencyFmt(report.expenses, lang)}</span></div>
          <div className="safe-row safe-row--total"><span>{t("balanceBeforeSavings", lang)}</span><span>{currencyFmt(report.savings, lang)}</span></div>
          {report.savingsAllocated > 0 && (
            <>
              <div className="safe-row safe-row--savings"><span>{t("reportSavingsAllocated", lang)}</span><span>-{currencyFmt(report.savingsAllocated, lang)}</span></div>
              <div className="safe-row safe-row--total"><span>{t("cashAfterSavings", lang)}</span><span>{currencyFmt(report.savings - report.savingsAllocated, lang)}</span></div>
            </>
          )}

          {!isPremium ? (
            <PremiumLockNotice onUpgrade={onPaywall} />
          ) : !report.hasPrevData ? (
            <p className="empty-hint">{t("reportNoData", lang)}</p>
          ) : (
            <>
              {report.topCategory && (
                <div className="safe-row safe-row--muted">
                  <span>{t("reportTopCategory", lang)}</span>
                  <span>{categoryLabel("expense", report.topCategory.categoryId, lang)} · {currencyFmt(report.topCategory.amount, lang)}</span>
                </div>
              )}
              {report.biggestExpense && (
                <div className="safe-row safe-row--muted">
                  <span>{t("reportBiggestExpense", lang)}</span>
                  <span>{report.biggestExpense.label} · {currencyFmt(report.biggestExpense.amount, lang)}</span>
                </div>
              )}
              {report.biggestChange && (
                <div className="safe-row safe-row--muted">
                  <span>{t("reportBiggestChange", lang)}</span>
                  <span className={report.biggestChange.pctChange >= 0 ? "whatif-neg" : "report-positive"}>
                    {categoryLabel("expense", report.biggestChange.categoryId, lang)} {report.biggestChange.pctChange >= 0 ? "+" : ""}{report.biggestChange.pctChange.toFixed(0)}%
                  </span>
                </div>
              )}
            </>
          )}
        </div>
      )}
    </section>
  );
}

/* =========================================================
   Year Summary / Year Projection
   ========================================================= */
function YearMonthStrip({ monthly, selectedMonthKey, onSelectMonth, lang }) {
  const max = Math.max(1, ...monthly.map((m) => Math.max(m.income, m.expenses)));
  return (
    <div className="year-strip">
      {monthly.map((m) => {
        const shortLabel = new Date(m.monthKey + "-01").toLocaleDateString(LOCALE_MAP[lang] || "en-US", { month: "short" });
        const isSelected = m.monthKey === selectedMonthKey;
        const barHeight = m.hasData ? Math.max(6, (Math.max(m.income, m.expenses) / max) * 44) : 3;
        return (
          <button
            type="button"
            key={m.monthKey}
            className={`year-strip-month ${isSelected ? "year-strip-month--selected" : ""} ${!m.hasData ? "year-strip-month--empty" : ""}`}
            onClick={() => m.hasData && onSelectMonth(m.monthKey)}
            disabled={!m.hasData}
          >
            <span
              className="year-strip-bar"
              style={{ height: `${barHeight}px`, background: !m.hasData ? "var(--border)" : m.balance >= 0 ? "var(--green)" : "var(--red)" }}
            />
            <span className="year-strip-label">{shortLabel}</span>
          </button>
        );
      })}
    </div>
  );
}

function YearSummaryCard({ entries, year, onYearChange, availableYears, selectedMonthKey, onSelectMonth, isPremium, onPaywall }) {
  const { lang } = useLang();
  const [expanded, setExpanded] = useState(false);
  const summary = useMemo(() => computeYearSummary(entries, year), [entries, year]);
  const idx = availableYears.indexOf(year);
  const canPrev = idx > 0;
  const canNext = idx < availableYears.length - 1;

  return (
    <section className="panel safe-panel">
      <button type="button" className="safe-toggle" onClick={() => setExpanded((v) => !v)}>
        <div className="safe-icon"><Icon name="barChart" size={20} /></div>
        <div className="safe-main">
          <div className="safe-label">{t("yearSummaryTitle", lang)}</div>
          <div className="safe-amount">{year} <span className="safe-per-day">· {summary.savingsRate.toFixed(0)}% {t("reportSavingsRate", lang)}</span></div>
        </div>
        <span className={`safe-chevron ${expanded ? "safe-chevron--open" : ""}`}>‹</span>
      </button>

      {expanded && (
        <div className="safe-breakdown">
          <div className="year-nav">
            <button type="button" className="month-arrow" disabled={!canPrev} onClick={() => canPrev && onYearChange(availableYears[idx - 1])} aria-label={t("prevMonth", lang)}>‹</button>
            <span className="year-nav-label">{year}</span>
            <button type="button" className="month-arrow" disabled={!canNext} onClick={() => canNext && onYearChange(availableYears[idx + 1])} aria-label={t("nextMonth", lang)}>›</button>
          </div>

          {!isPremium ? (
            <PremiumLockNotice onUpgrade={onPaywall} />
          ) : (
            <>
              <div className="safe-row"><span>{t("incomeLabel", lang)}</span><span>{currencyFmt(summary.totalIncome, lang)}</span></div>
              {summary.totalBonusIncome > 0 && (
                <>
                  <div className="safe-row safe-row--muted"><span>{t("yearRegularIncome", lang)}</span><span>{currencyFmt(summary.totalRegularIncome, lang)}</span></div>
                  <div className="safe-row safe-row--muted"><span>{t("yearBonusIncome", lang)}</span><span>{currencyFmt(summary.totalBonusIncome, lang)}</span></div>
                </>
              )}
              <div className="safe-row"><span>{t("expenseLabel", lang)}</span><span>{currencyFmt(summary.totalExpenses, lang)}</span></div>
              <div className="safe-row safe-row--total"><span>{t("balanceBeforeSavings", lang)}</span><span>{currencyFmt(summary.netBalance, lang)}</span></div>
              {summary.totalSavings > 0 && (
                <>
                  <div className="safe-row safe-row--savings"><span>{t("reportSavingsAllocated", lang)}</span><span>-{currencyFmt(summary.totalSavings, lang)}</span></div>
                  <div className="safe-row safe-row--total"><span>{t("cashAfterSavings", lang)}</span><span>{currencyFmt(summary.netBalance - summary.totalSavings, lang)}</span></div>
                </>
              )}

              {summary.monthly.some((m) => m.hasData) ? (
                <YearMonthStrip monthly={summary.monthly} selectedMonthKey={selectedMonthKey} onSelectMonth={onSelectMonth} lang={lang} />
              ) : (
                <p className="empty-hint">{t("yearNoData", lang)}</p>
              )}

              {summary.projection && (
                <div className="year-projection">
                  <div className="year-projection-title">
                    <Icon name="trending" size={14} /> {t("yearProjectionTitle", lang)}
                  </div>
                  <p className="year-projection-text">
                    {t("yearProjectionText1", lang)} {year} {t("yearProjectionText2", lang)}{" "}
                    <strong>{currencyFmt(Math.max(0, summary.projection.projectedSavings), lang)}</strong>{" "}
                    {t("yearProjectionText3", lang)}{" "}
                    <strong className={summary.projection.projectedBalance >= 0 ? "report-positive" : "whatif-neg"}>
                      {currencyFmt(summary.projection.projectedBalance, lang)}
                    </strong>.
                  </p>
                  <p className="year-projection-basis">
                    {t("yearProjectionBasis", lang)} {currencyFmt(summary.projection.avgRegularIncome, lang)}/{t("perMonthUnit", lang)}
                    {summary.totalBonusIncome > 0 ? ` · ${t("yearProjectionBonusNote", lang)}` : ""}
                  </p>
                </div>
              )}
              {summary.isFutureYear && <p className="empty-hint">{t("yearFutureHint", lang)}</p>}
            </>
          )}
        </div>
      )}
    </section>
  );
}

/* =========================================================
   Receipt scanning — Groq vision, opt-in
   ========================================================= */
function fileToBase64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
}

function isCanvasBlank(canvas) {
  const ctx = canvas.getContext("2d");
  if (!ctx) return true;
  const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const step = Math.max(4, Math.floor(data.length / 4 / 400) * 4);
  for (let i = 0; i < data.length; i += step) {
    if (data[i] > 8 || data[i + 1] > 8 || data[i + 2] > 8) return false;
  }
  return true;
}

function compressImageFile(file, maxDimension, quality) {
  return new Promise((resolve, reject) => {
    const objectUrl = URL.createObjectURL(file);
    const img = new Image();
    img.onload = () => {
      try {
        let width = img.naturalWidth || img.width;
        let height = img.naturalHeight || img.height;
        if (!width || !height) throw new Error("Image has no dimensions");
        if (width > height && width > maxDimension) {
          height = Math.round(height * (maxDimension / width));
          width = maxDimension;
        } else if (height >= width && height > maxDimension) {
          width = Math.round(width * (maxDimension / height));
          height = maxDimension;
        }
        const canvas = document.createElement("canvas");
        canvas.width = width;
        canvas.height = height;
        const ctx = canvas.getContext("2d");
        ctx.drawImage(img, 0, 0, width, height);
        URL.revokeObjectURL(objectUrl);
        if (isCanvasBlank(canvas)) {
          reject(new Error("Decoded image appears blank — likely an unsupported photo format"));
          return;
        }
        resolve(canvas.toDataURL("image/jpeg", quality));
      } catch (err) {
        URL.revokeObjectURL(objectUrl);
        reject(err);
      }
    };
    img.onerror = () => {
      URL.revokeObjectURL(objectUrl);
      reject(new Error("Could not decode image"));
    };
    img.src = objectUrl;
  });
}

// A live in-app camera view — used as a "Scan" alternative to handing off
// to the phone's separate native camera app. Keeps the person inside the
// app for a single continuous flow: point, tap capture, and the frame
// immediately goes into the same compress → upload → review pipeline as
// a regular photo. Takes an already-acquired MediaStream rather than
// requesting one itself — iOS Safari (especially in an installed,
// standalone PWA) is unreliable about granting camera access from inside
// a useEffect, so the request has to happen as directly as possible
// inside the button tap that opens this view, one level up.
function CameraCaptureView({ stream, onCapture, onCancel }) {
  const { lang } = useLang();
  const videoRef = useRef(null);

  useEffect(() => {
    if (videoRef.current && stream) {
      videoRef.current.srcObject = stream;
      const p = videoRef.current.play();
      if (p && p.catch) p.catch(() => {});
    }
    return () => {
      if (stream) stream.getTracks().forEach((t) => t.stop());
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [stream]);

  function capture() {
    const video = videoRef.current;
    if (!video || !video.videoWidth) return;
    const canvas = document.createElement("canvas");
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    const ctx = canvas.getContext("2d");
    ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
    canvas.toBlob((blob) => {
      if (blob) onCapture(blob);
    }, "image/jpeg", 0.85);
  }

  return (
    <div className="camera-capture-view">
      <div style={{ position: "relative", width: "100%", aspectRatio: "3 / 4", borderRadius: 12, overflow: "hidden", background: "#000" }}>
        <video ref={videoRef} playsInline muted style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }} />
      </div>
      <div style={{ marginTop: 10 }}>
        <button type="button" className="submit-btn" onClick={capture}>
          <Icon name="camera" size={16} /> {t("cameraCaptureBtn", lang)}
        </button>
        <button type="button" className="ghost-btn" style={{ width: "100%", marginTop: 8 }} onClick={onCancel}>
          {t("cancelBtn", lang)}
        </button>
      </div>
    </div>
  );
}

// Requests camera access as directly as possible from inside a real tap
// handler (see CameraCaptureView above for why) and returns the stream,
// or null with the error state set via setError.
async function requestCameraStream(setError, lang) {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: { facingMode: { ideal: "environment" } },
      audio: false,
    });
    return stream;
  } catch (err) {
    console.error("Camera access error:", err);
    setError(t("cameraAccessError", lang));
    return null;
  }
}

function ReceiptScanModal({ accessToken, onAddExpense, onClose, month }) {
  const { lang } = useLang();
  const fileInputRef = useRef(null);
  const libraryInputRef = useRef(null);
  const [scanning, setScanning] = useState(false);
  const [error, setError] = useState("");
  const [result, setResult] = useState(null);
  const [label, setLabel] = useState("");
  const [amount, setAmount] = useState("");
  const [category, setCategory] = useState(EXPENSE_CATEGORIES[0].id);
  // Whether this expense should repeat every month (e.g. a recurring
  // bill someone happens to have a paper receipt for) or was just this
  // one time — same choice offered on the manual Add Entry form.
  const [recurring, setRecurring] = useState(false);
  const [cameraStream, setCameraStream] = useState(null);

  async function openLiveCamera() {
    setError("");
    const stream = await requestCameraStream(setError, lang);
    if (stream) setCameraStream(stream);
  }
  function closeLiveCamera() {
    if (cameraStream) cameraStream.getTracks().forEach((t) => t.stop());
    setCameraStream(null);
  }

  async function processFile(file) {
    setError("");
    setScanning(true);
    try {
      let base64;
      try {
        base64 = await compressImageFile(file, 1200, 0.72);
      } catch (compressErr) {
        console.error("Receipt compression fallback:", compressErr);
        base64 = await fileToBase64(file);
      }
      const scanned = await scanReceiptImage(base64, accessToken);
      setScanning(false);
      if (!scanned) {
        setError(t("receiptScanFailed", lang));
        return;
      }
      setResult(scanned);
      setLabel(scanned.merchant || "");
      setAmount(scanned.amount != null ? String(scanned.amount) : "");
      const matchedCategory = EXPENSE_CATEGORIES.find((c) => c.id === scanned.category);
      setCategory(matchedCategory ? matchedCategory.id : EXPENSE_CATEGORIES[0].id);
    } catch (err) {
      setScanning(false);
      setError(t("receiptScanFailed", lang));
    }
  }

  async function handleFile(e) {
    const file = e.target.files && e.target.files[0];
    e.target.value = "";
    if (!file) return;
    await processFile(file);
  }

  async function handleCameraCapture(blob) {
    closeLiveCamera();
    await processFile(blob);
  }

  function confirmAdd() {
    const value = parseFloat(String(amount).replace(",", "."));
    if (!value || value <= 0 || !label.trim()) return;
    onAddExpense({
      id: makeId(), kind: "expense", category,
      label: label.trim(), amount: value,
      // Lands in whichever month the person is currently browsing, not
      // always today's real date — otherwise scanning a receipt while
      // looking at a different month would silently misfile it, the same
      // bug already fixed for the manual Add Entry form.
      date: defaultDateForMonth(month || monthKey(todayISO())), term: "day",
      recurring, endMonth: null, exceptions: {},
    });
    onClose();
  }

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{t("receiptScanTitle", lang)}</h3>
          <button type="button" className="modal-close" onClick={onClose} aria-label={t("cancelBtn", lang)}><Icon name="x" size={16} /></button>
        </div>

        {!result && !scanning && !cameraStream && (
          <>
            <p className="quick-desc">{t("receiptScanDesc", lang)}</p>
            <input
              ref={fileInputRef} type="file" accept="image/*" capture="environment"
              style={{ display: "none" }} onChange={handleFile}
            />
            <input
              ref={libraryInputRef} type="file" accept="image/*"
              style={{ display: "none" }} onChange={handleFile}
            />
            <button type="button" className="submit-btn" onClick={openLiveCamera}>
              <Icon name="camera" size={16} /> {t("cameraScanBtn", lang)}
            </button>
            <button type="button" className="ghost-btn" style={{ width: "100%", marginTop: 8 }} onClick={() => fileInputRef.current && fileInputRef.current.click()}>
              <Icon name="camera" size={14} /> {t("receiptScanTakePhotoBtn", lang)}
            </button>
            <button type="button" className="ghost-btn" style={{ width: "100%", marginTop: 8 }} onClick={() => libraryInputRef.current && libraryInputRef.current.click()}>
              <Icon name="upload" size={14} /> {t("dienstplanScanChooseBtn", lang)}
            </button>
            {error && <p className="form-error">{error}</p>}
          </>
        )}

        {cameraStream && !scanning && (
          <CameraCaptureView stream={cameraStream} onCapture={handleCameraCapture} onCancel={closeLiveCamera} />
        )}

        {scanning && (
          <div className="receipt-scan-loading">
            <div className="spinner" />
            <p>{t("receiptScanReading", lang)}</p>
          </div>
        )}

        {result && !scanning && (
          <>
            <p className="quick-desc">{t("receiptScanReviewDesc", lang)}</p>
            <div className="form-grid">
              <label className="field field--wide">
                <span>{t("receiptScanMerchant", lang)}</span>
                <input type="text" value={label} onChange={(e) => setLabel(e.target.value)} />
              </label>
              <label className="field">
                <span>{t("amountLabel", lang)}</span>
                <input type="text" inputMode="decimal" value={amount} onChange={(e) => setAmount(e.target.value)} placeholder="0.00" />
              </label>
              <label className="field">
                <span>{t("categoryLabel", lang)}</span>
                <select value={category} onChange={(e) => setCategory(e.target.value)}>
                  {EXPENSE_CATEGORIES.filter((c) => c.id !== "quick").map((c) => <option key={c.id} value={c.id}>{c[lang] || c.en}</option>)}
                </select>
              </label>
            </div>
            <label className="recurring-check">
              <input type="checkbox" checked={recurring} onChange={(e) => setRecurring(e.target.checked)} />
              <span>{t("recurringLabel", lang)}</span>
            </label>
            {recurring && <p className="recurring-hint">{t("recurringHint", lang)}</p>}
            <button type="button" className="submit-btn" onClick={confirmAdd}>{t("receiptScanConfirmBtn", lang)}</button>
            <button type="button" className="ghost-btn" style={{ width: "100%", marginTop: 8 }} onClick={() => { setResult(null); setError(""); }}>
              {t("receiptScanRetakeBtn", lang)}
            </button>
          </>
        )}
      </div>
    </div>
  );
}

/* =========================================================
   Dienstplan scanning — reads a shift-schedule photo for one named
   employee and prefills a review calendar for the open month. Never
   commits anything until the person taps confirm.
   ========================================================= */
function DienstplanScanModal({ accessToken, month, employeeName, onSaveEmployeeName, onConfirm, onClose }) {
  const { lang } = useLang();
  const fileInputRef = useRef(null);
  const libraryInputRef = useRef(null);
  const [name, setName] = useState(employeeName || "");
  const [scanning, setScanning] = useState(false);
  const [error, setError] = useState("");
  const [days, setDays] = useState(null); // { "1": "fruh", ... } once scanned

  const total = daysInMonth(month);
  const [year, mo] = month.split("-").map(Number);
  // iOS Safari can silently reload the page in the background (memory
  // pressure while the person is away in Photos/Camera, for instance),
  // which wipes all in-memory React state. A finished scan is expensive
  // (a real API call, tens of seconds) to lose that way, so the review
  // result is mirrored to localStorage the moment it arrives and
  // restored automatically if the modal remounts before it's confirmed.
  const draftKey = `salary-planner:dienstplan-scan-draft:v1:${month}`;
  useEffect(() => {
    try {
      const raw = localStorage.getItem(draftKey);
      if (raw) {
        const saved = JSON.parse(raw);
        if (saved && saved.days) setDays(saved.days);
      }
    } catch {}
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  useEffect(() => {
    try {
      if (days) localStorage.setItem(draftKey, JSON.stringify({ days }));
      else localStorage.removeItem(draftKey);
    } catch {}
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [days]);
  const [cameraStream, setCameraStream] = useState(null);

  async function openLiveCamera() {
    if (!name.trim()) { setError(t("dienstplanScanNameLabel", lang)); return; }
    setError("");
    const stream = await requestCameraStream(setError, lang);
    if (stream) setCameraStream(stream);
  }
  function closeLiveCamera() {
    if (cameraStream) cameraStream.getTracks().forEach((t) => t.stop());
    setCameraStream(null);
  }

  async function processDienstplanFile(file) {
    if (!name.trim()) { setError(t("dienstplanScanNameLabel", lang)); return; }
    setError("");
    setScanning(true);
    try {
      let base64;
      try {
        // A smaller/lighter image than receipts leaves more of Groq's
        // free-tier per-minute token budget for the model's own
        // reasoning over the full month's grid — sending it too large
        // was part of why replies were getting cut off before the JSON.
        base64 = await compressImageFile(file, 1100, 0.7);
      } catch (compressErr) {
        console.error("Dienstplan compression fallback:", compressErr);
        base64 = await fileToBase64(file);
      }
      onSaveEmployeeName(name.trim());
      const scanned = await scanDienstplanImage(base64, name.trim(), year, mo, total, accessToken);
      setScanning(false);
      if (!scanned) {
        setError(t("dienstplanScanFailed", lang));
        return;
      }
      if (!scanned.found) {
        setError(t("dienstplanScanNotFound", lang));
        return;
      }
      // Fill every day of the month — anything the model couldn't read
      // defaults to "frei" so the review grid always shows a complete,
      // editable month rather than gaps.
      const filled = {};
      for (let d = 1; d <= total; d++) {
        filled[String(d)] = (scanned.days && scanned.days[String(d)]) || "frei";
      }
      setDays(filled);
    } catch (err) {
      setScanning(false);
      setError(t("dienstplanScanFailed", lang));
    }
  }

  async function handleFile(e) {
    const file = e.target.files && e.target.files[0];
    e.target.value = "";
    if (!file) return;
    await processDienstplanFile(file);
  }

  async function handleCameraCapture(blob) {
    closeLiveCamera();
    await processDienstplanFile(blob);
  }

  function setDay(d, code) {
    setDays((prev) => ({ ...prev, [String(d)]: code }));
  }

  function confirm() {
    if (!days) return;
    const scheduleObj = { ...days, notes: {}, tags: {} };
    try { localStorage.removeItem(draftKey); } catch {}
    onConfirm(scheduleObj);
    onClose();
  }

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{t("dienstplanScanTitle", lang)}</h3>
          <button type="button" className="modal-close" onClick={onClose} aria-label={t("cancelBtn", lang)}><Icon name="x" size={16} /></button>
        </div>

        {!days && !scanning && !cameraStream && (
          <>
            <label className="field field--wide">
              <span>{t("dienstplanScanNameLabel", lang)}</span>
              <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder={t("dienstplanScanNamePlaceholder", lang)} />
            </label>
            <input
              ref={fileInputRef} type="file" accept="image/*" capture="environment"
              style={{ display: "none" }} onChange={handleFile}
            />
            <input
              ref={libraryInputRef} type="file" accept="image/*"
              style={{ display: "none" }} onChange={handleFile}
            />
            <button type="button" className="submit-btn" onClick={openLiveCamera}>
              <Icon name="camera" size={16} /> {t("cameraScanBtn", lang)}
            </button>
            <button type="button" className="ghost-btn" style={{ width: "100%", marginTop: 8 }} onClick={() => fileInputRef.current && fileInputRef.current.click()}>
              <Icon name="camera" size={14} /> {t("dienstplanScanTakePhotoBtn", lang)}
            </button>
            <button type="button" className="ghost-btn" style={{ width: "100%", marginTop: 8 }} onClick={() => libraryInputRef.current && libraryInputRef.current.click()}>
              <Icon name="upload" size={14} /> {t("dienstplanScanChooseBtn", lang)}
            </button>
            {error && <p className="form-error">{error}</p>}
          </>
        )}

        {cameraStream && !scanning && (
          <CameraCaptureView stream={cameraStream} onCapture={handleCameraCapture} onCancel={closeLiveCamera} />
        )}

        {scanning && (
          <div className="receipt-scan-loading">
            <div className="spinner" />
            <p>{t("dienstplanScanReading", lang)}</p>
          </div>
        )}

        {days && !scanning && (
          <>
            <p className="quick-desc">{t("dienstplanScanReviewDesc", lang)}</p>
            <div className="import-rows" style={{ maxHeight: 360, overflowY: "auto" }}>
              {Array.from({ length: total }).map((_, i) => {
                const d = i + 1;
                const code = days[String(d)];
                return (
                  <div className="import-row" key={d} style={{ alignItems: "center" }}>
                    <span style={{ width: 28, flexShrink: 0, fontWeight: 700, fontSize: 13 }}>{d}</span>
                    <div className="import-row-main">
                      <select value={code} onChange={(e) => setDay(d, e.target.value)}>
                        {SHIFT_TYPES.map((s) => (
                          <option key={s.code} value={s.code}>{t(s.labelKey, lang)}</option>
                        ))}
                      </select>
                    </div>
                  </div>
                );
              })}
            </div>
            <button type="button" className="submit-btn" onClick={confirm}>{t("dienstplanScanConfirmBtn", lang)}</button>
            <button type="button" className="ghost-btn" style={{ width: "100%", marginTop: 8 }} onClick={() => { setDays(null); setError(""); }}>
              {t("dienstplanScanRetakeBtn", lang)}
            </button>
          </>
        )}
      </div>
    </div>
  );
}

/* =========================================================
   Entry form
   ========================================================= */
function EntryForm({ onSubmit, initial, hideRecurring, submitLabel, defaultMonth }) {
  const { lang } = useLang();
  const [kind, setKind] = useState(initial ? initial.kind : "expense");
  const [date, setDate] = useState(initial ? initial.date : defaultDateForMonth(defaultMonth || monthKey(todayISO())));
  const [categoryId, setCategoryId] = useState(initial ? initial.category : EXPENSE_CATEGORIES[0].id);
  const [term, setTerm] = useState(initial && initial.term ? initial.term : "day");
  const [label, setLabel] = useState(initial ? initial.label : "");
  const [amount, setAmount] = useState(initial ? String(initial.amount) : "");
  const [recurring, setRecurring] = useState(initial ? !!initial.recurring : false);
  const [showCommonExpenses, setShowCommonExpenses] = useState(false);
  const [error, setError] = useState("");

  const categories = kind === "expense"
    ? EXPENSE_CATEGORIES.filter((c) => c.id !== "quick")
    : INCOME_CATEGORIES;

  function switchKind(next) {
    setKind(next);
    setCategoryId(next === "expense" ? EXPENSE_CATEGORIES[0].id : INCOME_CATEGORIES[0].id);
    if (next === "income") setRecurring(false);
    setError("");
  }

  function chooseCommonExpense(item) {
    setCategoryId(item.category);
    setLabel(t(item.labelKey, lang));
    if (item.category === "subscriptions") setRecurring(true);
    setError("");
  }

  function submit(e) {
    e.preventDefault();
    const value = parseFloat(String(amount).replace(",", "."));
    if (!value || value <= 0) { setError(t("errAmount", lang)); return; }
    if (!date) { setError(t("errDate", lang)); return; }
    const fallbackLabel = categoryLabel(kind, categoryId, lang);
    onSubmit({
      kind, date, category: categoryId,
      label: label.trim() || fallbackLabel, amount: value,
      term: kind === "expense" ? term : null,
      recurring: hideRecurring ? (initial ? !!initial.recurring : false) : recurring,
    });
    if (!initial) { setLabel(""); setAmount(""); setError(""); }
  }

  return (
    <form className="entry-form" onSubmit={submit}>
      <div className="kind-toggle" role="tablist" aria-label="kind">
        <button type="button" role="tab" aria-selected={kind === "expense"}
          className={`kind-btn ${kind === "expense" ? "kind-btn--active kind-expense" : ""}`}
          onClick={() => switchKind("expense")}>{t("expenseTab", lang)}</button>
        <button type="button" role="tab" aria-selected={kind === "income"}
          className={`kind-btn ${kind === "income" ? "kind-btn--active kind-income" : ""}`}
          onClick={() => switchKind("income")}>{t("incomeTab", lang)}</button>
      </div>

      {kind === "expense" && (
        <div style={{ margin: "12px 0 4px" }}>
          <button
            type="button"
            onClick={() => setShowCommonExpenses((visible) => !visible)}
            aria-expanded={showCommonExpenses}
            style={{ display: "inline-flex", alignItems: "center", gap: 7, padding: "8px 11px", borderRadius: 9, border: "1px solid rgba(224,166,78,.55)", background: showCommonExpenses ? "rgba(224,166,78,.18)" : "transparent", color: "inherit", cursor: "pointer" }}
          >
            <Icon name="calendarGrid" size={14} />
            {t("commonExpenseToggle", lang)}
            <span style={{ fontSize: 15, lineHeight: 1 }}>{showCommonExpenses ? "⌃" : "⌄"}</span>
          </button>
          {showCommonExpenses && (
            <div style={{ marginTop: 9 }}>
              <div style={{ marginBottom: 8, fontSize: 12, opacity: .75 }}>{t("commonExpenseLabel", lang)}</div>
              <div style={{ display: "flex", flexWrap: "wrap", gap: 7 }}>
                {COMMON_EXPENSES.map((item) => (
                  <button
                    type="button"
                    key={item.labelKey}
                    onClick={() => chooseCommonExpense(item)}
                    style={{ display: "inline-flex", alignItems: "center", gap: 5, padding: "7px 9px", borderRadius: 9, border: "1px solid rgba(141,154,181,.3)", background: categoryId === item.category && label === t(item.labelKey, lang) ? "rgba(224,166,78,.2)" : "transparent", color: "inherit", cursor: "pointer" }}
                  >
                    <Icon name={item.icon} size={13} />
                    {t(item.labelKey, lang)}
                  </button>
                ))}
              </div>
            </div>
          )}
        </div>
      )}

      <div className="form-grid">
        <label className="field">
          <span>{t("dateLabel", lang)}</span>
          <input type="date" value={date} onChange={(e) => setDate(e.target.value)} required />
        </label>
        <label className="field">
          <span>{kind === "expense" ? t("categoryLabel", lang) : t("sourceLabel", lang)}</span>
          <select value={categoryId} onChange={(e) => setCategoryId(e.target.value)}>
            {categories.map((c) => <option key={c.id} value={c.id}>{c[lang] || c.en}</option>)}
          </select>
        </label>
        {kind === "expense" && (
          <label className="field">
            <span>{t("termLabel", lang)}</span>
            <select value={term} onChange={(e) => setTerm(e.target.value)}>
              <option value="day">{t("termDay", lang)}</option>
              <option value="month">{t("termMonth", lang)}</option>
              <option value="year">{t("termYear", lang)}</option>
            </select>
          </label>
        )}
        <label className={`field ${kind === "expense" ? "" : "field--wide"}`}>
          <span>{t("descLabel", lang)}</span>
          <input type="text" placeholder={kind === "expense" ? t("descPlaceholderExpense", lang) : t("descPlaceholderIncome", lang)}
            value={label} onChange={(e) => setLabel(e.target.value)} />
        </label>
        <label className="field">
          <span>{t("amountLabel", lang)}</span>
          <input type="text" inputMode="decimal" placeholder="0.00" value={amount}
            onChange={(e) => setAmount(e.target.value)} />
        </label>
      </div>

      {!hideRecurring && (
        <label className="recurring-check">
          <input type="checkbox" checked={recurring} onChange={(e) => setRecurring(e.target.checked)} />
          <span>{t("recurringLabel", lang)}</span>
        </label>
      )}
      {!hideRecurring && recurring && <p className="recurring-hint">{t("recurringHint", lang)}</p>}

      {error && <p className="form-error">{error}</p>}

      <button type="submit" className={`submit-btn ${kind === "income" ? "submit-btn--income" : ""}`}>
        {submitLabel || (kind === "expense" ? t("addExpenseBtn", lang) : t("addIncomeBtn", lang))}
      </button>
    </form>
  );
}

/* =========================================================
   Quick temporary expense
   ========================================================= */
function QuickExpenseForm({ onAdd, onClose }) {
  const { lang } = useLang();
  const [amount, setAmount] = useState("");
  const [label, setLabel] = useState("");
  const [duration, setDuration] = useState("day");
  const [error, setError] = useState("");

  function submit(e) {
    e.preventDefault();
    const value = parseFloat(String(amount).replace(",", "."));
    if (!value || value <= 0) { setError(t("errAmount", lang)); return; }
    const ms = duration === "week" ? 7 * DAY_MS : DAY_MS;
    onAdd({
      id: makeId(),
      kind: "expense",
      date: todayISO(),
      category: "quick",
      term: "day",
      label: label.trim() || categoryLabel("expense", "quick", lang),
      amount: value,
      expiresAt: Date.now() + ms,
    });
    setAmount(""); setLabel(""); setError("");
    onClose();
  }

  return (
    <form className="quick-form" onSubmit={submit}>
      <p className="quick-desc">{t("quickDesc", lang)}</p>
      <div className="form-grid">
        <label className="field field--wide">
          <span>{t("descLabel", lang)}</span>
          <input type="text" placeholder={t("descPlaceholderExpense", lang)} value={label} onChange={(e) => setLabel(e.target.value)} />
        </label>
        <label className="field">
          <span>{t("amountLabel", lang)}</span>
          <input type="text" inputMode="decimal" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} autoFocus />
        </label>
        <label className="field">
          <span>{t("quickDuration", lang)}</span>
          <select value={duration} onChange={(e) => setDuration(e.target.value)}>
            <option value="day">{t("quickDay", lang)}</option>
            <option value="week">{t("quickWeek", lang)}</option>
          </select>
        </label>
      </div>
      {error && <p className="form-error">{error}</p>}
      <div className="quick-actions">
        <button type="button" className="ghost-btn" onClick={onClose}>{t("quickCancel", lang)}</button>
        <button type="submit" className="submit-btn" style={{ marginTop: 0, flex: 1 }}>{t("quickAddBtn", lang)}</button>
      </div>
    </form>
  );
}

/* =========================================================
   Search modal
   ========================================================= */
function SearchModal({ allEntries, onEdit, onDelete, onClose }) {
  const { lang } = useLang();
  const [query, setQuery] = useState("");
  const [kindFilter, setKindFilter] = useState("all");

  const filtered = useMemo(() => {
    const q = query.trim().toLowerCase();
    return allEntries.filter((e) => {
      if (kindFilter !== "all" && e.kind !== kindFilter) return false;
      if (!q) return true;
      const catLabel = categoryLabel(e.kind, e.category, lang).toLowerCase();
      return (e.label || "").toLowerCase().includes(q) || catLabel.includes(q);
    });
  }, [allEntries, query, kindFilter, lang]);

  const net = useMemo(
    () => filtered.reduce((s, e) => s + (e.kind === "income" ? e.amount : -e.amount), 0),
    [filtered]
  );

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{t("searchTitle", lang)}</h3>
          <button type="button" className="modal-close" onClick={onClose} aria-label={t("cancelBtn", lang)}><Icon name="x" size={16} /></button>
        </div>

        <div className="search-input-wrap">
          <Icon name="search" size={16} />
          <input
            type="text" autoFocus value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder={t("searchPlaceholder", lang)}
            className="search-input"
          />
        </div>

        <div className="kind-toggle" role="tablist" aria-label="kind-filter" style={{ margin: "12px 0 6px" }}>
          <button type="button" className={`kind-btn ${kindFilter === "all" ? "kind-btn--active" : ""}`} onClick={() => setKindFilter("all")}>
            {t("searchFilterAll", lang)}
          </button>
          <button type="button" className={`kind-btn ${kindFilter === "expense" ? "kind-btn--active kind-expense" : ""}`} onClick={() => setKindFilter("expense")}>
            {t("expenseTab", lang)}
          </button>
          <button type="button" className={`kind-btn ${kindFilter === "income" ? "kind-btn--active kind-income" : ""}`} onClick={() => setKindFilter("income")}>
            {t("incomeTab", lang)}
          </button>
        </div>

        {filtered.length > 0 && (
          <div className="search-summary">
            <span>{filtered.length} {t("searchResultsFound", lang)}</span>
            <span className={net >= 0 ? "search-net-pos" : "search-net-neg"}>
              {t("searchNet", lang)}: {currencyFmt(net, lang)}
            </span>
          </div>
        )}

        {filtered.length === 0 ? (
          <p className="empty-hint">{t("searchNoResults", lang)}</p>
        ) : (
          <EntryList entries={filtered} onEdit={onEdit} onDelete={onDelete} />
        )}
      </div>
    </div>
  );
}

/* =========================================================
   Import options
   ========================================================= */
function ImportOptionsModal({ onChooseBank, onChooseBackup, onClose }) {
  const { lang } = useLang();
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card modal-card--scope" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{t("importMenuTitle", lang)}</h3>
          <button type="button" className="modal-close" onClick={onClose} aria-label={t("cancelBtn", lang)}><Icon name="x" size={16} /></button>
        </div>
        <div className="scope-options">
          <button type="button" className="scope-option" onClick={onChooseBank}>
            <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
              <Icon name="upload" size={16} />
              <div>
                <div>{t("importBankBtn", lang)}</div>
                <div style={{ fontSize: 11, opacity: .65, marginTop: 2 }}>{t("importMenuBankHint", lang)}</div>
              </div>
            </div>
          </button>
          <button type="button" className="scope-option" onClick={onChooseBackup}>
            <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
              <Icon name="file" size={16} />
              <div>
                <div>{t("importBtn", lang)}</div>
                <div style={{ fontSize: 11, opacity: .65, marginTop: 2 }}>{t("importMenuBackupHint", lang)}</div>
              </div>
            </div>
          </button>
        </div>
        <button type="button" className="ghost-btn" style={{ width: "100%", marginTop: 10 }} onClick={onClose}>{t("cancelBtn", lang)}</button>
      </div>
    </div>
  );
}

/* =========================================================
   Bank statement import modal
   ========================================================= */
function ImportBankModal({ onImport, onClose }) {
  const { lang } = useLang();
  const [rows, setRows] = useState(null);
  const [error, setError] = useState("");
  const [pdfLoading, setPdfLoading] = useState(false);
  const fileRef = useRef(null);
  const pdfRef = useRef(null);

  function handleFile(e) {
    const file = e.target.files && e.target.files[0];
    if (!file) return;
    setError("");
    const reader = new FileReader();
    reader.onload = () => {
      try {
        const parsed = parseBankCSV(String(reader.result));
        setRows(parsed);
      } catch {
        setError(t("importBankParseError", lang));
      }
    };
    reader.onerror = () => setError(t("importBankParseError", lang));
    reader.readAsText(file);
    e.target.value = "";
  }

  async function handlePdfFile(e) {
    const file = e.target.files && e.target.files[0];
    if (!file) return;
    setError("");
    setPdfLoading(true);
    try {
      const text = await extractPdfText(file);
      const parsed = parseBankPDFText(text);
      setRows(parsed);
    } catch {
      setError(t("importBankPdfError", lang));
    } finally {
      setPdfLoading(false);
      e.target.value = "";
    }
  }

  function updateRow(idx, patch) {
    setRows((prev) => prev.map((r, i) => (i === idx ? { ...r, ...patch } : r)));
  }

  function confirmImport() {
    const included = rows.filter((r) => r.include);
    const entries = included.map((r) => ({
      id: makeId(),
      kind: r.kind,
      date: r.date,
      category: r.category,
      label: r.label,
      amount: r.amount,
      term: r.kind === "expense" ? "day" : null,
      recurring: false,
      endMonth: null,
      exceptions: {},
    }));
    onImport(entries);
    onClose();
  }

  const includedCount = rows ? rows.filter((r) => r.include).length : 0;
  const categoriesFor = (kind) => (kind === "expense" ? EXPENSE_CATEGORIES.filter((c) => c.id !== "quick") : INCOME_CATEGORIES);

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{t("importBankTitle", lang)}</h3>
          <button type="button" className="modal-close" onClick={onClose} aria-label={t("cancelBtn", lang)}><Icon name="x" size={16} /></button>
        </div>

        {rows === null && (
          <div>
            <p className="quick-desc">{t("importBankDesc", lang)}</p>
            {error && <p className="form-error">{error}</p>}
            <input ref={fileRef} type="file" accept=".csv,.txt,text/csv,text/plain,application/vnd.ms-excel,text/comma-separated-values" onChange={handleFile} hidden />
            <input ref={pdfRef} type="file" accept=".pdf,application/pdf" onChange={handlePdfFile} hidden />
            <button type="button" className="submit-btn" onClick={() => fileRef.current && fileRef.current.click()}>
              <Icon name="upload" size={16} /> {t("importBankChoose", lang)}
            </button>
            <button type="button" className="ghost-btn" style={{ width: "100%", marginTop: 10 }} disabled={pdfLoading} onClick={() => pdfRef.current && pdfRef.current.click()}>
              <Icon name="upload" size={14} /> {pdfLoading ? t("importBankPdfLoading", lang) : t("importBankChoosePdf", lang)}
            </button>
            <p className="dienstplan-hint" style={{ marginTop: 12 }}>{t("importBankPdfHint", lang)}</p>
          </div>
        )}

        {rows !== null && rows.length === 0 && (
          <div>
            <p className="form-error">{t("importBankNoRows", lang)}</p>
            <button type="button" className="ghost-btn" onClick={() => setRows(null)}>{t("importBankBack", lang)}</button>
          </div>
        )}

        {rows !== null && rows.length > 0 && (
          <div>
            <h4 className="import-review-title">{t("importBankReviewTitle", lang)}</h4>
            <ul className="import-rows">
              {rows.map((r, idx) => (
                <li className={`import-row ${r.include ? "" : "import-row--excluded"}`} key={idx}>
                  <input
                    type="checkbox"
                    checked={r.include}
                    onChange={(e) => updateRow(idx, { include: e.target.checked })}
                    aria-label={t("importBankInclude", lang)}
                  />
                  <div className="import-row-main">
                    <div className="import-row-top">
                      <span className="import-row-label">{r.label}</span>
                      <span className={`entry-amount ${r.kind === "income" ? "entry-amount--income" : ""}`}>
                        {r.kind === "income" ? "+" : "-"}{currencyFmt(r.amount, lang)}
                      </span>
                    </div>
                    <div className="import-row-controls">
                      <span className="import-row-date">{r.date}</span>
                      <select
                        value={r.category}
                        onChange={(e) => updateRow(idx, { category: e.target.value })}
                      >
                        {categoriesFor(r.kind).map((c) => (
                          <option key={c.id} value={c.id}>{c[lang] || c.en}</option>
                        ))}
                      </select>
                    </div>
                  </div>
                </li>
              ))}
            </ul>
            <button type="button" className="submit-btn" onClick={confirmImport} disabled={includedCount === 0}>
              {t("importBankConfirmBtn", lang)} ({includedCount})
            </button>
            <button type="button" className="ghost-btn" style={{ width: "100%", marginTop: 10 }} onClick={() => setRows(null)}>
              {t("importBankBack", lang)}
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

/* =========================================================
   Edit modal
   ========================================================= */
function EditModal({ entry, hideRecurring, onSave, onClose }) {
  const { lang } = useLang();
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{t("editEntryTitle", lang)}</h3>
          <button type="button" className="modal-close" onClick={onClose} aria-label={t("cancelBtn", lang)}><Icon name="x" size={16} /></button>
        </div>
        <EntryForm initial={entry} hideRecurring={hideRecurring} submitLabel={t("saveBtn", lang)} onSubmit={onSave} />
      </div>
    </div>
  );
}

/* =========================================================
   Scope modal
   ========================================================= */
function ScopeModal({ kind, onChoose, onClose }) {
  const { lang } = useLang();
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card modal-card--scope" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{kind === "delete" ? t("deleteScopeTitle", lang) : t("editScopeTitle", lang)}</h3>
          <button type="button" className="modal-close" onClick={onClose} aria-label={t("cancelBtn", lang)}><Icon name="x" size={16} /></button>
        </div>
        <div className="scope-options">
          <button type="button" className="scope-option" onClick={() => onChoose("this")}>{t("scopeThis", lang)}</button>
          <button type="button" className="scope-option" onClick={() => onChoose("future")}>{t("scopeFuture", lang)}</button>
          {kind === "delete" && (
            <button type="button" className="scope-option scope-option--danger" onClick={() => onChoose("all")}>{t("scopeAll", lang)}</button>
          )}
        </div>
        <button type="button" className="ghost-btn" style={{ width: "100%", marginTop: 10 }} onClick={onClose}>{t("cancelBtn", lang)}</button>
      </div>
    </div>
  );
}

/* =========================================================
   Settings modal
   ========================================================= */
function SettingsModal({ displayName, subscription, settings, onSettingsChange, onRenameUser, onClearData, onUpgradeClick, onLogout, onClose }) {
  const { lang, setLang } = useLang();
  const [name, setName] = useState(displayName);
  const [confirmingClear, setConfirmingClear] = useState(false);
  const [legalDoc, setLegalDoc] = useState(null);
  const isPremium = PREMIUM_ENFORCED ? isPremiumStatus(subscription.status) : true;
  const [permissionState, setPermissionState] = useState(
    typeof Notification !== "undefined" ? Notification.permission : "unsupported"
  );
  const isStandalone =
    (typeof window !== "undefined" && window.navigator && window.navigator.standalone) ||
    (typeof window !== "undefined" && window.matchMedia && window.matchMedia("(display-mode: standalone)").matches);

  useEffect(() => {
    if (typeof Notification === "undefined") return;
    if (settings.notificationsEnabled && Notification.permission !== "granted") {
      onSettingsChange({ ...settings, notificationsEnabled: false });
    }
  }, []);
  const [canInstall, setCanInstall] = useState(!!deferredInstallPrompt);
  const isIOS = typeof navigator !== "undefined" && /iphone|ipad|ipod/i.test(navigator.userAgent);

  useEffect(() => {
    function onAvailable() { setCanInstall(!!deferredInstallPrompt); }
    window.addEventListener("pwa-install-available", onAvailable);
    return () => window.removeEventListener("pwa-install-available", onAvailable);
  }, []);

  async function installApp() {
    if (!deferredInstallPrompt) return;
    deferredInstallPrompt.prompt();
    await deferredInstallPrompt.userChoice;
    deferredInstallPrompt = null;
    setCanInstall(false);
  }

  function toggleNotifications(checked) {
    if (!checked) {
      onSettingsChange({ ...settings, notificationsEnabled: false });
      return;
    }
    if (typeof Notification === "undefined") {
      setPermissionState("unsupported");
      return;
    }
    Notification.requestPermission().then((perm) => {
      setPermissionState(perm);
      onSettingsChange({ ...settings, notificationsEnabled: perm === "granted" });
    });
  }

  function saveName() {
    const trimmed = name.trim();
    if (trimmed && trimmed !== displayName) onRenameUser(trimmed);
  }

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{t("settingsTitle", lang)}</h3>
          <button type="button" className="modal-close" onClick={onClose} aria-label={t("doneBtn", lang)}><Icon name="x" size={16} /></button>
        </div>

        <div className="settings-section">
          <h4>{t("settingsLanguageSection", lang)}</h4>
          <select className="settings-select" value={lang} onChange={(e) => setLang(e.target.value)}>
            {LANGS.map((l) => <option key={l} value={l}>{LANG_NAMES[l]}</option>)}
          </select>
        </div>

        <div className="settings-section">
          <h4>{t("settingsCurrencySection", lang)}</h4>
          <select className="settings-select" value={settings.currency || "EUR"} onChange={(e) => onSettingsChange({ ...settings, currency: e.target.value })}>
            {CURRENCIES.map((c) => <option key={c.code} value={c.code}>{c.name}</option>)}
          </select>
        </div>

        <div className="settings-section">
          <h4>{t("settingsProfileSection", lang)}</h4>
          <label className="field">
            <span>{t("displayNameLabel", lang)}</span>
            <input type="text" value={name} onChange={(e) => setName(e.target.value)} onBlur={saveName} />
          </label>
          <button type="button" className="ghost-btn" style={{ marginTop: 12 }} onClick={onLogout}>
            {t("logout", lang)}
          </button>
        </div>

        <div className="settings-section">
          <h4>{t("settingsPlanSection", lang)}</h4>
          {!PREMIUM_ENFORCED ? (
            <p className="recurring-hint">{t("allFeaturesFreeNotice", lang)}</p>
          ) : (
            <>
              <div className="plan-row">
                <span className={`plan-badge ${isPremium ? "plan-badge--premium" : ""}`}>
                  {isPremium ? t("planPremium", lang) : t("planFree", lang)}
                </span>
                {isPremium && subscription.current_period_end && (
                  <span className="plan-renewal">
                    {t("planRenewsOn", lang)} {new Date(subscription.current_period_end).toLocaleDateString(LOCALE_MAP[lang] || "en-US")}
                  </span>
                )}
              </div>
              {isPremium ? (
                <button type="button" className="ghost-btn" style={{ marginTop: 10 }} onClick={onUpgradeClick}>
                  {t("manageSubscriptionBtn", lang)}
                </button>
              ) : (
                <button type="button" className="submit-btn" style={{ marginTop: 10 }} onClick={onUpgradeClick}>
                  {t("upgradeToPremiumBtn", lang)}
                </button>
              )}
            </>
          )}
        </div>

        {PLANER_FEATURE_ENABLED && (
          <div className="settings-section">
            <h4>{t("settingsPlanerSection", lang)}</h4>
            <label className="recurring-check">
              <input
                type="checkbox"
                checked={!!settings.planerEnabled}
                onChange={(e) => onSettingsChange({ ...settings, planerEnabled: e.target.checked })}
              />
              <span>{t("planerEnableLabel", lang)}</span>
            </label>
            <p className="recurring-hint">{t("planerEnableHint", lang)}</p>
          </div>
        )}

        <div className="settings-section">
          <h4>{t("settingsTranslateSection", lang)}</h4>
          <label className="recurring-check">
            <input
              type="checkbox"
              checked={!!settings.autoTranslateEnabled}
              onChange={(e) => onSettingsChange({ ...settings, autoTranslateEnabled: e.target.checked })}
            />
            <span>{t("autoTranslateLabel", lang)}</span>
          </label>
          <p className="recurring-hint">{t("autoTranslateHint", lang)}</p>
        </div>

        <div className="settings-section">
          <h4>{t("settingsReceiptScanSection", lang)}</h4>
          <label className="recurring-check">
            <input
              type="checkbox"
              checked={!!settings.receiptScanEnabled}
              onChange={(e) => onSettingsChange({ ...settings, receiptScanEnabled: e.target.checked })}
            />
            <span>{t("receiptScanLabel", lang)}</span>
          </label>
          <p className="recurring-hint">{t("receiptScanHint", lang)}</p>
        </div>

        <div className="settings-section">
          <h4>{t("settingsDienstplanScanSection", lang)}</h4>
          <label className="recurring-check">
            <input
              type="checkbox"
              checked={!!settings.dienstplanScanEnabled}
              onChange={(e) => onSettingsChange({ ...settings, dienstplanScanEnabled: e.target.checked })}
            />
            <span>{t("dienstplanScanLabel", lang)}</span>
          </label>
          <p className="recurring-hint">{t("dienstplanScanHint", lang)}</p>
        </div>

        <div className="settings-section">
          <h4>{t("settingsNotifSection", lang)}</h4>
          {!isStandalone && <p className="form-error">{t("notifNeedsInstall", lang)}</p>}
          <label className="recurring-check">
            <input type="checkbox" checked={settings.notificationsEnabled} onChange={(e) => toggleNotifications(e.target.checked)} />
            <span>{t("notifEnableLabel", lang)}</span>
          </label>
          <p className="recurring-hint">{t("notifEnableHint", lang)}</p>
          {permissionState === "denied" && <p className="form-error">{t("notifPermissionDenied", lang)}</p>}
          {permissionState === "unsupported" && <p className="form-error">{t("notifUnsupported", lang)}</p>}

          <label className="field" style={{ marginTop: 14 }}>
            <span>{t("budgetAlertLabel", lang)}</span>
            <select
              value={settings.budgetAlertThreshold}
              onChange={(e) => onSettingsChange({ ...settings, budgetAlertThreshold: Number(e.target.value) })}
            >
              <option value={0}>{t("budgetAlertOff", lang)}</option>
              <option value={80}>80%</option>
              <option value={90}>90%</option>
              <option value={100}>100%</option>
            </select>
          </label>

          <label className={`recurring-check ${!isPremium ? "recurring-check--locked" : ""}`} style={{ marginTop: 14 }}>
            <input
              type="checkbox"
              checked={settings.recurringReminderEnabled}
              disabled={!isPremium}
              onChange={(e) => {
                if (!isPremium) { onUpgradeClick(); return; }
                onSettingsChange({ ...settings, recurringReminderEnabled: e.target.checked });
              }}
            />
            <span>{t("recurringReminderLabel", lang)}</span>
            {!isPremium && <Icon name="crown" size={12} />}
          </label>

          <label className={`recurring-check ${!isPremium ? "recurring-check--locked" : ""}`} style={{ marginTop: 14 }}>
            <input
              type="checkbox"
              checked={settings.spendingSpikeAlertEnabled}
              disabled={!isPremium}
              onChange={(e) => {
                if (!isPremium) { onUpgradeClick(); return; }
                onSettingsChange({ ...settings, spendingSpikeAlertEnabled: e.target.checked });
              }}
            />
            <span>{t("spendingSpikeLabel", lang)}</span>
            {!isPremium && <Icon name="crown" size={12} />}
          </label>
        </div>

        <div className="settings-section">
          <label className="field">
            <span>{t("emergencyBufferLabel", lang)}</span>
            <input
              type="text" inputMode="decimal"
              value={settings.emergencyBuffer || ""}
              onChange={(e) => {
                const v = parseFloat(String(e.target.value).replace(",", "."));
                onSettingsChange({ ...settings, emergencyBuffer: isNaN(v) ? 0 : v });
              }}
              placeholder="0.00"
            />
          </label>
          <p className="recurring-hint">{t("emergencyBufferHint", lang)}</p>
        </div>

        <div className="settings-section">
          <h4>{t("settingsAppearanceSection", lang)}</h4>
          <div className="theme-toggle">
            <button
              type="button"
              className={`theme-option ${settings.themeMode !== "light" ? "theme-option--active" : ""}`}
              onClick={() => onSettingsChange({ ...settings, themeMode: "dark" })}
            >
              <Icon name="moon" size={16} /> {t("themeDarkLabel", lang)}
            </button>
            <button
              type="button"
              className={`theme-option ${settings.themeMode === "light" ? "theme-option--active" : ""}`}
              onClick={() => onSettingsChange({ ...settings, themeMode: "light" })}
            >
              <Icon name="sun" size={16} /> {t("themeLightLabel", lang)}
            </button>
          </div>
        </div>

        <div className="settings-section">
          <h4>{t("settingsInstallSection", lang)}</h4>
          {isStandalone ? (
            <p className="recurring-hint">{t("installAlreadyInstalled", lang)}</p>
          ) : canInstall ? (
            <button type="button" className="submit-btn" onClick={installApp}>
              <Icon name="upload" size={14} /> {t("installNowBtn", lang)}
            </button>
          ) : isIOS ? (
            <p className="recurring-hint">{t("installStepsIOS", lang)}</p>
          ) : (
            <p className="recurring-hint">{t("installStepsGeneric", lang)}</p>
          )}
        </div>

        <div className="settings-section">
          <h4>{t("settingsDataSection", lang)}</h4>
          {!confirmingClear ? (
            <button type="button" className="ghost-btn ghost-btn--danger" onClick={() => setConfirmingClear(true)}>
              <Icon name="trash" size={14} /> {t("clearDataBtn", lang)}
            </button>
          ) : (
            <div className="clear-confirm">
              <p>{t("clearDataConfirm", lang)}</p>
              <div className="quick-actions">
                <button type="button" className="ghost-btn" onClick={() => setConfirmingClear(false)}>{t("cancelBtn", lang)}</button>
                <button
                  type="button" className="submit-btn"
                  style={{ marginTop: 0, flex: 1, background: "var(--red)" }}
                  onClick={() => { onClearData(); setConfirmingClear(false); onClose(); }}
                >
                  {t("clearDataConfirmBtn", lang)}
                </button>
              </div>
            </div>
          )}
        </div>

        <div className="settings-section">
          <h4>{t("settingsLegalSection", lang)}</h4>
          <div className="legal-links">
            <button type="button" className="legal-link-row" onClick={() => setLegalDoc("privacy")}>
              <Icon name="file" size={16} /> <span>{t("legalPrivacyTitle", lang)}</span> <Icon name="chevronRight" size={16} className="legal-link-chevron" />
            </button>
            <button type="button" className="legal-link-row" onClick={() => setLegalDoc("terms")}>
              <Icon name="file" size={16} /> <span>{t("legalTermsTitle", lang)}</span> <Icon name="chevronRight" size={16} className="legal-link-chevron" />
            </button>
            <button type="button" className="legal-link-row" onClick={() => setLegalDoc("impressum")}>
              <Icon name="building" size={16} /> <span>{t("legalImpressumTitle", lang)}</span> <Icon name="chevronRight" size={16} className="legal-link-chevron" />
            </button>
            <a className="legal-link-row" href={`mailto:${SUPPORT_EMAIL}?subject=${encodeURIComponent(t("legalHelpMailSubject", lang))}`}>
              <Icon name="helpCircle" size={16} /> <span>{t("legalHelpBtn", lang)}</span> <Icon name="chevronRight" size={16} className="legal-link-chevron" />
            </a>
            <a className="legal-link-row" href={`mailto:${SUPPORT_EMAIL}`}>
              <Icon name="mail" size={16} /> <span>{t("legalContactBtn", lang)}</span> <Icon name="chevronRight" size={16} className="legal-link-chevron" />
            </a>
            <a className="legal-link-row" href="https://salary-5tv.pages.dev/landing.html" target="_blank" rel="noopener noreferrer">
              <Icon name="star" size={16} /> <span>{t("legalFeedbackBtn", lang)}</span> <Icon name="chevronRight" size={16} className="legal-link-chevron" />
            </a>
          </div>
        </div>

        <button type="button" className="submit-btn" onClick={onClose}>{t("doneBtn", lang)}</button>
      </div>
      {legalDoc && <LegalModal docType={legalDoc} onClose={() => setLegalDoc(null)} />}
    </div>
  );
}

/* =========================================================
   Entry list
   ========================================================= */
function EntryList({ entries, onEdit, onDelete, limit, viewAllLabel }) {
  const { lang } = useLang();
  const [showAll, setShowAll] = useState(false);
  if (entries.length === 0) {
    return <EmptyState icon="receipt" text={t("emptyTransactionsHint", lang)} />;
  }
  const sorted = [...entries].sort((a, b) => b.date.localeCompare(a.date));
  const visible = limit && !showAll ? sorted.slice(0, limit) : sorted;
  return (
    <>
    <ul className="entry-list">
      {visible.map((e) => (
        <li key={e.id} className={`entry-item entry-item--${e.kind}`}>
          <div className="entry-icon" aria-hidden="true"><Icon name={categoryIcon(e.kind, e.category)} size={16} /></div>
          <div className="entry-main">
            <div className="entry-top">
              <span className="entry-label">
                {(e.labelTranslations && e.labelTranslations[lang]) || e.label}
                {e.recurring && <span className="temp-badge temp-badge--recurring">{t("recurringBadge", lang)}</span>}
                {e.expiresAt && <span className="temp-badge">{t("quickBadge", lang)} · {formatRemaining(e.expiresAt, lang)} {t("quickRemaining", lang)}</span>}
              </span>
              <span className={`entry-amount ${e.kind === "income" ? "entry-amount--income" : ""}`}>
                {e.kind === "income" ? "+" : "-"}{currencyFmt(e.amount, lang)}
              </span>
            </div>
            <div className="entry-sub">
              <span>
                {categoryLabel(e.kind, e.category, lang)}
                {e.term && (
                  <span className="term-tag">
                    <Icon name={TERM_META[e.term] ? TERM_META[e.term].icon : "clock"} size={12} />
                    {" "}{t(TERM_META[e.term] ? TERM_META[e.term].key : "termDay", lang)}
                  </span>
                )}
              </span>
              <span className="entry-date">{e.date}</span>
            </div>
          </div>
          <div className="entry-actions">
            <button className="entry-action" onClick={() => onEdit(e)} aria-label={t("editAria", lang)} title={t("editAria", lang)}>
              <Icon name="edit" size={15} />
            </button>
            <button className="entry-action entry-action--danger" onClick={() => onDelete(e)} aria-label={t("deleteAria", lang)} title={t("deleteAria", lang)}>
              <Icon name="trash" size={15} />
            </button>
          </div>
        </li>
      ))}
    </ul>
    {limit && sorted.length > limit && (
      <button type="button" className="view-all-link" onClick={() => setShowAll((v) => !v)}>
        {showAll ? t("showLessBtn", lang) : (viewAllLabel || t("viewAllTransactionsBtn", lang))}
      </button>
    )}
    </>
  );
}

/* =========================================================
   Auth screen
   ========================================================= */
function AuthScreen() {
  const { lang } = useLang();
  const [mode, setMode] = useState("login");
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");
  const [info, setInfo] = useState("");
  const [busy, setBusy] = useState(false);
  const [showPassword, setShowPassword] = useState(false);

  async function submit(e) {
    e.preventDefault();
    setError(""); setInfo("");
    const cleanEmail = email.trim().toLowerCase();

    if (mode === "forgot") {
      if (!cleanEmail) { setError(t("errFillFields", lang)); return; }
      setBusy(true);
      const { error: err } = await supabase.auth.resetPasswordForEmail(cleanEmail);
      setBusy(false);
      if (err) { setError(err.message); return; }
      setInfo(t("forgotPasswordSent", lang));
      return;
    }

    if (!cleanEmail || !password) { setError(t("errFillFields", lang)); return; }
    setBusy(true);

    if (mode === "register") {
      const { error: err } = await supabase.auth.signUp({
        email: cleanEmail,
        password,
        options: { data: { name: name.trim() || cleanEmail.split("@")[0] } },
      });
      setBusy(false);
      if (err) { setError(err.message); return; }
      setInfo(t("registerConfirmEmail", lang));
    } else {
      const { error: err } = await supabase.auth.signInWithPassword({ email: cleanEmail, password });
      setBusy(false);
      if (err) { setError(t("errInvalidCreds", lang)); return; }
    }
  }

  return (
    <div className="auth-screen">
      <div className="auth-card">
        <div className="auth-lang"><LangSwitch /></div>
        <div className="auth-logo"><Icon name="moneyBox" size={26} /></div>
        <h1>{t("appName", lang)}</h1>
        <p>{t("authTaglineFull", lang)}</p>

        {mode !== "forgot" && (
          <div className="auth-tabs">
            <button type="button" className={`auth-tab ${mode === "login" ? "auth-tab--active" : ""}`} onClick={() => { setMode("login"); setError(""); setInfo(""); }}>
              {t("loginTab", lang)}
            </button>
            <button type="button" className={`auth-tab ${mode === "register" ? "auth-tab--active" : ""}`} onClick={() => { setMode("register"); setError(""); setInfo(""); }}>
              {t("registerTab", lang)}
            </button>
          </div>
        )}

        <form onSubmit={submit}>
          {mode === "register" && (
            <label className="auth-field">
              <span>{t("nameLabel", lang)}</span>
              <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder={t("namePlaceholder", lang)} />
            </label>
          )}
          <label className="auth-field">
            <span>{t("emailLabel", lang)}</span>
            <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="you@example.com" required />
          </label>
          {mode !== "forgot" && (
            <label className="auth-field">
              <span>{t("passwordLabel", lang)}</span>
              <div className="password-field-wrap">
                <input
                  type={showPassword ? "text" : "password"}
                  value={password}
                  onChange={(e) => setPassword(e.target.value)}
                  placeholder="••••••••"
                  required
                />
                <button
                  type="button"
                  className="password-toggle-btn"
                  onClick={() => setShowPassword((v) => !v)}
                  aria-label={showPassword ? t("hidePasswordAria", lang) : t("showPasswordAria", lang)}
                  tabIndex={-1}
                >
                  <Icon name={showPassword ? "eyeOff" : "eye"} size={17} />
                </button>
              </div>
            </label>
          )}

          {error && <p className="auth-error">{error}</p>}
          {info && <p className="auth-info">{info}</p>}

          <button type="submit" className="auth-submit" disabled={busy}>
            {mode === "register" && t("registerSubmit", lang)}
            {mode === "login" && t("loginSubmit", lang)}
            {mode === "forgot" && t("sendResetLink", lang)}
          </button>
        </form>

        {mode === "login" && (
          <button type="button" className="auth-forgot-link" onClick={() => { setMode("forgot"); setError(""); setInfo(""); }}>
            {t("forgotPasswordLink", lang)}
          </button>
        )}
        {mode === "forgot" && (
          <button type="button" className="auth-forgot-link" onClick={() => { setMode("login"); setError(""); setInfo(""); }}>
            {t("backToLoginLink", lang)}
          </button>
        )}

        <p className="auth-hint">{t("authHint", lang)}</p>
      </div>
    </div>
  );
}

/* =========================================================
   Planer — voice assistant
   ========================================================= */
const PLANER_STRINGS = {
  launcherTitle: { ar: "بلانر", en: "Planer", fr: "Planer", de: "Planer" },
  launcherSub: { ar: "اضغط للتحدث", en: "Tap to talk", fr: "Touchez pour parler", de: "Zum Sprechen tippen" },
  title: { ar: "بلانر", en: "Planer", fr: "Planer", de: "Planer" },
  subtitle: { ar: "المساعد الصوتي", en: "Voice assistant", fr: "Assistant vocal", de: "Sprachassistent" },
  idleState: { ar: "اضغط على الدائرة وتحدث", en: "Tap the circle and speak", fr: "Appuyez sur le cercle et parlez", de: "Auf den Kreis tippen und sprechen" },
  listeningState: { ar: "كنسمع...", en: "Listening...", fr: "Écoute...", de: "Höre zu..." },
  thinkingState: { ar: "كنفكر...", en: "Thinking...", fr: "Réflexion...", de: "Denke nach..." },
  unsupportedState: { ar: "المتصفح ماكيدعمش التعرف على الصوت", en: "Your browser doesn't support voice recognition", fr: "Votre navigateur ne prend pas en charge la reconnaissance vocale", de: "Ihr Browser unterstützt keine Spracherkennung" },
  micDenied: { ar: "الرجاء السماح باستخدام الميكروفون", en: "Please allow microphone access", fr: "Veuillez autoriser l'accès au microphone", de: "Bitte erlauben Sie den Mikrofonzugriff" },
  errorGeneric: { ar: "حدث خطأ، حاول مرة أخرى", en: "Something went wrong, try again", fr: "Une erreur s'est produite, réessayez", de: "Etwas ist schiefgelaufen, versuchen Sie es erneut" },
  addConfirm: { ar: "زدت", en: "Added", fr: "Ajouté", de: "Hinzugefügt" },
  deleteConfirmQ: { ar: "هل تريد حذف", en: "Delete", fr: "Supprimer", de: "Löschen" },
  deleteConfirmYes: { ar: "نعم، احذف", en: "Yes, delete", fr: "Oui, supprimer", de: "Ja, löschen" },
  deleteConfirmNo: { ar: "لا", en: "No", fr: "Non", de: "Nein" },
  deleteDone: { ar: "تمسحات", en: "Deleted", fr: "Supprimé", de: "Gelöscht" },
  deleteNone: { ar: "لا توجد حركة لحذفها", en: "No entry to delete", fr: "Aucune opération à supprimer", de: "Kein Eintrag zum Löschen" },
  example1: { ar: "أضِف 50 يورو قهوة", en: "Add 50 euros coffee", fr: "Ajoute 50 euros café", de: "Füge 50 Euro Kaffee hinzu" },
  example2: { ar: "كم أنفقت هذا الشهر", en: "How much did I spend this month", fr: "Combien j'ai dépensé ce mois", de: "Wie viel habe ich diesen Monat ausgegeben" },
  example3: { ar: "احذف آخر حركة", en: "Delete last entry", fr: "Supprimer la dernière opération", de: "Letzten Eintrag löschen" },
  cancel: { ar: "إلغاء", en: "Cancel", fr: "Annuler", de: "Abbrechen" },
  typedPlaceholder: { ar: "ولا اكتب سؤالك هنا...", en: "Or type your question here...", fr: "Ou écrivez votre question ici...", de: "Oder tippen Sie hier Ihre Frage..." },
  typedSendBtn: { ar: "صيفط", en: "Send", fr: "Envoyer", de: "Senden" },
  clearChatBtn: { ar: "امسح المحادثة", en: "Clear chat", fr: "Effacer la conversation", de: "Chat löschen" },
};
function pt(key, lang) {
  const entry = PLANER_STRINGS[key];
  if (!entry) return key;
  return entry[lang] || entry.en || key;
}

const SPEECH_LOCALE = { ar: "ar-MA", en: "en-US", fr: "fr-FR", de: "de-DE" };
const ADD_WORDS = ["أضف", "إضافة", "زد", "add", "ajoute", "ajouter", "hinzufügen", "füge"];
const INCOME_WORDS = ["دخل", "راتب", "income", "revenu", "einkommen", "salaire", "gehalt"];
const DELETE_WORDS = ["احذف", "حذف", "امسح", "مسح", "delete", "supprim", "löschen", "loesch", "remove"];

function parseVoiceCommand(text) {
  const lower = text.toLowerCase();
  const numMatch = text.match(/(\d+(?:[.,]\d+)?)/);
  const amount = numMatch ? parseFloat(numMatch[1].replace(",", ".")) : null;

  const isDelete = DELETE_WORDS.some((w) => lower.includes(w));
  if (isDelete) return { action: "delete" };

  const isAdd = ADD_WORDS.some((w) => lower.includes(w));
  if (isAdd && amount) {
    let label = text;
    ADD_WORDS.forEach((w) => { label = label.replace(new RegExp(w, "gi"), ""); });
    if (numMatch) label = label.replace(numMatch[0], "");
    label = label.replace(/€|euro|euros|يورو/gi, "").trim();
    const isIncome = INCOME_WORDS.some((w) => lower.includes(w));
    return { action: "add", amount, kind: isIncome ? "income" : "expense", label: label || null };
  }
  return null;
}
// Finds a specific entry Planer should act on (for delete/update) instead
// of always assuming "the last one added" — lets past-month entries be
// targeted by describing them, e.g. "delete the taxi ride from last
// month". Falls back to the most recently added entry when neither a
// label nor a month was given, matching the original simpler behavior.
function findEntryByQuery(entries, targetLabel, targetMonth) {
  if (!targetLabel && !targetMonth) {
    return entries.length ? entries[entries.length - 1] : null;
  }
  const needle = (targetLabel || "").trim().toLowerCase();
  let candidates = entries;
  if (targetMonth) {
    candidates = candidates.filter((e) => e.date && e.date.slice(0, 7) === targetMonth);
  }
  if (needle) {
    const labelMatches = candidates.filter((e) => (e.label || "").toLowerCase().includes(needle));
    if (labelMatches.length) candidates = labelMatches;
  }
  if (!candidates.length) return null;
  return candidates.slice().sort((a, b) => b.date.localeCompare(a.date))[0];
}

function buildFinancialContext(entries, monthEntries, monthLabelStr, settings, goals, credits, isPremium, shiftSchedules, lang) {
  const income = monthEntries.filter((e) => e.kind === "income").reduce((s, e) => s + e.amount, 0);
  const expenses = monthEntries.filter((e) => e.kind === "expense" && e.category !== "savings").reduce((s, e) => s + e.amount, 0);
  const savings = monthEntries.filter((e) => e.kind === "expense" && e.category === "savings").reduce((s, e) => s + e.amount, 0);
  // Scoped to THIS calendar month only (expanded recurring entries
  // included) — passing the raw, all-time entries list here was the bug
  // behind wildly wrong answers to "how much did I spend this month".
  let out = "For " + monthLabelStr + " (the currently open month) — Income: " + currencyFmt(income, lang) + ", Expenses (excluding savings): " + currencyFmt(expenses, lang) +
    ", Savings & investments: " + currencyFmt(savings, lang) + ", Balance: " + currencyFmt(income - expenses, lang) +
    ", Entry count: " + monthEntries.length + ". Always answer using this exact currency, never a different one.";

  // "Last month" and "this/last year" are common enough questions that
  // it's worth always computing them rather than only ever knowing the
  // one month currently open in the UI — this is what actually lets
  // Planer answer things like "how much did I spend last month?" or
  // "what was my income in 2025?" from real data instead of guessing.
  try {
    const todayKey = todayISO();
    const thisMonthKey = monthKey(todayKey);
    const lastMonthKey = shiftMonth(thisMonthKey, -1);
    const lastMonthEntries = expandEntriesForMonth(entries, lastMonthKey);
    const lastMonthIncome = lastMonthEntries.filter((e) => e.kind === "income").reduce((s, e) => s + e.amount, 0);
    const lastMonthExpenses = lastMonthEntries.filter((e) => e.kind === "expense" && e.category !== "savings").reduce((s, e) => s + e.amount, 0);
    out += " Last month (" + monthLabel(lastMonthKey, lang) + ") — Income: " + currencyFmt(lastMonthIncome, lang) + ", Expenses: " + currencyFmt(lastMonthExpenses, lang) + ".";

    const thisYear = Number(thisMonthKey.slice(0, 4));
    const thisYearSummary = computeYearSummary(entries, thisYear);
    out += " This year (" + thisYear + ") so far — Income: " + currencyFmt(thisYearSummary.totalIncome, lang) + ", Expenses: " + currencyFmt(thisYearSummary.totalExpenses, lang) + ".";
    if (thisYearSummary.projection) {
      out += " Full-year " + thisYear + " PROJECTION (estimated by extrapolating the months seen so far, not actual data for future months) — projected total income: " + currencyFmt(thisYearSummary.projection.projectedTotalIncome, lang) + ", projected total expenses: " + currencyFmt(thisYearSummary.projection.projectedTotalExpenses, lang) + ". If asked about spending/income for the whole year (e.g. \"January to December\") and some of those months are still in the future, give this projection but clearly label it as an estimate, not a fact.";
    }

    const lastYear = thisYear - 1;
    const lastYearSummary = computeYearSummary(entries, lastYear);
    if (lastYearSummary.monthly.some((m) => m.hasData)) {
      out += " Last year (" + lastYear + ") — Income: " + currencyFmt(lastYearSummary.totalIncome, lang) + ", Expenses: " + currencyFmt(lastYearSummary.totalExpenses, lang) + ".";
    }
  } catch (e) {}
  out += " If asked about a month or year not covered above, say you don't have that data rather than guessing.";

  // Give Planer visibility into everything else the app already
  // calculates, so it can actually answer questions about them instead
  // of only knowing raw income/expense totals.
  try {
    const sts = computeSafeToSpend(entries, settings, goals);
    out += " Safe to spend per day: " + currencyFmt(Math.max(0, sts.safeToSpend), lang) + ".";
  } catch (e) {}
  try {
    const upcoming7 = getUpcomingExpenses(entries, 7).reduce((s, e) => s + e.amount, 0);
    out += " Upcoming expenses in the next 7 days: " + currencyFmt(upcoming7, lang) + ".";
  } catch (e) {}
  if (goals && goals.length) {
    out += " Savings goals: " + goals.map((g) => g.name + " (" + currencyFmt(Number(g.currentAmount) || 0, lang) + " of " + currencyFmt(Number(g.targetAmount) || 0, lang) + ")").join(", ") + ".";
  } else {
    out += " No savings goals set yet.";
  }
  if (credits && credits.length) {
    out += " Credits/loans: " + credits.map((c) => c.name + " (" + currencyFmt(Number(c.monthlyPayment) || 0, lang) + "/month)").join(", ") + ".";
  } else {
    out += " No credits/loans tracked.";
  }
  // Dienstplan (shift schedule) awareness — without this, Planer had no
  // way to answer even simple questions like "what's my shift tomorrow"
  // or "how many vacation days do I have this month", and was forced to
  // say "I don't have that data" for anything schedule-related.
  try {
    const todayKey = todayISO();
    const tomorrowKey = addDaysISO(todayKey, 1);
    const todayMonthKey = monthKey(todayKey);
    const todaySchedule = shiftSchedules && shiftSchedules[todayMonthKey];
    function shiftLabelFor(dateISO) {
      const sched = shiftSchedules && shiftSchedules[monthKey(dateISO)];
      const code = sched && sched[String(Number(dateISO.slice(8, 10)))];
      const def = SHIFT_TYPES.find((s) => s.code === code);
      return def ? def.labelKey.replace("shift", "") : null;
    }
    const todayShift = shiftLabelFor(todayKey);
    const tomorrowShift = shiftLabelFor(tomorrowKey);
    if (todayShift || tomorrowShift) {
      out += " Dienstplan — today's shift: " + (todayShift || "not set") + ", tomorrow's shift: " + (tomorrowShift || "not set") + ".";
    } else {
      out += " No Dienstplan entries found for today or tomorrow.";
    }
    if (todaySchedule) {
      const codes = Object.keys(todaySchedule).filter((k) => k !== "notes" && k !== "tags").map((k) => todaySchedule[k]);
      const urlaubCount = codes.filter((c) => c === "urlaub").length;
      const krankCount = codes.filter((c) => c === "krank").length;
      const freiCount = codes.filter((c) => c === "frei").length;
      out += " This month's Dienstplan so far — vacation (Urlaub) days: " + urlaubCount + ", sick (Krank) days: " + krankCount + ", off (Frei) days: " + freiCount + ".";
    }
    const annualVacationDays = Number(settings && settings.shiftCalc && settings.shiftCalc.annualVacationDays) || 0;
    if (annualVacationDays > 0) {
      const usedThisYear = computeVacationDaysUsed(shiftSchedules, thisYear);
      out += " Annual vacation allowance for " + thisYear + ": " + annualVacationDays + " days, used so far: " + usedThisYear + ", remaining: " + Math.max(0, annualVacationDays - usedThisYear) + ".";
    }
  } catch (e) {}
  out += " Premium status: " + (isPremium ? "active" : "free plan") + ".";
  return out;
}

const PLANER_FEATURE_ENABLED = true;
const PREMIUM_ENFORCED = false;

const PLANER_AI_URL = "https://tydoceznnmhgqzyutmmd.supabase.co/functions/v1/planer-ai";
const TRANSLATE_URL = "https://tydoceznnmhgqzyutmmd.supabase.co/functions/v1/translate-text";
const SCAN_RECEIPT_URL = "https://tydoceznnmhgqzyutmmd.supabase.co/functions/v1/scan-receipt";
const SCAN_DIENSTPLAN_URL = "https://tydoceznnmhgqzyutmmd.supabase.co/functions/v1/scan-dienstplan";
const TEXT_TO_SPEECH_URL = "https://tydoceznnmhgqzyutmmd.supabase.co/functions/v1/text-to-speech";

async function scanReceiptImage(imageBase64, accessToken) {
  if (!imageBase64 || !accessToken) return null;
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 25000);
  try {
    const res = await fetch(SCAN_RECEIPT_URL, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": "Bearer " + accessToken,
      },
      body: JSON.stringify({ imageBase64 }),
      signal: controller.signal,
    });
    clearTimeout(timeoutId);
    const data = await res.json();
    if (!res.ok || data.error) return null;
    return data;
  } catch (err) {
    clearTimeout(timeoutId);
    console.error("Receipt scan error:", err);
    return null;
  }
}

// Sends a photo of a printed/handwritten Dienstplan to Groq's vision
// model along with the employee's name and the target month, and gets
// back a { found, days: { "1": "fruh", ... } } mapping — used only to
// PREFILL the review calendar for the person to check and correct before
// anything is actually saved, never applied directly. Explicitly opt-in,
// same cost/privacy tradeoff as receipt scanning and auto-translate.
async function scanDienstplanImage(imageBase64, employeeName, year, monthNum, daysInMonthCount, accessToken) {
  if (!imageBase64 || !accessToken) return null;
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 35000);
  try {
    const res = await fetch(SCAN_DIENSTPLAN_URL, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": "Bearer " + accessToken,
      },
      body: JSON.stringify({
        imageBase64, employeeName, year, month: monthNum, daysInMonth: daysInMonthCount,
      }),
      signal: controller.signal,
    });
    clearTimeout(timeoutId);
    const data = await res.json();
    if (!res.ok || data.error) return null;
    return data;
  } catch (err) {
    clearTimeout(timeoutId);
    console.error("Dienstplan scan error:", err);
    return null;
  }
}

// Fetches natural-sounding speech audio for Planer's replies from
// ElevenLabs (multilingual voice — covers Arabic, English, French, and
// German). Real per-request cost, same tradeoff as the app's other
// Groq-backed features.
async function fetchGroqSpeech(text, lang, accessToken) {
  if (!text || !accessToken) return null;
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 15000);
  try {
    const res = await fetch(TEXT_TO_SPEECH_URL, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": "Bearer " + accessToken,
      },
      body: JSON.stringify({ text, lang }),
      signal: controller.signal,
    });
    clearTimeout(timeoutId);
    const data = await res.json();
    if (!res.ok || data.error) return null;
    if (data.unsupported) return { unsupported: true };
    return data;
  } catch (err) {
    clearTimeout(timeoutId);
    console.error("Text-to-speech error:", err);
    return null;
  }
}

async function translateTexts(texts, targetLang, accessToken) {
  if (!texts.length || !accessToken) return null;
  try {
    const res = await fetch(TRANSLATE_URL, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": "Bearer " + accessToken,
      },
      body: JSON.stringify({ texts, targetLang }),
    });
    const data = await res.json();
    if (!res.ok || data.error || !Array.isArray(data.translations)) return null;
    return data.translations;
  } catch (err) {
    console.error("Translation error:", err);
    return null;
  }
}

// A small two-blinking-eyes mark for Planer — matches the app's own
// splash-screen mascot instead of a generic microphone icon, so the
// assistant reads as "the app's own face" rather than a stock icon.
// Size-driven so it can stand in for the mic icon at any of its call
// sites (launcher pill, panel header, big orb button).
function PlanerEyesMark({ size = 22 }) {
  const eyeW = Math.round(size * 0.24);
  const eyeH = Math.round(size * 0.34);
  const gap = Math.round(size * 0.22);
  return (
    <span className="planer-eyes" style={{ gap, flexDirection: "row" }} aria-hidden="true">
      <span className="planer-eye" style={{ width: eyeW, height: eyeH }} />
      <span className="planer-eye" style={{ width: eyeW, height: eyeH }} />
    </span>
  );
}

function PlanerAssistant({ entries, monthEntries, monthLabelText, lang, onAddEntry, onDeleteLastEntry, onUpdateLastEntryAmount, onSetShift, onChangeSetting, currentSettings, goals, onAddGoal, onFindGoalByName, onDeleteGoalById, credits, onAddCredit, onFindCreditByName, onDeleteCreditById, isPremium, shiftSchedules, chatHistory, onAppendChat, onClearChat, session }) {
  const [open, setOpen] = useState(false);
  // Lets the person drag the floating launcher wherever they'd rather it
  // sit (e.g. the middle of the screen) instead of it being pinned to one
  // fixed corner — position is remembered across visits once moved.
  const [launcherPos, setLauncherPos] = useState(() => {
    try {
      const raw = localStorage.getItem("salary-planner:planer-launcher-pos:v1");
      return raw ? JSON.parse(raw) : null;
    } catch { return null; }
  });
  const dragStateRef = useRef(null);
  const launcherRef = useRef(null);
  const chatScrollRef = useRef(null);
  useEffect(() => {
    if (chatScrollRef.current) {
      chatScrollRef.current.scrollTop = chatScrollRef.current.scrollHeight;
    }
  }, [chatHistory]);

  function onLauncherPointerDown(e) {
    const rect = launcherRef.current.getBoundingClientRect();
    dragStateRef.current = {
      startX: e.clientX, startY: e.clientY,
      originLeft: rect.left, originTop: rect.top,
      moved: false,
    };
    try { launcherRef.current.setPointerCapture(e.pointerId); } catch (err) {}
  }
  function onLauncherPointerMove(e) {
    const drag = dragStateRef.current;
    if (!drag) return;
    const dx = e.clientX - drag.startX;
    const dy = e.clientY - drag.startY;
    if (!drag.moved && Math.hypot(dx, dy) < 6) return;
    drag.moved = true;
    const w = launcherRef.current.offsetWidth;
    const h = launcherRef.current.offsetHeight;
    const maxLeft = window.innerWidth - w - 6;
    const maxTop = window.innerHeight - h - 6;
    const left = Math.min(Math.max(6, drag.originLeft + dx), Math.max(6, maxLeft));
    const top = Math.min(Math.max(6, drag.originTop + dy), Math.max(6, maxTop));
    setLauncherPos({ left, top });
  }
  function onLauncherPointerUp(e) {
    const drag = dragStateRef.current;
    dragStateRef.current = null;
    try { launcherRef.current.releasePointerCapture(e.pointerId); } catch (err) {}
    if (drag && drag.moved) {
      setLauncherPos((pos) => {
        try { localStorage.setItem("salary-planner:planer-launcher-pos:v1", JSON.stringify(pos)); } catch (err) {}
        return pos;
      });
    } else {
      // A tap, not a drag — open the panel as usual.
      setOpen(true);
    }
  }

  const [uiState, setUiState] = useState("idle");
  const [heard, setHeard] = useState("");
  const [typedText, setTypedText] = useState("");
  const [answer, setAnswer] = useState("");
  const [pendingDelete, setPendingDelete] = useState(null);
  const recognitionRef = useRef(null);
  const synthRef = useRef(typeof window !== "undefined" ? window.speechSynthesis : null);
  const audioRef = useRef(typeof window !== "undefined" ? new Audio() : null);
  const supported = typeof window !== "undefined" && !!(window.SpeechRecognition || window.webkitSpeechRecognition);

  // Mirrors of the frequently-changing props, read by handleTranscript via
  // .current instead of closing over the props directly. This lets the
  // speech-recognition object below be created once per language rather
  // than being torn down and rebuilt every time entries/session change —
  // which, since Planer's own "add"/"delete" actions change entries,
  // meant the mic could get torn down mid-conversation and silently fail
  // to restart for the very next question.
  const entriesRef = useRef(entries);
  const monthEntriesRef = useRef(monthEntries);
  const monthLabelTextRef = useRef(monthLabelText);
  const sessionRef = useRef(session);
  const currentSettingsRef = useRef(currentSettings);
  const goalsRef = useRef(goals);
  const creditsRef = useRef(credits);
  const isPremiumRef = useRef(isPremium);
  const shiftSchedulesRef = useRef(shiftSchedules);
  const chatHistoryRef = useRef(chatHistory);
  useEffect(() => { entriesRef.current = entries; }, [entries]);
  useEffect(() => { monthEntriesRef.current = monthEntries; }, [monthEntries]);
  useEffect(() => { monthLabelTextRef.current = monthLabelText; }, [monthLabelText]);
  useEffect(() => { sessionRef.current = session; }, [session]);
  useEffect(() => { currentSettingsRef.current = currentSettings; }, [currentSettings]);
  useEffect(() => { goalsRef.current = goals; }, [goals]);
  useEffect(() => { creditsRef.current = credits; }, [credits]);
  useEffect(() => { isPremiumRef.current = isPremium; }, [isPremium]);
  useEffect(() => { shiftSchedulesRef.current = shiftSchedules; }, [shiftSchedules]);
  useEffect(() => { chatHistoryRef.current = chatHistory; }, [chatHistory]);

  // Stop any in-flight recognition if the assistant unmounts mid-listen.
  useEffect(() => {
    return () => { try { recognitionRef.current && recognitionRef.current.stop(); } catch (e) {} };
  }, []);

  // Picks the best-sounding installed voice for the target language —
  // used only as the fallback path (French/German, or if Groq's TTS is
  // unreachable) since the browser's own voices are a clear step down
  // from Groq's.
  function pickBestVoice(targetLang) {
    if (!synthRef.current) return null;
    const locale = SPEECH_LOCALE[targetLang] || "en-US";
    const shortCode = locale.split("-")[0];
    const voices = synthRef.current.getVoices() || [];
    if (!voices.length) return null;
    const exact = voices.filter((v) => v.lang === locale);
    const sameLang = voices.filter((v) => v.lang && v.lang.split("-")[0] === shortCode);
    const pool = exact.length ? exact : sameLang;
    if (!pool.length) return null;
    const premium = pool.find((v) => /enhanced|premium|neural/i.test(v.name));
    return premium || pool[0];
  }

  // Safari on iOS only allows audio (speechSynthesis or an <audio>
  // element) to actually produce sound when triggered synchronously
  // inside a real user gesture — the real reply always arrives after an
  // async network round-trip, well outside that window, so both were
  // getting silently blocked. Priming both right here, inside the tap
  // handler that calls this, "unlocks" audio for the rest of this
  // interaction so the later real playback actually plays.
  // Lightweight audit trail for what Planer actually did — not a full
  // admin screen, but every successful action is written to this user's
  // own localStorage (capped at the last 100) with what happened and
  // when, so there's a real record to check rather than just trusting
  // the spoken confirmation. Scoped to the session's own user id, same
  // as the rest of the app's per-user local caches.
  function logPlannerAction(action, summary) {
    if (!session || !session.user) return;
    try {
      const key = "salary-planner:planer-audit-log:v1:" + session.user.id;
      const raw = localStorage.getItem(key);
      const list = raw ? JSON.parse(raw) : [];
      list.unshift({ action, summary, at: new Date().toISOString() });
      localStorage.setItem(key, JSON.stringify(list.slice(0, 100)));
      console.log("Planer action:", action, "—", summary);
    } catch (e) {}
  }

  function unlockSpeech() {
    if (synthRef.current) {
      try {
        const unlock = new SpeechSynthesisUtterance(" ");
        unlock.volume = 0;
        synthRef.current.speak(unlock);
      } catch (unlockErr) {}
    }
    if (audioRef.current) {
      try {
        audioRef.current.src = "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=";
        audioRef.current.volume = 0;
        const p = audioRef.current.play();
        if (p && p.catch) p.catch(() => {});
      } catch (unlockErr) {}
    }
  }

  function speakBrowser(text) {
    if (!synthRef.current) {
      // Nothing can play speech at all here — go straight back to idle
      // instead of leaving the UI stuck on "speaking" forever, which
      // was part of why the mic looked unresponsive on the next question.
      setUiState("idle");
      return;
    }
    synthRef.current.cancel();
    const utterance = new SpeechSynthesisUtterance(ttsFriendlyText(text));
    utterance.lang = SPEECH_LOCALE[lang] || "en-US";
    const voice = pickBestVoice(lang);
    if (voice) utterance.voice = voice;
    utterance.rate = 0.95;
    utterance.pitch = 1.0;
    utterance.onend = () => setUiState((s) => (s === "speaking" ? "idle" : s));
    utterance.onerror = () => setUiState((s) => (s === "speaking" ? "idle" : s));
    synthRef.current.speak(utterance);
  }

  // Tries ElevenLabs' natural-sounding multilingual voice first (covers
  // all four app languages); a failed request or no session falls back
  // to the browser's built-in voice so Planer is never completely silent.
  // TTS engines can badly mispronounce numbers when a currency symbol or
  // code sits right next to the digits (e.g. "50,00 €" or "145 MAD") —
  // stripping those symbols before sending text to speech (while leaving
  // the on-screen text with them, since that's still useful to read)
  // lets the bare number come through naturally instead.
  // TTS engines can badly mispronounce numbers when a currency symbol or
  // code sits right next to the digits (e.g. "50,00 €" or "145 MAD") —
  // stripping those symbols before sending text to speech (while leaving
  // the on-screen text with them, since that's still useful to read)
  // lets the bare number come through naturally instead. Converting any
  // Arabic-Indic digits (١٢٣) to plain Western digits removes a second,
  // independent source of confusion for the TTS engine.
  //
  // Note: this used to also wrap every number in Unicode directional-
  // isolate marks (LRI...PDI) to stop Groq's Orpheus TTS from visually
  // reordering digits embedded in Arabic text. ElevenLabs' multilingual
  // model doesn't have that bug and, it turns out, reads noticeably
  // worse with those invisible control characters injected around every
  // number — so that step was removed now that ElevenLabs is the engine.
  const ARABIC_INDIC_DIGITS = "٠١٢٣٤٥٦٧٨٩";
  function normalizeDigits(str) {
    return str.replace(/[٠-٩]/g, (d) => String(ARABIC_INDIC_DIGITS.indexOf(d)));
  }
  // Converting numbers to written-out words before they reach the TTS
  // engine sidesteps the whole "does this particular voice/model read
  // digits correctly" question entirely — it did with Groq's older
  // Orpheus voice, then broke again on every ElevenLabs model tried
  // (multilingual_v2, flash_v2_5, turbo_v2_5), so the fix that actually
  // holds regardless of which provider is behind this is spelling
  // numbers out as words ourselves, in the app's own text, before speech
  // is ever requested.
  function numberToWordsEN(n) {
    if (n === 0) return "zero";
    const ones = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
      "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"];
    const tens = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"];
    function threeDigits(num) {
      let s = "";
      if (num >= 100) { s += ones[Math.floor(num / 100)] + " hundred "; num %= 100; }
      if (num >= 20) { s += tens[Math.floor(num / 10)] + (num % 10 ? "-" + ones[num % 10] : ""); }
      else if (num > 0) { s += ones[num]; }
      return s.trim();
    }
    const scales = ["", " thousand", " million", " billion"];
    let parts = [], scaleIdx = 0, rest = n;
    while (rest > 0) {
      const chunk = rest % 1000;
      if (chunk) parts.unshift(threeDigits(chunk) + scales[scaleIdx]);
      rest = Math.floor(rest / 1000);
      scaleIdx++;
    }
    return parts.join(" ").trim();
  }
  function numberToWordsFR(n) {
    if (n === 0) return "zéro";
    const ones = ["", "un", "deux", "trois", "quatre", "cinq", "six", "sept", "huit", "neuf", "dix",
      "onze", "douze", "treize", "quatorze", "quinze", "seize", "dix-sept", "dix-huit", "dix-neuf"];
    function twoDigits(num) {
      if (num < 20) return ones[num];
      if (num < 70) {
        const t = Math.floor(num / 10), u = num % 10;
        const tensWords = ["", "", "vingt", "trente", "quarante", "cinquante", "soixante"];
        if (u === 1 && t !== 8) return tensWords[t] + " et un";
        return tensWords[t] + (u ? "-" + ones[u] : "");
      }
      if (num < 80) return "soixante-" + (num - 60 === 11 ? "onze" : twoDigits(num - 60));
      if (num < 100) {
        const u = num - 80;
        if (u === 0) return "quatre-vingts";
        return "quatre-vingt-" + twoDigits(u);
      }
      return "";
    }
    function threeDigits(num) {
      let s = "";
      if (num >= 100) {
        const h = Math.floor(num / 100);
        s += (h > 1 ? ones[h] + " " : "") + "cent" + (h > 1 && num % 100 === 0 ? "s" : "") + " ";
        num %= 100;
      }
      if (num > 0) s += twoDigits(num);
      return s.trim();
    }
    const scales = ["", " mille", " million", " milliard"];
    let parts = [], scaleIdx = 0, rest = n;
    while (rest > 0) {
      const chunk = rest % 1000;
      if (chunk) parts.unshift((chunk === 1 && scaleIdx === 1 ? "" : threeDigits(chunk)) + scales[scaleIdx]);
      rest = Math.floor(rest / 1000);
      scaleIdx++;
    }
    return parts.join(" ").trim();
  }
  function numberToWordsDE(n) {
    if (n === 0) return "null";
    const ones = ["", "eins", "zwei", "drei", "vier", "fünf", "sechs", "sieben", "acht", "neun", "zehn",
      "elf", "zwölf", "dreizehn", "vierzehn", "fünfzehn", "sechzehn", "siebzehn", "achtzehn", "neunzehn"];
    const tens = ["", "", "zwanzig", "dreißig", "vierzig", "fünfzig", "sechzig", "siebzig", "achtzig", "neunzig"];
    function threeDigits(num) {
      let s = "";
      if (num >= 100) { s += (Math.floor(num / 100) > 1 ? ones[Math.floor(num / 100)] : "") + "hundert"; num %= 100; }
      if (num >= 20) {
        const u = num % 10;
        s += (u ? (u === 1 ? "ein" : ones[u]) + "und" : "") + tens[Math.floor(num / 10)];
      } else if (num > 0) {
        s += ones[num];
      }
      return s;
    }
    const scales = ["", " tausend", " million", " milliarde"];
    let parts = [], scaleIdx = 0, rest = n;
    while (rest > 0) {
      const chunk = rest % 1000;
      if (chunk) parts.unshift(threeDigits(chunk) + scales[scaleIdx]);
      rest = Math.floor(rest / 1000);
      scaleIdx++;
    }
    return parts.join(" ").trim();
  }
  // A practical (not grammatically exhaustive — Arabic number/noun
  // agreement is genuinely complex) MSA number-word conversion, good
  // enough for TTS to read amounts, years, and percentages correctly,
  // which is the actual goal here rather than textbook-perfect grammar.
  function numberToWordsAR(n) {
    if (n === 0) return "صفر";
    const ones = ["", "واحد", "اثنان", "ثلاثة", "أربعة", "خمسة", "ستة", "سبعة", "ثمانية", "تسعة", "عشرة",
      "أحد عشر", "اثنا عشر", "ثلاثة عشر", "أربعة عشر", "خمسة عشر", "ستة عشر", "سبعة عشر", "ثمانية عشر", "تسعة عشر"];
    const tens = ["", "", "عشرون", "ثلاثون", "أربعون", "خمسون", "ستون", "سبعون", "ثمانون", "تسعون"];
    function threeDigits(num) {
      let parts = [];
      if (num >= 100) {
        const h = Math.floor(num / 100);
        parts.push(h === 1 ? "مئة" : h === 2 ? "مئتان" : ones[h] + " مئة");
        num %= 100;
      }
      if (num >= 20) {
        const u = num % 10;
        parts.push(u ? ones[u] + " و" + tens[Math.floor(num / 10)] : tens[Math.floor(num / 10)]);
      } else if (num > 0) {
        parts.push(ones[num]);
      }
      return parts.join(" و");
    }
    const scales = ["", " ألف", " مليون", " مليار"];
    let parts = [], scaleIdx = 0, rest = n;
    while (rest > 0) {
      const chunk = rest % 1000;
      if (chunk) parts.unshift(threeDigits(chunk) + scales[scaleIdx]);
      rest = Math.floor(rest / 1000);
      scaleIdx++;
    }
    return parts.join(" و ").trim();
  }
  function numberToWords(n, targetLang) {
    const intPart = Math.floor(Math.abs(n));
    if (targetLang === "fr") return numberToWordsFR(intPart);
    if (targetLang === "de") return numberToWordsDE(intPart);
    if (targetLang === "ar") return numberToWordsAR(intPart);
    return numberToWordsEN(intPart);
  }
  // Finds number-looking tokens (plain integers, decimals with . or ,
  // as separator, and thousand-separated numbers) and replaces each with
  // its spelled-out word form. A decimal part is read as its own two/one
  // -digit number after "point"/"virgule"/"komma"/"فاصلة", which reads
  // naturally for currency amounts like 50.00 or 12,50.
  function expandNumbersInText(str, targetLang) {
    const pointWord = { en: "point", fr: "virgule", de: "Komma", ar: "فاصلة" }[targetLang] || "point";
    // Separators seen across locales for grouping thousands: plain
    // space, non-breaking space, narrow no-break space (the one French/
    // some European currency formatting actually uses), plus . and , —
    // the original version only recognized . and , as separators, so a
    // space-grouped number like "1 234" left the leading "1" as a raw,
    // unconverted digit sitting right next to the spelled-out "234",
    // which is exactly the "part garbled, part fine" sound reported.
    const sepClass = "[.,\\s\u00A0\u202F]";
    // Two alternatives, tried in this order: a properly thousand-grouped
    // number (needs at least one separator-delimited 3-digit group,
    // e.g. "1 234" or "8.925"), otherwise a plain ungrouped digit run of
    // any length. Without that second branch, a plain "*" quantifier on
    // the grouped part meant `\d{1,3}` alone capped a bare number like
    // "2026" at its first 3 digits ("202"), leaving the trailing "6" to
    // match separately as its own number — which is exactly why a single
    // spoken number was coming out part-words, part-raw-digit.
    const numberPattern = new RegExp(
      "\\d{1,3}(?:" + sepClass + "\\d{3})+(?:" + sepClass + "\\d+)?" + "|" + "\\d+(?:" + sepClass + "\\d+)?",
      "g"
    );
    return str.replace(numberPattern, (match) => {
      // Find the LAST separator of any kind to decide whether it's a
      // decimal point (1-2 trailing digits) or just another thousands
      // group (exactly 3 trailing digits) — same disambiguation as
      // before, just checking every separator type instead of only . and ,.
      let lastSepIdx = -1;
      for (let i = match.length - 1; i >= 0; i--) {
        if (/[.,\s\u00A0\u202F]/.test(match[i])) { lastSepIdx = i; break; }
      }
      let intStr = match, decStr = null;
      if (lastSepIdx !== -1) {
        const trailingDigits = match.length - lastSepIdx - 1;
        if (trailingDigits > 0 && trailingDigits <= 2) {
          intStr = match.slice(0, lastSepIdx);
          decStr = match.slice(lastSepIdx + 1);
        }
      }
      intStr = intStr.replace(/[.,\s\u00A0\u202F]/g, "");
      const intNum = parseInt(intStr, 10);
      if (isNaN(intNum)) return match;
      let words = numberToWords(intNum, targetLang);
      if (decStr && parseInt(decStr, 10) > 0) {
        words += " " + pointWord + " " + numberToWords(parseInt(decStr, 10), targetLang);
      }
      return words;
    });
  }

  function ttsFriendlyText(text) {
    let out = text
      .replace(/[€$£¥₹]/g, "")
      .replace(/\b(MAD|USD|SAR|AED|EUR|GBP|EGP|TND|DZD|QAR|KWD|BHD|OMR|CHF|SEK|NOK|DKK|PLN|CZK|TRY|JPY|CNY|INR|KRW|IDR|MYR|PHP|THB|PKR|CAD|MXN|BRL|ARS|COP|CLP|ZAR|NGN|GAD)\b/g, "")
      .replace(/\s{2,}/g, " ")
      .trim();
    out = normalizeDigits(out);
    out = expandNumbersInText(out, lang);
    return out;
  }

  async function speak(text) {
    if (!text) return;
    if (session && session.access_token) {
      const result = await fetchGroqSpeech(ttsFriendlyText(text), lang, session.access_token);
      if (result && result.audioBase64 && audioRef.current) {
        try {
          audioRef.current.pause();
          audioRef.current.src = "data:" + (result.mimeType || "audio/wav") + ";base64," + result.audioBase64;
          audioRef.current.volume = 1;
          // Without resetting uiState here, it stayed stuck on "speaking"
          // after the audio actually finished — the mic button isn't
          // disabled in that state, but the panel looked "busy" and the
          // person reasonably assumed a second question wouldn't register.
          audioRef.current.onended = () => setUiState((s) => (s === "speaking" ? "idle" : s));
          audioRef.current.onerror = () => setUiState((s) => (s === "speaking" ? "idle" : s));
          const p = audioRef.current.play();
          if (p && p.catch) p.catch(() => speakBrowser(text));
          return;
        } catch (playErr) {
          speakBrowser(text);
          return;
        }
      }
    }
    speakBrowser(text);
  }

  async function handleTranscript(text) {
    setUiState("thinking");
    setAnswer("");

    // Read the latest values via refs rather than the closed-over props —
    // this function is called from a speech-recognition instance that,
    // now, is created once per language instead of being torn down and
    // rebuilt on every entries/session change (see the refs above).
    const session = sessionRef.current;
    const entries = entriesRef.current;
    const monthEntries = monthEntriesRef.current;
    const monthLabelText = monthLabelTextRef.current;
    const currentSettings = currentSettingsRef.current;
    const goals = goalsRef.current;
    const credits = creditsRef.current;
    const isPremium = isPremiumRef.current;
    const shiftSchedules = shiftSchedulesRef.current;
    const chatHistory = chatHistoryRef.current;

    // The user's own message goes into the permanent transcript right
    // away, before the reply comes back — so the chat reads naturally
    // (their line appears, then Planer's) instead of only showing up
    // once everything is resolved.
    onAppendChat("user", text);

    try {
      if (!session || !session.access_token) {
        throw new Error("not signed in");
      }
      const context = buildFinancialContext(entries, monthEntries, monthLabelText, currentSettings, goals, credits, isPremium, shiftSchedules, lang);
      const todayRealISO = todayISO();
      // Recent turns (both sides) so Planer can follow up on what was
      // just discussed ("and yesterday?", "make it 60 instead") instead
      // of treating every message as a cold start — this is genuinely
      // new since chat history didn't persist across turns before at all.
      const recentTurns = (chatHistory || []).slice(-8)
        .map((m) => (m.role === "user" ? "User: " : "Planer: ") + m.text)
        .join("\n");
      // A single structured call replaces the old keyword/regex matcher —
      // the model classifies the request, resolves any date reference
      // ("next month", "the 5th", "tomorrow") against today's real date,
      // and extracts the amount/label/shift/setting itself, so it
      // understands natural phrasing in any of the app's languages.
      const prompt =
        "You are Planer, the financial assistant built into the PARAPLANER budgeting app. " +
        "Today's real date is " + todayRealISO + " (YYYY-MM-DD). Current settings — language: " + lang + ", currency: " + ((currentSettings && currentSettings.currency) || "EUR") + ", theme: " + ((currentSettings && currentSettings.themeMode) || "dark") + ", notifications: " + !!(currentSettings && currentSettings.notificationsEnabled) + ". " +
        "Current savings goals: " + ((goals && goals.length) ? goals.map((g) => g.name + " (" + currencyFmt(Number(g.currentAmount) || 0, lang) + "/" + currencyFmt(Number(g.targetAmount) || 0, lang) + ")").join(", ") : "none yet") + ". " +
        "Current credits/loans: " + ((credits && credits.length) ? credits.map((c) => c.name + " (" + currencyFmt(Number(c.monthlyPayment) || 0, lang) + "/month)").join(", ") : "none yet") + ". " +
        (recentTurns ? ("Recent conversation, for context (most recent last) — use it to resolve follow-ups like \"and for last month?\" or \"make it 60 instead\", but only classify and act on the NEW message below, not on anything already handled earlier in this history:\n" + recentTurns + "\n") : "") +
        "Classify the user's spoken request and reply with ONLY a JSON object, no markdown, no extra text, in this exact shape: " +
        '{"action": "add" | "delete" | "update" | "shift" | "setting" | "addGoal" | "deleteGoal" | "addCredit" | "deleteCredit" | "answer", "kind": "expense" | "income" | null, "amount": number or null, "label": "string or null", "date": "YYYY-MM-DD or null", "targetLabel": "string or null", "targetMonth": "YYYY-MM or null", "shiftCode": "fruh" | "spat" | "nacht" | "frei" | "urlaub" | "krank" | null, "settingKey": "language" | "currency" | "theme" | "notifications" | "emergencyBuffer" | "budgetAlertThreshold" | "recurringReminder" | "spendingSpikeAlert" | "autoTranslate" | "receiptScan" | "dienstplanScan" | "displayName" | null, "settingValue": "string, number, boolean, or null", "goalName": "string or null", "goalTarget": number or null, "goalMonthly": number or null, "creditName": "string or null", "creditTotal": number or null, "creditMonthly": number or null, "reply": "string"}. ' +
        'Use "add" to record a new expense or income — extract the amount, a short label (translate it into ' + lang + ' if said in another language). For "date": resolve any timing they mentioned relative to today\'s real date above — this works for the past just as much as the future (e.g. "last month", "in March", "on the 5th of last month" all resolve to a real past date; "next month", "tomorrow" resolve forward). If they did NOT mention any timing at all, "date" MUST be today\'s real date exactly as given above — never guess. ' +
        'Use "update" to change the amount of an entry — if they described which one (by what it was for, and optionally which month), put a short version of that description in "targetLabel" and, if a month was mentioned, "targetMonth" as YYYY-MM; if they said nothing more specific than "it" or gave no description, leave both null and the most recently added entry will be used. Put the new final amount in "amount". ' +
        'Use "delete" the same way — set "targetLabel"/"targetMonth" when they describe a specific past entry to remove (from any month, not just the current one), or leave both null to mean the most recently added entry. ' +
        'Use "shift" when they want to set, change, or clear a work shift on the Dienstplan for a given day — resolve "date" the same way as above, and map what they said to "shiftCode": fruh = an early/morning shift (Frühdienst), spat = a late/afternoon shift (Spätdienst), nacht = a night shift (Nachtdienst), frei = a day off, urlaub = vacation/leave, krank = sick. ' +
        'Use "setting" when they want to change how the app itself behaves: "settingKey": "language" (settingValue one of ar/en/fr/de), "currency" (settingValue a 3-letter ISO currency code matching what they asked for, e.g. MAD, USD, SAR, GBP), "theme" (settingValue "dark" or "light"), "notifications" (settingValue true/false), "emergencyBuffer" (settingValue the amount to always keep aside, a number), "budgetAlertThreshold" (settingValue a percentage 0-100, or 0 to turn off), "recurringReminder" (settingValue true/false — Premium-only reminder for recurring bills), "spendingSpikeAlert" (settingValue true/false — Premium-only), "autoTranslate" (settingValue true/false), "receiptScan" (settingValue true/false), "dienstplanScan" (settingValue true/false), or "displayName" (settingValue the new name as a string). ' +
        'Use "addGoal" when they want to create a new savings goal — "goalName" a short name (translated into ' + lang + ' if said in another language), "goalTarget" the target amount, and "goalMonthly" the monthly saving amount if they mentioned one (otherwise null). ' +
        'Use "deleteGoal" when they want to remove an existing goal — put the goal\'s name (as close as possible to how they said it) in "goalName" so it can be matched against the list above. ' +
        'Use "addCredit" when they want to track a new credit/loan — "creditName" a short name, "creditTotal" the total price, and "creditMonthly" the monthly payment if they mentioned one (otherwise null; if they only gave the total and a duration in months instead, compute creditMonthly = creditTotal / months yourself). ' +
        'Use "deleteCredit" when they want to remove an existing credit — put its name (as close as possible to how they said it) in "creditName" so it can be matched against the list above. ' +
        'Use "answer" for anything else (questions about their budget, general advice, or anything unclear) — put your full answer in "reply". ' +
        "For \"answer\", speak like a calm, knowledgeable personal finance advisor — clear, warm, to the point, in 2-3 short sentences, referencing concrete numbers from the data below when relevant instead of speaking in generalities. If the question can't be answered from the data given, say so plainly rather than guessing. " +
        "For every action, also fill \"reply\" with a short natural confirmation sentence in the same language, e.g. confirming what was added, changed, deleted, or scheduled. " +
        "Always reply in this language: " + lang + ". " +
        "User's current numbers — " + context + ". " +
        'What they said: "' + text + '"';

      // Without a timeout, a slow or hung Groq response leaves the person
      // staring at "Thinking..." indefinitely with no feedback.
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), 20000);
      let res;
      try {
        res = await fetch(PLANER_AI_URL, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "Authorization": "Bearer " + session.access_token,
          },
          body: JSON.stringify({ prompt: prompt, expectJson: true }),
          signal: controller.signal,
        });
      } finally {
        clearTimeout(timeoutId);
      }
      const data = await res.json();
      if (!res.ok || data.error) {
        throw new Error(data.error || "request failed");
      }

      const targetDate = (data.date && /^\d{4}-\d{2}-\d{2}$/.test(data.date)) ? data.date : todayRealISO;

      if (data.action === "add" && data.amount) {
        const entry = {
          id: makeId(),
          kind: data.kind === "income" ? "income" : "expense",
          date: targetDate,
          category: data.kind === "income" ? "other_income" : "other",
          term: data.kind === "income" ? null : "day",
          label: data.label || (data.kind === "income" ? "Voice income" : "Voice expense"),
          amount: Number(data.amount),
          recurring: false,
          endMonth: null,
          exceptions: {},
        };
        onAddEntry(entry);
        logPlannerAction("add", entry.label + " " + currencyFmt(entry.amount, lang) + " (" + targetDate + ")");
        const msg = data.reply || (pt("addConfirm", lang) + ": " + entry.label + " — " + currencyFmt(entry.amount, lang));
        onAppendChat("assistant", msg);
        setUiState("speaking");
        speak(msg);
        return;
      }

      if (data.action === "update" && data.amount) {
        const target = findEntryByQuery(entries, data.targetLabel, data.targetMonth);
        if (!target) {
          const msg = pt("deleteNone", lang);
          onAppendChat("assistant", msg);
          setUiState("speaking");
          speak(msg);
          return;
        }
        onUpdateLastEntryAmount(target.id, Number(data.amount));
        logPlannerAction("update", target.label + " -> " + currencyFmt(Number(data.amount), lang));
        const msg = data.reply || (target.label + " — " + currencyFmt(Number(data.amount), lang));
        onAppendChat("assistant", msg);
        setUiState("speaking");
        speak(msg);
        return;
      }

      if (data.action === "delete") {
        const target = findEntryByQuery(entries, data.targetLabel, data.targetMonth);
        if (!target) {
          const msg = pt("deleteNone", lang);
          onAppendChat("assistant", msg);
          setUiState("speaking");
          speak(msg);
          return;
        }
        onAppendChat("assistant", pt("deleteConfirmQ", lang) + ": " + target.label + " — " + currencyFmt(target.amount, lang) + "?");
        setPendingDelete({ type: "entry", id: target.id, label: target.label, amount: target.amount });
        setUiState("confirm-delete");
        return;
      }

      if (data.action === "shift" && data.shiftCode) {
        onSetShift(targetDate, data.shiftCode);
        logPlannerAction("shift", targetDate + " -> " + data.shiftCode);
        const shiftDef = SHIFT_TYPES.find((s) => s.code === data.shiftCode);
        const msg = data.reply || (shortDateLabel(targetDate, lang) + " — " + (shiftDef ? t(shiftDef.labelKey, lang) : data.shiftCode));
        onAppendChat("assistant", msg);
        setUiState("speaking");
        speak(msg);
        return;
      }

      if (data.action === "setting" && data.settingKey) {
        const applied = onChangeSetting(data.settingKey, data.settingValue);
        if (applied) logPlannerAction("setting", data.settingKey + " -> " + data.settingValue);
        const msg = applied ? (data.reply || pt("addConfirm", lang)) : pt("errorGeneric", lang);
        onAppendChat("assistant", msg);
        setUiState("speaking");
        speak(msg);
        return;
      }

      if (data.action === "addGoal" && data.goalName && data.goalTarget) {
        const ok = onAddGoal({
          name: data.goalName,
          targetAmount: Number(data.goalTarget),
          currentAmount: 0,
          monthlySaving: data.goalMonthly ? Number(data.goalMonthly) : 0,
        });
        if (ok) logPlannerAction("addGoal", data.goalName + " " + currencyFmt(Number(data.goalTarget), lang));
        const msg = ok
          ? (data.reply || (pt("addConfirm", lang) + ": " + data.goalName + " — " + currencyFmt(Number(data.goalTarget), lang)))
          : pt("errorGeneric", lang);
        onAppendChat("assistant", msg);
        setUiState("speaking");
        speak(msg);
        return;
      }

      if (data.action === "deleteGoal" && data.goalName) {
        const match = onFindGoalByName(data.goalName);
        if (!match) {
          const msg = pt("deleteNone", lang);
          onAppendChat("assistant", msg);
          setUiState("speaking");
          speak(msg);
          return;
        }
        onAppendChat("assistant", pt("deleteConfirmQ", lang) + ": " + match.name + "?");
        setPendingDelete({ type: "goal", id: match.id, label: match.name, amount: match.targetAmount });
        setUiState("confirm-delete");
        return;
      }

      if (data.action === "addCredit" && data.creditName && data.creditTotal) {
        const monthly = data.creditMonthly ? Number(data.creditMonthly) : Number(data.creditTotal) / 12;
        const duration = Math.max(1, Math.ceil(Number(data.creditTotal) / monthly));
        const ok = onAddCredit({
          name: data.creditName,
          totalAmount: Number(data.creditTotal),
          monthlyPayment: monthly,
          durationMonths: duration,
          startMonth: todayRealISO.slice(0, 7),
        });
        if (ok) logPlannerAction("addCredit", data.creditName + " " + currencyFmt(monthly, lang) + "/mo");
        const msg = ok
          ? (data.reply || (pt("addConfirm", lang) + ": " + data.creditName + " — " + currencyFmt(monthly, lang) + "/mo"))
          : pt("errorGeneric", lang);
        onAppendChat("assistant", msg);
        setUiState("speaking");
        speak(msg);
        return;
      }

      if (data.action === "deleteCredit" && data.creditName) {
        const match = onFindCreditByName(data.creditName);
        if (!match) {
          const msg = pt("deleteNone", lang);
          onAppendChat("assistant", msg);
          setUiState("speaking");
          speak(msg);
          return;
        }
        onAppendChat("assistant", pt("deleteConfirmQ", lang) + ": " + match.name + "?");
        setPendingDelete({ type: "credit", id: match.id, label: match.name, amount: match.totalAmount });
        setUiState("confirm-delete");
        return;
      }

      const reply = data.reply || pt("errorGeneric", lang);
      onAppendChat("assistant", reply);
      setUiState("speaking");
      speak(reply);
    } catch (err) {
      console.error("Planer AI error:", err);
      onAppendChat("assistant", pt("errorGeneric", lang));
      setUiState("idle");
    }
  }

  function confirmDelete() {
    if (pendingDelete) {
      if (pendingDelete.type === "goal") onDeleteGoalById(pendingDelete.id);
      else if (pendingDelete.type === "credit") onDeleteCreditById(pendingDelete.id);
      else onDeleteLastEntry(pendingDelete.id);
      logPlannerAction("delete:" + pendingDelete.type, pendingDelete.label);
    }
    const msg = pt("deleteDone", lang);
    setPendingDelete(null);
    onAppendChat("assistant", msg);
    setUiState("speaking");
    speak(msg);
  }
  function cancelDelete() {
    onAppendChat("assistant", pt("quickCancel", lang));
    setPendingDelete(null);
    setUiState("idle");
  }

  function startListening() {
    if (!supported) { setUiState("unsupported"); return; }
    setHeard(""); setAnswer("");
    // Safari on iOS only allows speechSynthesis to actually produce sound
    // when speak() is called synchronously inside a real user gesture —
    // the real reply always arrives after an async network round-trip to
    // Groq, well outside that window, so it was getting silently blocked
    // (no error, it just never spoke). Speaking a silent, empty utterance
    // right here — inside this tap handler — "unlocks" audio for the rest
    // of this interaction, so the later speak() call actually plays.
    unlockSpeech();

    // A fresh SpeechRecognition instance every time, rather than reusing
    // one long-lived instance across multiple questions — iOS Safari's
    // webkitSpeechRecognition is known to become unreliable on repeated
    // start() calls against the same instance (it can accept the second
    // start() without throwing, look like it's listening, and then never
    // fire onresult at all). This also means onresult always closes over
    // the current render's handleTranscript, so it's never stale.
    if (recognitionRef.current) {
      try { recognitionRef.current.stop(); } catch (e) {}
    }
    const SpeechRecognitionCtor = window.SpeechRecognition || window.webkitSpeechRecognition;
    const recognition = new SpeechRecognitionCtor();
    recognition.lang = SPEECH_LOCALE[lang] || "en-US";
    recognition.continuous = false;
    recognition.interimResults = false;
    recognition.onresult = (event) => {
      const text = event.results[0][0].transcript;
      setHeard(text);
      handleTranscript(text);
    };
    recognition.onerror = (event) => {
      setUiState("idle");
      if (event.error === "not-allowed") onAppendChat("assistant", pt("micDenied", lang));
    };
    recognition.onend = () => {
      setUiState((s) => (s === "listening" ? "idle" : s));
    };
    recognitionRef.current = recognition;

    try {
      recognition.start();
      setUiState("listening");
    } catch (err) {
      // A brand-new instance can still occasionally fail to start right
      // after stopping the previous one — one short retry recovers from
      // that instead of the mic silently doing nothing.
      console.warn("Speech recognition start failed, retrying:", err);
      setTimeout(() => {
        try {
          recognition.start();
          setUiState("listening");
        } catch (retryErr) {
          console.warn("Speech recognition retry failed:", retryErr);
          setUiState("idle");
        }
      }, 250);
    }
  }

  function submitTyped(e) {
    if (e) e.preventDefault();
    const value = typedText.trim();
    if (!value) return;
    unlockSpeech();
    setHeard(value);
    setTypedText("");
    handleTranscript(value);
  }

  function closePanel() {
    try { recognitionRef.current && recognitionRef.current.stop(); } catch (e) {}
    if (synthRef.current) synthRef.current.cancel();
    setOpen(false);
    setUiState("idle");
    setHeard("");
    setAnswer("");
    setTypedText("");
    setPendingDelete(null);
  }

  let panelStateClass = "";
  if (uiState === "listening") panelStateClass = "planer-panel--listening";
  else if (uiState === "thinking") panelStateClass = "planer-panel--thinking";
  else if (uiState === "speaking") panelStateClass = "planer-panel--speaking";
  else if (!supported) panelStateClass = "planer-panel--unsupported";

  return (
    <>
      <button
        type="button"
        ref={launcherRef}
        className="planer-launcher"
        style={launcherPos ? { left: launcherPos.left, top: launcherPos.top, right: "auto", bottom: "auto" } : undefined}
        onPointerDown={onLauncherPointerDown}
        onPointerMove={onLauncherPointerMove}
        onPointerUp={onLauncherPointerUp}
        aria-label={pt("launcherTitle", lang)}
      >
        <span className="planer-mark"><PlanerEyesMark size={22} /></span>
        <span>
          <strong>{pt("launcherTitle", lang)}</strong>
          <small>{pt("launcherSub", lang)}</small>
        </span>
        <i></i>
      </button>

      {open && (
        <div className="planer-backdrop" onClick={closePanel}>
          <div className={"planer-panel " + panelStateClass} onClick={(e) => e.stopPropagation()}>
            <div className="planer-header">
              <div className="planer-title">
                <span className="planer-mark"><PlanerEyesMark size={18} /></span>
                <div>
                  <strong>{pt("title", lang)}</strong>
                  <span>{pt("subtitle", lang)}</span>
                </div>
              </div>
              <div style={{ display: "flex", alignItems: "center", gap: 4 }}>
                {chatHistory && chatHistory.length > 0 && (
                  <button type="button" className="planer-close" onClick={onClearChat} aria-label={pt("clearChatBtn", lang)} title={pt("clearChatBtn", lang)}>
                    <Icon name="trash" size={15} />
                  </button>
                )}
                <button type="button" className="planer-close" onClick={closePanel} aria-label={pt("cancel", lang)}><Icon name="x" size={16} /></button>
              </div>
            </div>

            <div className="planer-orb">
              <div className="planer-orb-ring planer-orb-ring--three"></div>
              <div className="planer-orb-ring"></div>
              <div className="planer-orb-ring planer-orb-ring--two"></div>
              <button
                type="button"
                className="planer-orb-button"
                onClick={startListening}
                disabled={uiState === "listening" || uiState === "thinking"}
                aria-label={pt("launcherTitle", lang)}
              >
                <span className="planer-mark"><PlanerEyesMark size={40} /></span>
              </button>
            </div>

            {(uiState === "listening" || uiState === "speaking") && (
              <div className="planer-wave" aria-hidden="true">
                <span></span><span></span><span></span><span></span><span></span>
              </div>
            )}

            <div className="planer-state">
              {!supported
                ? pt("unsupportedState", lang)
                : uiState === "listening" ? pt("listeningState", lang)
                : uiState === "thinking" ? pt("thinkingState", lang)
                : pt("idleState", lang)}
            </div>

            {chatHistory && chatHistory.length > 0 && (
              <div className="planer-chat-log" ref={chatScrollRef}>
                {chatHistory.map((m, i) => (
                  <div key={i} className={"planer-msg planer-msg--" + m.role}>
                    {m.text}
                  </div>
                ))}
              </div>
            )}

            {(uiState === "idle" || uiState === "speaking") && (
              <form className="planer-type-row" onSubmit={submitTyped}>
                <input
                  type="text"
                  value={typedText}
                  onChange={(e) => setTypedText(e.target.value)}
                  placeholder={pt("typedPlaceholder", lang)}
                />
                <button type="submit" aria-label={pt("typedSendBtn", lang)} disabled={!typedText.trim()}>
                  <Icon name="chevronRight" size={16} />
                </button>
              </form>
            )}

            {uiState === "confirm-delete" && pendingDelete && (
              <div className="planer-confirm">
                <button type="button" onClick={confirmDelete}>{pt("deleteConfirmYes", lang)}</button>
                <button type="button" onClick={cancelDelete}>{pt("deleteConfirmNo", lang)}</button>
              </div>
            )}

            {(!chatHistory || chatHistory.length === 0) && uiState === "idle" && (
              <div className="planer-examples">
                <button type="button" onClick={() => { unlockSpeech(); handleTranscript(pt("example1", lang)); }}>{pt("example1", lang)}</button>
                <button type="button" onClick={() => { unlockSpeech(); handleTranscript(pt("example2", lang)); }}>{pt("example2", lang)}</button>
                <button type="button" onClick={() => { unlockSpeech(); handleTranscript(pt("example3", lang)); }}>{pt("example3", lang)}</button>
              </div>
            )}

            <button type="button" className="planer-cancel" onClick={closePanel}>{pt("cancel", lang)}</button>
          </div>
        </div>
      )}
    </>
  );
}

/* =========================================================
   Notifications center
   ========================================================= */
function timeAgoLabel(timestamp, lang) {
  const diffMs = Date.now() - timestamp;
  const diffMin = Math.floor(diffMs / 60000);
  if (diffMin < 1) return t("notifJustNow", lang);
  if (diffMin < 60) return `${diffMin} ${t("notifMinutesAgo", lang)}`;
  const diffHr = Math.floor(diffMin / 60);
  if (diffHr < 24) return `${diffHr} ${t("notifHoursAgo", lang)}`;
  const diffDay = Math.floor(diffHr / 24);
  return `${diffDay} ${t("notifDaysAgo", lang)}`;
}

function SponsoredCard({ ad, onDismiss }) {
  const { lang } = useLang();
  if (!ad) return null;
  return (
    <section className="panel sponsored-card">
      <div className="sponsored-top">
        <span className="sponsored-label">{t("sponsoredLabel", lang)}</span>
        <button type="button" className="sponsored-dismiss" onClick={onDismiss} aria-label={t("cancelBtn", lang)}><Icon name="x" size={16} /></button>
      </div>
      <div className="sponsored-title">{ad.title}</div>
      {ad.description && <p className="sponsored-desc">{ad.description}</p>}
      <a href={ad.link_url} target="_blank" rel="noopener noreferrer sponsored" className="sponsored-cta">
        {ad.cta_label || t("sponsoredDefaultCta", lang)}
      </a>
    </section>
  );
}

function NotificationsPanel({ notifications, onClose, onClearAll }) {
  const { lang } = useLang();
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{t("notificationsTitle", lang)}</h3>
          <button type="button" className="modal-close" onClick={onClose} aria-label={t("cancelBtn", lang)}><Icon name="x" size={16} /></button>
        </div>
        {notifications.length === 0 ? (
          <EmptyState icon="bell" text={t("notificationsEmpty", lang)} />
        ) : (
          <>
            <ul className="notif-list">
              {notifications.map((n) => (
                <li className={`notif-item ${n.read ? "" : "notif-item--unread"}`} key={n.id}>
                  <div className="notif-item-icon"><Icon name="bell" size={15} /></div>
                  <div className="notif-item-main">
                    <div className="notif-item-title">{n.title}</div>
                    <div className="notif-item-body">{n.body}</div>
                    <div className="notif-item-time">{timeAgoLabel(n.createdAt, lang)}</div>
                  </div>
                </li>
              ))}
            </ul>
            <button type="button" className="ghost-btn" style={{ width: "100%", marginTop: 10 }} onClick={onClearAll}>
              {t("notificationsClearAll", lang)}
            </button>
          </>
        )}
      </div>
    </div>
  );
}

/* =========================================================
   Main planner
   ========================================================= */
function Planner() {
  const { lang, setLang } = useLang();
  const [session, setSession] = useState(null);
  const [authChecked, setAuthChecked] = useState(false);
  const [dataLoading, setDataLoading] = useState(true);
  const [syncBlocked, setSyncBlocked] = useState(false);
  const [retryTick, setRetryTick] = useState(0);
  const [entries, setEntries] = useState([]);
  const [goals, setGoals] = useState([]);
  const [settings, setSettings] = useState(DEFAULT_SETTINGS);
  const [displayName, setDisplayName] = useState("");
  const [shiftSchedules, setShiftSchedules] = useState({});
  const [credits, setCredits] = useState([]);
  const [planerChat, setPlanerChat] = useState([]);
  const [subscription, setSubscription] = useState({ status: "inactive", plan: null, current_period_end: null });
  const [month, setMonth] = useState(() => new Date().toISOString().slice(0, 7));
  const [reportYear, setReportYear] = useState(() => new Date().getFullYear());
  const [showQuick, setShowQuick] = useState(false);
  const [showAddEntry, setShowAddEntry] = useState(false);
  const [editingEntry, setEditingEntry] = useState(null);
  const [editScope, setEditScope] = useState(null);
  const [scopeAction, setScopeAction] = useState(null);
  const [showSettings, setShowSettings] = useState(false);
  const [goalForm, setGoalForm] = useState(null);
  const [creditForm, setCreditForm] = useState(null);
  const [showImportBank, setShowImportBank] = useState(false);
  const [showImportOptions, setShowImportOptions] = useState(false);
  const [showSearch, setShowSearch] = useState(false);
  const [notifications, setNotifications] = useState([]);
  const [showNotifications, setShowNotifications] = useState(false);
  const [activeAd, setActiveAd] = useState(null);
  const [paywallFeature, setPaywallFeature] = useState(null);
  // iOS Safari can fully reload the page after backgrounding it for a
  // while (memory pressure), which resets all in-memory React state —
  // including which tab was open, dropping the person back on Home even
  // if they were deep in Stats/Goals/Activity. Persisting just the active
  // tab is a small thing, but it means a reload puts them back roughly
  // where they were instead of at square one.
  const [activeTab, setActiveTab] = useState(() => {
    try { return localStorage.getItem("salary-planner:active-tab:v1") || "home"; } catch { return "home"; }
  });
  useEffect(() => {
    try { localStorage.setItem("salary-planner:active-tab:v1", activeTab); } catch {}
  }, [activeTab]);
  const [showPremiumWelcome, setShowPremiumWelcome] = useState(false);
  const [dienstplanOpen, setDienstplanOpen] = useState(false);
  const [showAffordability, setShowAffordability] = useState(false);
  const [showReceiptScan, setShowReceiptScan] = useState(false);
  const [selectedCategory, setSelectedCategory] = useState(null);
   const fileInputRef = useRef(null);
   const justLoadedRef = useRef(false);
   const pendingUpsertRef = useRef(Promise.resolve());

  const email = session ? session.user.email : null;
  const isPremium = PREMIUM_ENFORCED ? isPremiumStatus(subscription.status) : true;
  currentCurrency = settings.currency || "EUR";

  useEffect(() => {
    if (!settings.autoTranslateEnabled || !session || !session.access_token) return;
    let cancelled = false;

    async function run() {
      const jobs = [];

      entries.forEach((e) => {
        if (!e.label || (e.labelTranslations && e.labelTranslations[lang])) return;
        jobs.push({
          text: e.label,
          apply: (translated) => setEntries((prev) => prev.map((x) => (
            x.id === e.id ? { ...x, labelTranslations: { ...(x.labelTranslations || {}), [lang]: translated } } : x
          ))),
        });
      });

      goals.forEach((g) => {
        if (!g.name || (g.nameTranslations && g.nameTranslations[lang])) return;
        jobs.push({
          text: g.name,
          apply: (translated) => setGoals((prev) => prev.map((x) => (
            x.id === g.id ? { ...x, nameTranslations: { ...(x.nameTranslations || {}), [lang]: translated } } : x
          ))),
        });
      });

      credits.forEach((c) => {
        if (!c.name || (c.nameTranslations && c.nameTranslations[lang])) return;
        jobs.push({
          text: c.name,
          apply: (translated) => setCredits((prev) => prev.map((x) => (
            x.id === c.id ? { ...x, nameTranslations: { ...(x.nameTranslations || {}), [lang]: translated } } : x
          ))),
        });
      });

      Object.keys(shiftSchedules).forEach((mKey) => {
        const sched = shiftSchedules[mKey];
        if (!sched || !sched.notes) return;
        Object.keys(sched.notes).forEach((day) => {
          const noteText = sched.notes[day];
          if (!noteText) return;
          const already = sched.noteTranslations && sched.noteTranslations[day] && sched.noteTranslations[day][lang];
          if (already) return;
          jobs.push({
            text: noteText,
            apply: (translated) => setShiftSchedules((prev) => {
              const monthSched = prev[mKey] || {};
              const noteTranslations = { ...(monthSched.noteTranslations || {}) };
              noteTranslations[day] = { ...(noteTranslations[day] || {}), [lang]: translated };
              return { ...prev, [mKey]: { ...monthSched, noteTranslations } };
            }),
          });
        });
      });

      if (jobs.length === 0 || cancelled) return;

      for (let i = 0; i < jobs.length; i += 60) {
        if (cancelled) return;
        const chunk = jobs.slice(i, i + 60);
        const translations = await translateTexts(chunk.map((j) => j.text), lang, session.access_token);
        if (translations && !cancelled) {
          chunk.forEach((job, idx) => { if (translations[idx]) job.apply(translations[idx]); });
        }
      }
    }
    run();
    return () => { cancelled = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [lang, settings.autoTranslateEnabled, session]);

  useEffect(() => {
    setNotifications(email ? loadNotificationHistory(email) : []);
  }, [email]);

  useEffect(() => {
    if (!session || isPremium) { setActiveAd(null); return; }
    let cancelled = false;
    fetchActiveAd().then((ad) => {
      if (cancelled || !ad) return;
      const dismissed = loadDismissedAds(session.user.id);
      if (!dismissed.includes(ad.id)) setActiveAd(ad);
    });
    return () => { cancelled = true; };
  }, [session, isPremium]);
  function dismissActiveAd() {
    if (activeAd && session) saveDismissedAd(session.user.id, activeAd.id);
    setActiveAd(null);
  }

  function pushNotification(title, body) {
    fireNotification(title, body);
    if (!email) return;
    setNotifications((prev) => {
      const next = [{ id: makeId(), title, body, createdAt: Date.now(), read: false }, ...prev].slice(0, 50);
      saveNotificationHistory(email, next);
      return next;
    });
  }
  const unreadNotifCount = notifications.filter((n) => !n.read).length;
  function openNotifications() {
    setShowNotifications(true);
    if (unreadNotifCount > 0) {
      setNotifications((prev) => {
        const next = prev.map((n) => ({ ...n, read: true }));
        saveNotificationHistory(email, next);
        return next;
      });
    }
  }
  function clearAllNotifications() {
    setNotifications([]);
    if (email) saveNotificationHistory(email, []);
  }

  useEffect(() => {
    let mounted = true;
    supabase.auth.getSession().then(({ data }) => {
      if (mounted) { setSession(data.session); setAuthChecked(true); }
    });
    const { data: listener } = supabase.auth.onAuthStateChange((_event, newSession) => {
      setSession(newSession);
    });
    return () => { mounted = false; listener.subscription.unsubscribe(); };
  }, []);

  useEffect(() => {
    if (!session) {
      setEntries([]); setGoals([]); setSettings(DEFAULT_SETTINGS); setDisplayName(""); setShiftSchedules({}); setCredits([]); setPlanerChat([]);
      setSubscription({ status: "inactive", plan: null, current_period_end: null });
      setDataLoading(false);
      setSyncBlocked(false);
      justLoadedRef.current = false;
      return;
    }
    let cancelled = false;
    justLoadedRef.current = true;
    setDataLoading(true);
    setSyncBlocked(false);
    (async () => {
      const userId = session.user.id;

      let result = await fetchUserData(userId);
      for (let attempt = 0; !result.ok && attempt < 2 && !cancelled; attempt++) {
        await new Promise((r) => setTimeout(r, 800));
        result = await fetchUserData(userId);
      }
      if (cancelled) return;

      if (!result.ok) {
        const cached = loadCachedUserData(userId);
        if (cached) {
          setEntries(cached.entries);
          setGoals(cached.goals);
          setSettings({ ...DEFAULT_SETTINGS, ...cached.settings });
          setDisplayName(cached.displayName || "");
          setShiftSchedules(cached.shiftSchedules || {});
          setCredits(cached.credits || []);
          setPlanerChat(cached.planerChat || []);
        }
        setSyncBlocked(true);
        setDataLoading(false);
        return;
      }

      let data = result.data;
      if (!data) {
        await new Promise((r) => setTimeout(r, 700));
        const recheck = !cancelled ? await fetchUserData(userId) : result;
        if (cancelled) return;

        if (recheck.ok && recheck.data) {
          data = recheck.data;
        } else if (!recheck.ok) {
          const cached = loadCachedUserData(userId);
          if (cached) {
            setEntries(cached.entries);
            setGoals(cached.goals);
            setSettings({ ...DEFAULT_SETTINGS, ...cached.settings });
            setDisplayName(cached.displayName || "");
            setShiftSchedules(cached.shiftSchedules || {});
            setCredits(cached.credits || []);
            setPlanerChat(cached.planerChat || []);
          }
          setSyncBlocked(true);
          setDataLoading(false);
          return;
        } else {
          const legacy = collectLegacyLocalData(session.user.email);
          const cached = loadCachedUserData(userId);
          data = legacy || cached || {
            entries: [], goals: [], settings: { ...DEFAULT_SETTINGS },
            displayName: (session.user.user_metadata && session.user.user_metadata.name) || "",
            shiftSchedules: {},
            credits: [],
            planerChat: [],
          };
        }
      }
      setEntries(data.entries);
      setGoals(data.goals);
      setSettings({ ...DEFAULT_SETTINGS, ...data.settings });
      setDisplayName(data.displayName || (session.user.user_metadata && session.user.user_metadata.name) || "");
      setShiftSchedules(data.shiftSchedules || {});
      setCredits(data.credits || []);
      setPlanerChat(data.planerChat || []);
      saveCachedUserData(userId, data);
      setDataLoading(false);

      const sub = await fetchSubscription(userId);
      if (!cancelled) setSubscription(sub);
    })();
    return () => { cancelled = true; };
    // Deliberately keyed on the user id, not the whole session object.
    // Supabase silently issues a new session object (same user, refreshed
    // JWT) whenever the app regains focus/visibility — keying this on
    // `session` itself made every one of those refreshes re-trigger the
    // full "loading..." screen, which unmounts the entire app tree and
    // was silently closing whatever the person had open (e.g. a Dienstplan
    // scan mid-review) after even a few seconds away in another app.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [session && session.user && session.user.id, retryTick]);

  function retrySync() { setRetryTick((n) => n + 1); }

  useEffect(() => {
    if (!session || dataLoading || syncBlocked) return;
    if (justLoadedRef.current) {
      justLoadedRef.current = false;
      return;
    }
    const userId = session.user.id;
    const payload = { entries, goals, settings, displayName, shiftSchedules, credits, planerChat };
    saveCachedUserData(userId, payload);
    pendingUpsertRef.current = pendingUpsertRef.current
      .catch(() => {})
      .then(() => upsertUserData(userId, payload));
  }, [entries, goals, settings, displayName, shiftSchedules, credits, planerChat, session, dataLoading, syncBlocked]);

  useEffect(() => {
    const isLight = settings.themeMode === "light";
    document.documentElement.classList.toggle("theme-light", isLight);
    try { localStorage.setItem("paraplanner:theme:v1", isLight ? "light" : "dark"); } catch {}
  }, [settings.themeMode]);

  useEffect(() => {
    if (!session || dataLoading) return;
    if (typeof window === "undefined") return;
    const params = new URLSearchParams(window.location.search);
    if (params.get("upgraded") !== "1") return;

    const cleanUrl = window.location.pathname + window.location.hash;
    window.history.replaceState({}, "", cleanUrl);

    let cancelled = false;
    (async () => {
      const userId = session.user.id;
      for (let attempt = 0; attempt < 6 && !cancelled; attempt++) {
        const sub = await fetchSubscription(userId);
        if (isPremiumStatus(sub.status)) {
          if (!cancelled) {
            setSubscription(sub);
            setShowPremiumWelcome(true);
          }
          return;
        }
        await new Promise((r) => setTimeout(r, 1500));
      }
    })();
    return () => { cancelled = true; };
  }, [session, dataLoading]);

  function saveShiftMonth(monthKey, scheduleObj) {
    setShiftSchedules((prev) => ({ ...prev, [monthKey]: scheduleObj }));
  }

  function addCredit(values) {
    if (!isPremium) { setPaywallFeature("credits"); return; }
    const entryId = makeId();
    const endMonth = shiftMonth(values.startMonth, Math.max(0, values.durationMonths - 1));
    const linkedEntry = {
      id: entryId, kind: "expense", category: "debt",
      label: values.name, amount: values.monthlyPayment,
      date: `${values.startMonth}-01`, term: "month",
      recurring: true, endMonth, exceptions: {},
    };
    setEntries((prev) => [...prev, linkedEntry]);
    setCredits((prev) => [...prev, { id: makeId(), ...values, linkedEntryId: entryId }]);
    setCreditForm(null);
  }
  function updateCredit(id, values) {
    setCredits((prev) => prev.map((c) => (c.id === id ? { ...c, ...values } : c)));
    setCredits((prev) => {
      const credit = prev.find((c) => c.id === id);
      if (credit) {
        const endMonth = shiftMonth(credit.startMonth, Math.max(0, values.durationMonths - 1));
        setEntries((prevEntries) => prevEntries.map((e) => (e.id === credit.linkedEntryId
          ? { ...e, label: values.name, amount: values.monthlyPayment, endMonth }
          : e)));
      }
      return prev;
    });
    setCreditForm(null);
  }
  function deleteCredit(credit) {
    if (window.confirm(t("goalDeleteConfirm", lang))) {
      setCredits((prev) => prev.filter((c) => c.id !== credit.id));
      setEntries((prev) => prev.filter((e) => e.id !== credit.linkedEntryId));
    }
  }

  // Voice/typed-command versions — same linked-recurring-entry mechanics
  // as addCredit/deleteCredit above, without the paywall UI or a native
  // confirm() dialog (Planer already runs its own confirm-before-delete
  // step for anything it's about to remove).
  function voiceAddCredit(values) {
    if (!isPremium) return false;
    const entryId = makeId();
    const endMonth = shiftMonth(values.startMonth, Math.max(0, values.durationMonths - 1));
    const linkedEntry = {
      id: entryId, kind: "expense", category: "debt",
      label: values.name, amount: values.monthlyPayment,
      date: `${values.startMonth}-01`, term: "month",
      recurring: true, endMonth, exceptions: {},
    };
    setEntries((prev) => [...prev, linkedEntry]);
    setCredits((prev) => [...prev, { id: makeId(), ...values, linkedEntryId: entryId }]);
    return true;
  }
  function findCreditByName(name) {
    if (!name) return null;
    const needle = name.trim().toLowerCase();
    if (!needle) return null;
    return (
      credits.find((c) => c.name.toLowerCase() === needle) ||
      credits.find((c) => c.name.toLowerCase().includes(needle) || needle.includes(c.name.toLowerCase())) ||
      null
    );
  }
  function deleteCreditById(id) {
    setCredits((prev) => {
      const credit = prev.find((c) => c.id === id);
      if (credit) setEntries((prevEntries) => prevEntries.filter((e) => e.id !== credit.linkedEntryId));
      return prev.filter((c) => c.id !== id);
    });
  }

  function addGoal(values) {
    if (!isPremium && goals.length >= FREE_GOAL_LIMIT) { setPaywallFeature("goals"); return; }
    setGoals((prev) => [...prev, { id: makeId(), ...values }]);
    setGoalForm(null);
  }
  function updateGoal(id, values) {
    setGoals((prev) => prev.map((g) => (g.id === id ? { ...g, ...values } : g)));
    setGoalForm(null);
  }
  function deleteGoal(goal) {
    if (window.confirm(t("goalDeleteConfirm", lang))) {
      setGoals((prev) => prev.filter((g) => g.id !== goal.id));
    }
  }

  // Voice/typed-command versions of the goal handlers above — same rules
  // (premium gate, generated id) but without touching goalForm or opening
  // a native confirm() dialog, since those are built for the manual form
  // flow. Returns a value Planer can react to instead.
  function voiceAddGoal(values) {
    if (!isPremium && goals.length >= FREE_GOAL_LIMIT) return false;
    setGoals((prev) => [...prev, { id: makeId(), ...values }]);
    return true;
  }
  function findGoalByName(name) {
    if (!name) return null;
    const needle = name.trim().toLowerCase();
    if (!needle) return null;
    return (
      goals.find((g) => g.name.toLowerCase() === needle) ||
      goals.find((g) => g.name.toLowerCase().includes(needle) || needle.includes(g.name.toLowerCase())) ||
      null
    );
  }
  function deleteGoalById(id) {
    setGoals((prev) => prev.filter((g) => g.id !== id));
  }

  useEffect(() => {
    if (!session) return;
    function checkNotifications() {
      if (!settings.notificationsEnabled) return;
      if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
      const notified = loadNotified(email);
      let changed = false;
      const todayKey = todayISO();
      const currentMonthKey = monthKey(todayKey);
      const todayList = expandEntriesForMonth(entries, currentMonthKey);

      if (settings.budgetAlertThreshold > 0) {
        const inc = todayList.filter((e) => e.kind === "income").reduce((s, e) => s + e.amount, 0);
        const exp = todayList.filter((e) => e.kind === "expense" && e.category !== "savings").reduce((s, e) => s + e.amount, 0);
        if (inc > 0) {
          const ratio = (exp / inc) * 100;
          const key = `budget:${currentMonthKey}:${settings.budgetAlertThreshold}`;
          if (ratio >= settings.budgetAlertThreshold && !notified.has(key)) {
            pushNotification(t("notifBudgetTitle", lang), `${t("notifBudgetBody", lang)} ${Math.round(ratio)}%`);
            notified.add(key); changed = true;
          }
        }
      }

      if (settings.recurringReminderEnabled) {
        todayList.filter((e) => e.recurring && e.date === todayKey).forEach((e) => {
          const key = `recurring:${e.baseId}:${todayKey}`;
          if (!notified.has(key)) {
            pushNotification(t("notifRecurringTitle", lang), `${e.label} · ${currencyFmt(e.amount, lang)}`);
            notified.add(key); changed = true;
          }
        });
        getUpcomingExpenses(entries, 3).filter((e) => e.recurring).forEach((e) => {
          const key = `dueSoon:${e.baseId}:${e.date}`;
          if (!notified.has(key)) {
            const d = daysFromToday(e.date);
            pushNotification(t("notifDueSoonTitle", lang), `${e.label} · ${currencyFmt(e.amount, lang)} · ${t("upcomingInDays", lang)} ${d} ${d === 1 ? t("upcomingDayUnit", lang) : t("upcomingDaysUnit", lang)}`);
            notified.add(key); changed = true;
          }
        });
      }

      const todaySchedule = shiftSchedules[currentMonthKey];
      const todayShiftCode = todaySchedule && todaySchedule[String(Number(todayKey.slice(8, 10)))];
      const todayShift = SHIFT_TYPES.find((item) => item.code === todayShiftCode);
      const todayNote = todaySchedule && todaySchedule.notes && todaySchedule.notes[String(Number(todayKey.slice(8, 10)))];
      if (todayShift || todayNote) {
        const key = `shift:${todayKey}:${todayShiftCode || ""}:${todayNote || ""}`;
        if (!notified.has(key)) {
          const shiftText = todayShift ? t(todayShift.labelKey, lang) : "";
          const body = [t("notifShiftBody", lang), shiftText, todayNote].filter(Boolean).join(" · ");
          pushNotification(t("notifShiftTitle", lang), body);
          notified.add(key); changed = true;
        }
      }

      if (settings.spendingSpikeAlertEnabled) {
        const prevMonthKey = shiftMonth(currentMonthKey, -1);
        const todayDay = Number(todayKey.slice(8, 10));
        const prevList = expandEntriesForMonth(entries, prevMonthKey);
        const prevMonthToDateKey = `${prevMonthKey}-${String(Math.min(todayDay, daysInMonth(prevMonthKey))).padStart(2, "0")}`;
        const curSpend = todayList.filter((e) => e.kind === "expense" && e.category !== "savings" && e.date <= todayKey).reduce((s, e) => s + e.amount, 0);
        const prevSpend = prevList.filter((e) => e.kind === "expense" && e.category !== "savings" && e.date <= prevMonthToDateKey).reduce((s, e) => s + e.amount, 0);
        if (prevSpend > 0) {
          const pctUp = ((curSpend - prevSpend) / prevSpend) * 100;
          const key = `spike:${currentMonthKey}`;
          if (pctUp >= 15 && !notified.has(key)) {
            pushNotification(t("notifSpikeTitle", lang), `${t("notifSpikeBody", lang)} ${Math.round(pctUp)}%`);
            notified.add(key); changed = true;
          }
        }
      }

      {
        const stsCalc = computeSafeToSpend(entries, settings, goals);
        if (stsCalc.available < 0) {
          const key = `negativeAvailable:${currentMonthKey}`;
          if (!notified.has(key)) {
            pushNotification(t("notifNegativeTitle", lang), t("notifNegativeBody", lang));
            notified.add(key); changed = true;
          }
        }
      }

      entries
        .filter((e) => e.expiresAt && e.expiresAt > Date.now() && e.expiresAt - Date.now() < 60 * 60 * 1000)
        .forEach((e) => {
          const key = `expiring:${e.id}`;
          if (!notified.has(key)) {
            pushNotification(t("notifExpiringTitle", lang), `${e.label} · ${currencyFmt(e.amount, lang)}`);
            notified.add(key); changed = true;
          }
        });

      if (changed) saveNotified(email, notified);
    }
    checkNotifications();
    const id = setInterval(checkNotifications, 60 * 1000);
    return () => clearInterval(id);
  }, [entries, settings, session, lang, goals, shiftSchedules]);

  useEffect(() => {
    function purgeExpired() {
      setEntries((prev) => prev.filter((e) => !e.expiresAt || e.expiresAt > Date.now()));
    }
    purgeExpired();
    const id = setInterval(purgeExpired, 60 * 1000);
    return () => clearInterval(id);
  }, []);

  const availableMonths = useMemo(() => {
    const set = new Set(entries.map((e) => monthKey(e.date)));
    set.add(month);
    return Array.from(set).sort().reverse();
  }, [entries, month]);
  const availableYears = useMemo(() => {
    const set = new Set(entries.map((e) => Number(monthKey(e.date).slice(0, 4))));
    set.add(new Date().getFullYear());
    set.add(reportYear);
    return Array.from(set).sort((a, b) => a - b);
  }, [entries, reportYear]);

  const monthEntries = useMemo(() => expandEntriesForMonth(entries, month), [entries, month]);
  const allEntries = useMemo(() => expandEntriesForRange(entries), [entries]);

  const income = useMemo(() => monthEntries.filter((e) => e.kind === "income").reduce((s, e) => s + e.amount, 0), [monthEntries]);
  const currentMonthIncome = useMemo(() => {
    const realMonth = monthKey(todayISO());
    return expandEntriesForMonth(entries, realMonth).filter((e) => e.kind === "income").reduce((s, e) => s + e.amount, 0);
  }, [entries]);
  const expenses = useMemo(() => monthEntries.filter((e) => e.kind === "expense" && e.category !== "savings").reduce((s, e) => s + e.amount, 0), [monthEntries]);
  const savingsAllocated = useMemo(() => monthEntries.filter((e) => e.kind === "expense" && e.category === "savings").reduce((s, e) => s + e.amount, 0), [monthEntries]);
  const balance = income - expenses;
  const savingsRatio = income > 0 ? savingsAllocated / income : 0;

  const termTotals = useMemo(() => {
    const totals = { day: 0, month: 0, year: 0 };
    monthEntries
      .filter((e) => e.kind === "expense" && e.category !== "savings" && totals.hasOwnProperty(e.term))
      .forEach((e) => { totals[e.term] += e.amount; });
    return totals;
  }, [monthEntries]);

  const categoryRows = useMemo(() => {
    const map = new Map();
    monthEntries.filter((e) => e.kind === "expense" && e.category !== "savings").forEach((e) => map.set(e.category, (map.get(e.category) || 0) + e.amount));
    return Array.from(map, ([categoryId, amount]) => ({ categoryId, amount })).sort((a, b) => b.amount - a.amount);
  }, [monthEntries]);

  function addEntry(entry) { setEntries((prev) => [...prev, entry]); }
  function handleAddEntry(values) {
    addEntry({ ...values, id: values.id || makeId(), endMonth: null, exceptions: {} });
  }

  function addPendingPurchase(item) {
    setSettings((prev) => ({ ...prev, pendingPurchases: [...(prev.pendingPurchases || []), item] }));
  }
  function updatePendingPurchase(id, patch) {
    setSettings((prev) => ({
      ...prev,
      pendingPurchases: (prev.pendingPurchases || []).map((p) => (p.id === id ? { ...p, ...patch } : p)),
    }));
  }
  function handlePendingBought(item) {
    addEntry({
      id: makeId(), kind: "expense", category: item.category || "other",
      label: item.name, amount: item.price, date: todayISO(), term: "day",
      recurring: false, endMonth: null, exceptions: {},
    });
    updatePendingPurchase(item.id, { status: "bought" });
  }
  function handlePendingSkipped(item) {
    updatePendingPurchase(item.id, { status: "skipped" });
  }
  function handlePendingKeepWaiting(item) {
    updatePendingPurchase(item.id, { reviewAt: Date.now() + 24 * 60 * 60 * 1000 });
  }

  // Appends a message to Planer's persistent chat history (synced to
  // Supabase like everything else) — capped so the stored history and
  // the prompt built from it don't grow unbounded forever.
  function appendPlanerChatMessage(role, text) {
    setPlanerChat((prev) => [...prev, { role, text, at: Date.now() }].slice(-200));
  }
  function clearPlanerChat() {
    setPlanerChat([]);
  }

  function deleteEntryById(id) {
    setEntries((prev) => prev.filter((e) => e.id !== id));
  }

  // Lets Planer adjust the amount of the entry it just spoke about
  // (increase, decrease, or set to a specific new value) without needing
  // a full edit-scope flow — matches how it already deletes "the last
  // entry" by id.
  function updateLastEntryAmount(id, amount) {
    setEntries((prev) => prev.map((e) => (e.id === id ? { ...e, amount } : e)));
  }

  // Sets a single day's Dienstplan shift from a resolved ISO date +
  // shift code, merging into whatever that month's schedule already has
  // (creating the month if it doesn't exist yet) — the same shape
  // startFilling()/setDayShift() build up manually in the Dienstplan UI.
  function setShiftForDate(dateISO, code) {
    const mKey = monthKey(dateISO);
    const day = String(Number(dateISO.slice(8, 10)));
    setShiftSchedules((prev) => {
      const existing = prev[mKey] || { notes: {}, tags: {} };
      return { ...prev, [mKey]: { ...existing, [day]: code } };
    });
  }

  // Lets Planer change a handful of the most commonly requested settings
  // by voice — language, currency, theme, and notifications. Deliberately
  // scoped to these rather than every field in Settings: they're safe to
  // flip without a confirmation step (unlike, say, clearing all data) and
  // cover what people actually ask an assistant to change for them.
  function handleSettingChange(key, value) {
    if (key === "language" && LANGS.includes(value)) {
      setLang(value);
      return true;
    }
    if (key === "currency" && CURRENCIES.some((c) => c.code === value)) {
      setSettings((prev) => ({ ...prev, currency: value }));
      return true;
    }
    if (key === "theme" && (value === "dark" || value === "light")) {
      setSettings((prev) => ({ ...prev, themeMode: value }));
      return true;
    }
    if (key === "notifications" && typeof value === "boolean") {
      if (value && typeof Notification !== "undefined") {
        Notification.requestPermission().then((perm) => {
          setSettings((prev) => ({ ...prev, notificationsEnabled: perm === "granted" }));
        });
      } else {
        setSettings((prev) => ({ ...prev, notificationsEnabled: false }));
      }
      return true;
    }
    // The rest are plain boolean/numeric toggles already in Settings —
    // wiring them up here just means Planer can flip the same field the
    // Settings screen would, nothing new is invented.
    if (key === "emergencyBuffer" && typeof value === "number" && value >= 0) {
      setSettings((prev) => ({ ...prev, emergencyBuffer: value }));
      return true;
    }
    if (key === "budgetAlertThreshold" && typeof value === "number" && value >= 0) {
      setSettings((prev) => ({ ...prev, budgetAlertThreshold: value }));
      return true;
    }
    if (key === "recurringReminder" && typeof value === "boolean") {
      if (!isPremium) return false;
      setSettings((prev) => ({ ...prev, recurringReminderEnabled: value }));
      return true;
    }
    if (key === "spendingSpikeAlert" && typeof value === "boolean") {
      if (!isPremium) return false;
      setSettings((prev) => ({ ...prev, spendingSpikeAlertEnabled: value }));
      return true;
    }
    if (key === "autoTranslate" && typeof value === "boolean") {
      setSettings((prev) => ({ ...prev, autoTranslateEnabled: value }));
      return true;
    }
    if (key === "receiptScan" && typeof value === "boolean") {
      setSettings((prev) => ({ ...prev, receiptScanEnabled: value }));
      return true;
    }
    if (key === "dienstplanScan" && typeof value === "boolean") {
      setSettings((prev) => ({ ...prev, dienstplanScanEnabled: value }));
      return true;
    }
    if (key === "displayName" && typeof value === "string" && value.trim()) {
      handleRenameUser(value.trim());
      return true;
    }
    return false;
  }

  function updateSimpleEntry(id, values) {
    setEntries((prev) => prev.map((e) => (e.id === id ? { ...e, ...values } : e)));
  }
  function addExceptionDeleted(baseId, monthKeyVal) {
    setEntries((prev) => prev.map((e) => (e.id === baseId
      ? { ...e, exceptions: { ...(e.exceptions || {}), [monthKeyVal]: { deleted: true } } }
      : e)));
  }
  function setEndMonthBefore(baseId, monthKeyVal) {
    const prevM = shiftMonth(monthKeyVal, -1);
    setEntries((prev) => prev.map((e) => (e.id === baseId ? { ...e, endMonth: prevM } : e)));
  }
  function deleteBaseEntirely(baseId) {
    setEntries((prev) => prev.filter((e) => e.id !== baseId));
  }
  function setExceptionOverride(baseId, monthKeyVal, values) {
    setEntries((prev) => prev.map((e) => {
      if (e.id !== baseId) return e;
      const currentEffectiveDate = adjustDateToMonth(e.date, monthKeyVal);
      const unchanged = (
        e.label === values.label &&
        Number(e.amount) === Number(values.amount) &&
        e.category === values.category &&
        e.kind === values.kind &&
        e.term === values.term &&
        currentEffectiveDate === values.date
      );
      if (unchanged) return e;
      return { ...e, exceptions: { ...(e.exceptions || {}), [monthKeyVal]: values } };
    }));
  }
  function splitSeries(baseId, monthKeyVal, values) {
    setEntries((prev) => {
      const base = prev.find((e) => e.id === baseId);
      if (!base) return prev;
      const unchanged = (
        base.label === values.label &&
        Number(base.amount) === Number(values.amount) &&
        base.category === values.category &&
        base.kind === values.kind &&
        base.term === values.term &&
        adjustDateToMonth(base.date, monthKeyVal) === values.date
      );
      if (unchanged) return prev;
      const prevM = shiftMonth(monthKeyVal, -1);
      const updatedBase = { ...base, endMonth: prevM };
      const newEntry = { ...base, ...values, id: makeId(), recurring: true, endMonth: null, exceptions: {} };
      return prev.map((e) => (e.id === baseId ? updatedBase : e)).concat(newEntry);
    });
  }

  function requestEdit(entry) {
    if (!entry.recurring) {
      setEditScope(null);
      setEditingEntry(entry);
      return;
    }
    setScopeAction({ type: "edit", entry });
  }
  function requestDelete(entry) {
    if (!entry.recurring) {
      setEntries((prev) => prev.filter((e) => e.id !== entry.id));
      return;
    }
    setScopeAction({ type: "delete", entry });
  }
  function handleScopeChoice(scope) {
    const { type, entry } = scopeAction;
    setScopeAction(null);
    if (type === "delete") {
      if (scope === "this") addExceptionDeleted(entry.baseId, entry.occurrenceMonth);
      else if (scope === "future") setEndMonthBefore(entry.baseId, entry.occurrenceMonth);
      else if (scope === "all") deleteBaseEntirely(entry.baseId);
      return;
    }
    setEditScope(scope);
    setEditingEntry(entry);
  }
  function handleEditSave(values) {
    if (!editingEntry) return;
    if (!editingEntry.recurring) {
      updateSimpleEntry(editingEntry.id, values);
    } else if (editScope === "this") {
      setExceptionOverride(editingEntry.baseId, editingEntry.occurrenceMonth, values);
    } else if (editScope === "future") {
      splitSeries(editingEntry.baseId, editingEntry.occurrenceMonth, values);
    }
    setEditingEntry(null);
    setEditScope(null);
  }

  function logout() { supabase.auth.signOut(); }
  function handleRenameUser(newName) {
    setDisplayName(newName);
    supabase.auth.updateUser({ data: { name: newName } });
  }
  function handleBankImport(newEntries) {
    setEntries((prev) => [...prev, ...newEntries]);
  }
  function handleClearData() {
    setEntries([]);
  }
  function goPrevMonth() { setMonth((m) => shiftMonth(m, -1)); }
  function goNextMonth() { setMonth((m) => shiftMonth(m, 1)); }

  function exportData() {
    const blob = new Blob([JSON.stringify(entries, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url; a.download = `masrofi-backup-${month}.json`; a.click();
    URL.revokeObjectURL(url);
  }
  function importData(e) {
    const file = e.target.files && e.target.files[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => {
      try {
        const parsed = JSON.parse(String(reader.result));
        if (Array.isArray(parsed)) setEntries(parsed);
      } catch { alert(t("importError", lang)); }
    };
    reader.readAsText(file);
    e.target.value = "";
  }

  if (!authChecked) {
    return <div className="auth-screen"><div className="app-loading">{t("loadingLabel", lang)}</div></div>;
  }
  if (!session) {
    return <AuthScreen />;
  }
  if (dataLoading) {
    return <div className="auth-screen"><div className="app-loading">{t("loadingLabel", lang)}</div></div>;
  }

  const prevGlyph = DIR_MAP[lang] === "rtl" ? "›" : "‹";
  const nextGlyph = DIR_MAP[lang] === "rtl" ? "‹" : "›";

  return (
    <div className="app">
      {PLANER_FEATURE_ENABLED && settings.planerEnabled && (
        <PlanerAssistant
          entries={entries}
          monthEntries={monthEntries}
          monthLabelText={monthLabel(month, lang)}
          lang={lang}
          onAddEntry={handleAddEntry}
          onDeleteLastEntry={deleteEntryById}
          onUpdateLastEntryAmount={updateLastEntryAmount}
          onSetShift={setShiftForDate}
          onChangeSetting={handleSettingChange}
          currentSettings={settings}
          goals={goals}
          onAddGoal={voiceAddGoal}
          onFindGoalByName={findGoalByName}
          onDeleteGoalById={deleteGoalById}
          credits={credits}
          onAddCredit={voiceAddCredit}
          onFindCreditByName={findCreditByName}
          onDeleteCreditById={deleteCreditById}
          isPremium={isPremium}
          shiftSchedules={shiftSchedules}
          chatHistory={planerChat}
          onAppendChat={appendPlanerChatMessage}
          onClearChat={clearPlanerChat}
          session={session}
        />
      )}
      <div className="user-bar" style={{ paddingTop: 14, paddingBottom: 10 }}>
        <span>{t("greeting", lang)} <strong style={{ color: "#B8FF00" }}>{displayName || email}</strong></span>
      </div>

      {syncBlocked && (
        <div className="sync-blocked-banner">
          <strong>{t("syncBlockedTitle", lang)}</strong>
          <p>{t("syncBlockedBody", lang)}</p>
          <button type="button" className="ghost-btn" onClick={retrySync}>{t("syncBlockedRetryBtn", lang)}</button>
        </div>
      )}

      <header className="topbar">
        <div className="brand">
          <span className="brand-mark"><Icon name="moneyBox" size={16} /></span>
          <div><h1>{t("appName", lang)}</h1><p>{t("tagline", lang)}</p></div>
        </div>
        <div className="top-actions">
          <button type="button" className="quick-fab" onClick={() => setShowSearch(true)} aria-label={t("searchAria", lang)} title={t("searchAria", lang)}>
            <Icon name="search" size={16} />
          </button>
          <button type="button" className="quick-fab quick-fab--bell" onClick={openNotifications} aria-label={t("notificationsTitle", lang)} title={t("notificationsTitle", lang)}>
            <Icon name="bell" size={16} />
            {unreadNotifCount > 0 && <span className="notif-badge">{unreadNotifCount > 9 ? "9+" : unreadNotifCount}</span>}
          </button>
          <button type="button" className="quick-fab" onClick={() => setShowSettings(true)} aria-label={t("settingsAria", lang)} title={t("settingsAria", lang)}>
            <Icon name="sliders" size={16} />
          </button>
        </div>
      </header>

      <div className="month-nav">
        <button type="button" className="month-arrow" onClick={goPrevMonth} aria-label={t("prevMonth", lang)}>{prevGlyph}</button>
        <div className="month-picker">
          <select value={month} onChange={(e) => setMonth(e.target.value)}>
            {availableMonths.map((m) => <option key={m} value={m}>{monthLabel(m, lang)}</option>)}
          </select>
        </div>
        <button type="button" className="month-arrow" onClick={goNextMonth} aria-label={t("nextMonth", lang)}>{nextGlyph}</button>
      </div>

      <div className="tab-content" key={activeTab}>
        {activeTab === "home" && (
          <>
            <SafeToSpendCard entries={entries} settings={settings} goals={goals} isPremium={isPremium} onPaywall={() => setPaywallFeature("safe")} />
            <AffordabilityLauncherCard onOpen={() => setShowAffordability(true)} />
            <PendingPurchasesCard
              pendingPurchases={settings.pendingPurchases}
              onBought={handlePendingBought}
              onSkipped={handlePendingSkipped}
              onKeepWaiting={handlePendingKeepWaiting}
            />
             <DienstplanReminderCard
               shiftSchedules={shiftSchedules}
               onOpen={() => { setActiveTab("goals"); setDienstplanOpen(true); }}
             />

            <section className="hero" style={{ marginTop: 18 }}>
              <SavingsRing ratio={savingsRatio} balance={balance} />
              <div className={`stat-chips ${savingsAllocated > 0 ? "stat-chips--with-savings" : ""}`}>
                <div className="chip chip--income"><span className="chip-label">{t("incomeLabel", lang)}</span><span className="chip-value">{currencyFmt(income, lang)}</span></div>
                <div className="chip chip--expense"><span className="chip-label">{t("expenseLabel", lang)}</span><span className="chip-value">{currencyFmt(expenses, lang)}</span></div>
                {savingsAllocated > 0 && (
                  <div className="chip chip--savings"><span className="chip-label">{t("reportSavingsAllocated", lang)}</span><span className="chip-value">{currencyFmt(savingsAllocated, lang)}</span></div>
                )}
                <div className={`chip ${balance >= 0 ? "chip--balance-pos" : "chip--balance-neg"}`}><span className="chip-label">{t("balanceLabel", lang)}</span><span className="chip-value">{currencyFmt(balance, lang)}</span></div>
              </div>
              {savingsAllocated > 0 && (
                <div className="hero-after-savings">
                  {t("cashAfterSavings", lang)}: <strong>{currencyFmt(balance - savingsAllocated, lang)}</strong>
                </div>
              )}
            </section>

            <UpcomingExpensesCard entries={entries} />
            <SponsoredCard ad={activeAd} onDismiss={dismissActiveAd} />
          </>
        )}

        {activeTab === "stats" && (
          <>
            <MonthlyReportCard entries={entries} month={month} isPremium={isPremium} onPaywall={() => setPaywallFeature("report")} />

            <YearSummaryCard
              entries={entries}
              year={reportYear}
              onYearChange={setReportYear}
              availableYears={availableYears}
              selectedMonthKey={month}
              onSelectMonth={setMonth}
              isPremium={isPremium}
              onPaywall={() => setPaywallFeature("yearSummary")}
            />

            <section className="panel">
              <h2>{t("trendChartTitle", lang)}</h2>
              <MonthlyTrendChart entries={entries} month={month} />
            </section>

            <section className="panel">
              <h2>{t("categoryBreakdownTitle", lang)}</h2>
              <CategoryDonutChart rows={categoryRows} onSelectCategory={setSelectedCategory} />
              <div style={{ marginTop: 18 }}><CategoryBars rows={categoryRows} onSelectCategory={setSelectedCategory} /></div>
            </section>

            <section className="panel">
              <h2>{t("termBreakdownTitle", lang)}</h2>
              <TermBreakdown totals={termTotals} />
            </section>

            <MonthlyCommitmentsCard entries={entries} currentIncome={currentMonthIncome} />
          </>
        )}

        {activeTab === "goals" && (
          <>
            <GoalsSection
              goals={goals}
              onAdd={() => setGoalForm("new")}
              onEdit={(g) => setGoalForm(g)}
              onDelete={deleteGoal}
            />
            <CreditsSection
              credits={credits}
              isPremium={isPremium}
              onPaywall={() => setPaywallFeature("credits")}
              onAdd={() => setCreditForm("new")}
              onEdit={(c) => setCreditForm(c)}
              onDelete={deleteCredit}
            />
            <DienstplanCard
              month={month}
              shiftSchedules={shiftSchedules}
              onSaveMonth={saveShiftMonth}
              isPremium={isPremium}
              onPaywall={() => setPaywallFeature("dienstplan")}
               openOnMount={dienstplanOpen}
               onCloseDienstplan={() => setDienstplanOpen(false)}
               shiftCalc={settings.shiftCalc}
               shiftCalcOverrides={settings.shiftCalcOverrides}
               onSaveShiftCalc={(calc, scope, forMonth) => {
                 setSettings((prev) => {
                   if (scope === "this") {
                     return { ...prev, shiftCalcOverrides: { ...(prev.shiftCalcOverrides || {}), [forMonth]: calc } };
                   }
                   return { ...prev, shiftCalc: calc };
                 });
               }}
               onAddIncome={addEntry}
               scanEnabled={settings.dienstplanScanEnabled}
               accessToken={session && session.access_token}
               employeeName={settings.dienstplanEmployeeName}
               onSaveEmployeeName={(name) => setSettings((prev) => ({ ...prev, dienstplanEmployeeName: name }))}
            />
          </>
        )}

        {activeTab === "transactions" && (
          <>
            <section className="panel safe-panel">
              <button type="button" className="safe-toggle" onClick={() => setShowAddEntry((v) => !v)}>
                <div className="safe-icon"><Icon name="edit" size={20} /></div>
                <div className="safe-main">
                  <div className="safe-label">{t("addEntryTitle", lang)}</div>
                </div>
                <span className={`safe-chevron ${showAddEntry ? "safe-chevron--open" : ""}`}>‹</span>
              </button>
              {showAddEntry && (
                <div className="safe-breakdown">
                  <div className="panel-header-row" style={{ marginBottom: 6 }}>
                    <span />
                    <div style={{ display: "flex", gap: 6 }}>
                      {settings.receiptScanEnabled && (
                        <button type="button" className="quick-fab" onClick={() => setShowReceiptScan(true)} aria-label={t("receiptScanTitle", lang)} title={t("receiptScanTitle", lang)}>
                          <Icon name="camera" size={16} />
                        </button>
                      )}
                      <button type="button" className="quick-fab" onClick={() => setShowQuick((v) => !v)} aria-label={t("quickAddAria", lang)} title={t("quickAddAria", lang)}>
                        <Icon name="bolt" size={16} />
                      </button>
                    </div>
                  </div>
                  {showQuick ? (
                    <QuickExpenseForm onAdd={addEntry} onClose={() => setShowQuick(false)} />
                  ) : (
                    <EntryForm onSubmit={handleAddEntry} defaultMonth={month} />
                  )}
                </div>
              )}
            </section>

            <section className="panel">
              <div className="panel-header-row"><h2>{t("transactionsTitlePrefix", lang)} {monthLabel(month, lang)}</h2></div>
              <EntryList entries={monthEntries} onEdit={requestEdit} onDelete={requestDelete} limit={5} viewAllLabel={t("viewAllTransactionsBtn", lang)} />
            </section>

            <footer className="footer">
              <p>{t("footerNote", lang)}</p>
              <div className="footer-actions">
                <button onClick={() => setShowImportOptions(true)} className="ghost-btn">
                  <Icon name="upload" size={14} /> {t("importMenuBtn", lang)}
                </button>
                <button onClick={exportData} className="ghost-btn">{t("exportBtn", lang)}</button>
                <input ref={fileInputRef} type="file" accept="application/json" onChange={importData} hidden />
              </div>
            </footer>
          </>
        )}
      </div>

      <nav className="tab-bar">
        <button type="button" className={`tab-bar-item ${activeTab === "home" ? "tab-bar-item--active" : ""}`} onClick={() => setActiveTab("home")}>
          <span className="tab-bar-icon"><Icon name="home" size={26} /></span>
          <span>{t("tabHome", lang)}</span>
        </button>
        <button type="button" className={`tab-bar-item ${activeTab === "stats" ? "tab-bar-item--active" : ""}`} onClick={() => setActiveTab("stats")}>
          <span className="tab-bar-icon"><Icon name="barChart" size={26} /></span>
          <span>{t("tabStats", lang)}</span>
        </button>
        <button type="button" className={`tab-bar-item ${activeTab === "goals" ? "tab-bar-item--active" : ""}`} onClick={() => setActiveTab("goals")}>
          <span className="tab-bar-icon"><Icon name="target" size={26} /></span>
          <span>{t("tabGoals", lang)}</span>
        </button>
        <button type="button" className={`tab-bar-item ${activeTab === "transactions" ? "tab-bar-item--active" : ""}`} onClick={() => setActiveTab("transactions")}>
          <span className="tab-bar-icon"><Icon name="receipt" size={26} /></span>
          <span>{t("tabTransactions", lang)}</span>
        </button>
      </nav>

      {editingEntry && (
        <EditModal
          entry={editingEntry}
          hideRecurring={!!editingEntry.recurring}
          onSave={handleEditSave}
          onClose={() => { setEditingEntry(null); setEditScope(null); }}
        />
      )}
      {scopeAction && (
        <ScopeModal
          kind={scopeAction.type}
          onChoose={handleScopeChoice}
          onClose={() => setScopeAction(null)}
        />
      )}
      {showSettings && (
        <SettingsModal
          displayName={displayName}
          subscription={subscription}
          settings={settings}
          onSettingsChange={setSettings}
          onRenameUser={handleRenameUser}
          onClearData={handleClearData}
          onUpgradeClick={() => setPaywallFeature("settings")}
           onLogout={logout}
          onClose={() => setShowSettings(false)}
        />
      )}
      {showImportOptions && (
        <ImportOptionsModal
          onChooseBank={() => { setShowImportOptions(false); setShowImportBank(true); }}
          onChooseBackup={() => { setShowImportOptions(false); fileInputRef.current && fileInputRef.current.click(); }}
          onClose={() => setShowImportOptions(false)}
        />
      )}
      {showImportBank && (
        <ImportBankModal onImport={handleBankImport} onClose={() => setShowImportBank(false)} />
      )}
      {showSearch && (
        <SearchModal
          allEntries={allEntries}
          onEdit={(entry) => { setShowSearch(false); requestEdit(entry); }}
          onDelete={requestDelete}
          onClose={() => setShowSearch(false)}
        />
      )}
      {showNotifications && (
        <NotificationsPanel
          notifications={notifications}
          onClose={() => setShowNotifications(false)}
          onClearAll={clearAllNotifications}
        />
      )}
      {goalForm && (
        <GoalFormModal
          initial={goalForm === "new" ? null : goalForm}
          onSave={(values) => (goalForm === "new" ? addGoal(values) : updateGoal(goalForm.id, values))}
          onClose={() => setGoalForm(null)}
        />
      )}
      {creditForm && (
        <CreditFormModal
          initial={creditForm === "new" ? null : creditForm}
          defaultMonth={month}
          onSave={(values) => (creditForm === "new" ? addCredit(values) : updateCredit(creditForm.id, values))}
          onClose={() => setCreditForm(null)}
        />
      )}
      {paywallFeature && (
        <PaywallModal session={session} onClose={() => setPaywallFeature(null)} />
      )}
      {showPremiumWelcome && (
        <PremiumWelcomeModal onClose={() => setShowPremiumWelcome(false)} />
      )}
      {showAffordability && (
        <AffordabilityModal
          entries={entries}
          monthEntries={monthEntries}
          settings={settings}
          goals={goals}
          onAddExpense={addEntry}
          onSaveForLater={addPendingPurchase}
          onClose={() => setShowAffordability(false)}
        />
      )}
      {showReceiptScan && (
        <ReceiptScanModal
          accessToken={session && session.access_token}
          onAddExpense={addEntry}
          onClose={() => setShowReceiptScan(false)}
          month={month}
        />
      )}
      {selectedCategory && (
        <CategoryDetailModal
          categoryId={selectedCategory}
          monthEntries={monthEntries}
          month={month}
          lang={lang}
          onEdit={(entry) => { setSelectedCategory(null); requestEdit(entry); }}
          onDelete={requestDelete}
          onClose={() => setSelectedCategory(null)}
        />
      )}
    </div>
  );
}

/* =========================================================
   Root
   ========================================================= */
function Root() {
  const [lang, setLang] = useState(() => {
    try { return localStorage.getItem(LANG_KEY) || "en"; } catch { return "en"; }
  });

  useEffect(() => {
    document.documentElement.lang = lang;
    document.documentElement.dir = DIR_MAP[lang] || "ltr";
    try { localStorage.setItem(LANG_KEY, lang); } catch {}
  }, [lang]);

  const ctx = useMemo(() => ({ lang, setLang }), [lang]);

  useEffect(() => {
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.register("/sw.js").catch(() => {});
    }
  }, []);

  return (
    <LangContext.Provider value={ctx}>
      <Planner />
    </LangContext.Provider>
  );
}

/* =========================================================
   Error boundary
   ========================================================= */
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null };
  }
  static getDerivedStateFromError(error) {
    return { error };
  }
  componentDidCatch(error, info) {
    console.error("App crashed:", error, info);
  }
  render() {
    if (this.state.error) {
      const err = this.state.error;
      return (
        <div style={{ padding: "40px 20px", textAlign: "center", color: "#FFFFFF", fontFamily: "sans-serif", lineHeight: 1.6 }}>
          <p>حدث خطأ في تحميل التطبيق.<br />تأكد من الاتصال بالإنترنت وأعد فتح الصفحة.</p>
          <pre style={{ color: "#A3A3A3", fontSize: 11, whiteSpace: "pre-wrap", wordBreak: "break-all", textAlign: "start", marginTop: 16 }}>
            {String(err && err.message)}
            {"\n"}
            {String(err && err.stack || "")}
          </pre>
        </div>
      );
    }
    return this.props.children;
  }
}

function renderFatalError(err) {
  document.getElementById("root").innerHTML =
    '<div style="padding:40px 20px;text-align:center;color:#FFFFFF;font-family:sans-serif;line-height:1.6;">' +
    "حدث خطأ في تحميل التطبيق.<br/>تأكد من الاتصال بالإنترنت وأعد فتح الصفحة." +
    '<br/><pre style="color:#A3A3A3;font-size:11px;white-space:pre-wrap;word-break:break-all;text-align:start;margin-top:16px;">' +
    (err && err.message) + "\n" + (err && err.stack || "") +
    "</pre></div>";
}

waitForSupabaseAndBoot(8000)
  .then(() => {
    try {
      ReactDOM.createRoot(document.getElementById("root")).render(
        <ErrorBoundary>
          <Root />
        </ErrorBoundary>
      );
    } catch (err) {
      renderFatalError(err);
    }
  })
  .catch((err) => {
    renderFatalError(err);
  });

window.addEventListener("unhandledrejection", function (e) {
  var root = document.getElementById("root");
  if (root && root.querySelector(".app")) return;
  var reason = e.reason;
  var detail = (reason && (reason.message || String(reason))) || "unknown promise rejection";
  if (reason && reason.stack) detail += "\n" + String(reason.stack).slice(0, 400);
  if (root) {
    root.innerHTML =
      '<div style="padding:40px 20px;text-align:center;color:#FFFFFF;font-family:sans-serif;line-height:1.6;">' +
      "حدث خطأ في تحميل التطبيق.<br/>تأكد من الاتصال بالإنترنت وأعد فتح الصفحة." +
      '<br/><span style="color:#A3A3A3;font-size:11px;white-space:pre-wrap;word-break:break-all;">' + detail + "</span></div>";
  }
});
