(async () => {
  "use strict";

  const STORAGE_KEY = "refone-facture-os-workspace-v1";
  const LEGACY_STORAGE_KEYS = [
    "refone-facture-os-v0"
  ];
  const SYNC_CODE_KEY = "refone-facture-os-sync-code-v1";
  const SYNC_SCHEMA = "refone-facture-os-workspace-v1";
  const ATELIER_SYNC_SCHEMA = "refone-atelier-os-workspace-v2";
  const DEFAULT_SYNC_CODE = "RF-ERWANN";
  const TVA_FRANCHISE_NOTE = "TVA non applicable, art. 293 B du CGI";
  const MIN_SYNC_CODE_LENGTH = 8;
  const REMOTE_ORIGIN = "https://refonetech.com";
  const AUTH_SESSION_KEY = "refone-facture-os-auth-v1";
  const AUTH_USERNAME = "admin";
  const AUTH_PASSWORD_HASH = "430e6d6ab5bad93eaba486226da10ac5ce0f98ecd4c51f04877e4d2c9eb436bc";
  const SYNC_PUSH_DEBOUNCE_MS = 450;
  const SYNC_POLL_INTERVAL_MS = 120000;
  const PURCHASE_VAT_RATE = 20;
  const DEFAULT_MARGIN_RATE = 40;

  const VIEW_META = {
    documents: {
      kicker: "DOCUMENTS",
      title: "Documents actifs",
      copy: "Retrouvez vos devis, bons de commande et factures, puis cherchez instantanement par client, numero ou objet."
    },
    customers: {
      kicker: "CLIENTS",
      title: "Base clients",
      copy: "Conservez une base propre et reutilisable. Les fiches importees depuis AtelierOS arrivent ici sans refaire la saisie."
    },
    settings: {
      kicker: "REGLAGES",
      title: "Configuration ReFone",
      copy: "Renseignez les mentions entreprise, les parametres de paiement et votre catalogue pour sortir des PDF propres."
    }
  };

  const DOCUMENT_TYPES = [
    { id: "quote", label: "Devis", prefix: "DEVIS" },
    { id: "order", label: "Bon de commande", prefix: "BC" },
    { id: "invoice", label: "Facture", prefix: "FACTURE" }
  ];

  const DOCUMENT_LEGACY_PREFIXES = ["DEV", "FAC", "BDC"];
  const DOCUMENT_TYPE_LABELS = Object.fromEntries(DOCUMENT_TYPES.map((item) => [item.id, item.label]));

  const DOCUMENT_STATUS_LABELS = {
    draft: "Brouillon",
    sent: "Envoye",
    approved: "Accepte",
    paid: "Regle",
    cancelled: "Annule"
  };

  const CUSTOMER_TYPE_LABELS = {
    individual: "Particulier",
    business: "Professionnel"
  };

  const PAYMENT_METHODS = ["Espece", "CB", "Cheque", "Virement"];

  const ui = {
    view: "documents",
    search: "",
    documentFilter: "all"
  };

  const elements = {
    authGate: document.querySelector("[data-auth-gate]"),
    authShell: document.querySelector("[data-auth-shell]"),
    authForm: document.querySelector("[data-auth-form]"),
    authUsername: document.querySelector("[data-auth-username]"),
    authPassword: document.querySelector("[data-auth-password]"),
    authError: document.querySelector("[data-auth-error]"),
    panelKicker: document.querySelector("[data-panel-kicker]"),
    panelTitle: document.querySelector("[data-panel-title]"),
    panelCopy: document.querySelector("[data-panel-copy]"),
    listCount: document.querySelector("[data-list-count]"),
    searchInput: document.querySelector("[data-search-input]"),
    documentFilters: document.querySelector("[data-document-filters]"),
    list: document.querySelector("[data-list]"),
    detail: document.querySelector("[data-detail]"),
    stats: document.querySelector("[data-stats]"),
    syncSummary: document.querySelector("[data-sync-summary]"),
    syncModal: document.querySelector("[data-sync-modal]"),
    syncInput: document.querySelector("[data-sync-code]"),
    syncStatus: document.querySelector("[data-sync-status]"),
    documentModal: document.querySelector("[data-document-modal]"),
    customerModal: document.querySelector("[data-customer-modal]"),
    documentCreateForm: document.querySelector("[data-document-create-form]"),
    customerCreateForm: document.querySelector("[data-customer-create-form]"),
    documentCreateCustomer: document.querySelector("[data-document-create-customer]"),
    toast: document.querySelector("[data-toast]")
  };

  const syncRuntime = {
    apiUrl: resolveApiUrl("/api/facture-os-sync"),
    atelierApiUrl: resolveApiUrl("/api/atelier-os-sync"),
    lastRemoteUpdatedAt: "",
    eventSource: null,
    streamConnected: false,
    pushTimer: 0,
    pollTimer: 0,
    isApplyingRemote: false,
    isPushing: false
  };

  let toastTimer = 0;
  let authFormBound = false;
  let formsBound = false;
  let appBooted = false;
  const state = loadState();

  function createDefaultCompany() {
    const company = {
      legalName: "ReFone",
      businessType: "Entrepreneur individuel",
      address: "12 Yellenn 22140 Bégard",
      locality: "Bégard, France",
      email: "contact@refonetech.com",
      phone: "07 64 51 79 51",
      siret: "10075619600018",
      siren: "100756196",
      rib: "FR7640618805050004012411762",
      vatNumber: "",
      vatMode: "franchise",
      vatNote: TVA_FRANCHISE_NOTE,
      defaultMarginRate: DEFAULT_MARGIN_RATE,
      paymentTerms: "Paiement a reception de facture, sauf accord contraire indique sur le document.",
      paymentMethod: "Virement",
      latePenalty: "En cas de retard de paiement, penalites exigibles au taux legal en vigueur et indemnite forfaitaire de recouvrement de 20 EUR",
      footerNote: "Aucun escompte pour paiement anticipe.",
      eInvoiceStatus: "PDF metier pret. PDP a connecter plus tard pour la facturation electronique B2B France."
    };
    company.termsText = defaultTermsText(company);
    return company;
  }

  function defaultTermsText(company = {}) {
    const legalName = company.legalName || "ReFone";
    const address = company.address || "12 Yellenn 22140 Bégard";
    const siret = company.siret || "10075619600018";
    const email = company.email || "contact@refonetech.com";
    const phone = company.phone || "07 64 51 79 51";
    return [
      "CONDITIONS GENERALES DE VENTE - ReFone",
      `1. Identite du prestataire\n${legalName} - Erwann Faria EI. Adresse : ${address}. SIRET : ${siret}. Email : ${email}. Telephone : ${phone}. Site internet : refonetech.com.`,
      "2. Prestations proposees\nReFone propose des prestations de diagnostic, reparation, remplacement de pieces, entretien et assistance sur smartphones, tablettes et appareils electroniques, notamment de marque Apple. Les prestations peuvent inclure ecran, batterie, connecteur de charge, camera, haut-parleur, nettoyage, recuperation de donnees ou autre intervention technique.",
      "3. Devis et acceptation\nToute intervention peut faire l objet d un devis prealable. Le client est informe du prix avant realisation. L acceptation du devis, orale, ecrite, par message ou par depot de l appareil, vaut acceptation des presentes conditions generales de vente. Si un probleme supplementaire est decouvert, ReFone informe le client avant toute reparation avec cout supplementaire.",
      "4. Tarifs et paiement\nLes prix sont indiques en euros. Ils peuvent varier selon le modele, la disponibilite des pieces et la nature de la panne. Le paiement est exigible a la recuperation de l appareil, sauf accord contraire. Moyens acceptes : carte bancaire, especes, virement ou tout autre moyen accepte par ReFone. En cas de non-paiement, l appareil pourra etre conserve jusqu au reglement complet. TVA non applicable, article 293 B du Code general des impots, sauf changement de regime fiscal.",
      "5. Delais d intervention\nLes delais annonces sont indicatifs. Ils peuvent varier selon la disponibilite des pieces, la complexite de la reparation ou la charge de travail. Un retard raisonnable ne peut pas donner lieu a une annulation automatique, une remise obligatoire ou une indemnisation, sauf accord ecrit.",
      "6. Garantie des reparations\nLes reparations realisees par ReFone sont garanties 3 mois a compter de la date de facturation, uniquement sur la piece remplacee et la main-d'oeuvre liee. La garantie ne couvre pas la casse, chute, choc, oxydation, liquide, mauvaise utilisation, intervention d un tiers, panne non liee a la prestation ou piece fournie par le client. Toute demande doit etre accompagnee de la facture.",
      "7. Donnees personnelles\nLe client reste responsable de ses donnees, photos, fichiers, mots de passe et sauvegardes. Une sauvegarde complete est recommandee avant intervention. ReFone ne peut etre tenu responsable d une perte de donnees, sauf faute prouvee. Le client fournit les informations necessaires, notamment le code de deverrouillage si besoin. ReFone s engage a ne pas consulter les donnees hors necessite technique.",
      "8. Pieces detachees\nSelon la demande et la disponibilite, les pieces peuvent etre d origine, compatibles, reconditionnees ou equivalentes. Le type de piece est precise au client lorsque necessaire. Une piece compatible peut presenter de legeres differences avec une piece d origine, notamment affichage, luminosite, autonomie ou finition.",
      "9. Appareils non recuperes\nLe client est informe lorsque son appareil est pret. En cas de non-recuperation sous 30 jours apres notification, ReFone pourra relancer le client. Au-dela de 90 jours sans reponse, des frais de garde pourront etre appliques, sauf force majeure ou accord ecrit.",
      `10. Reclamations et litiges\nToute reclamation peut etre adressee a ${email}. Une solution amiable sera recherchee en priorite. A defaut d accord amiable, le litige pourra etre porte devant les juridictions competentes selon les regles legales applicables.`,
      "11. Acceptation des CGV\nLe depot d un appareil, l acceptation d un devis, le paiement d une facture ou la commande d une prestation implique l acceptation pleine et entiere des presentes conditions generales de vente. ReFone - Erwann Faria EI."
    ].join("\n\n");
  }

  function normalizeTermsText(value, company = {}) {
    return String(value || defaultTermsText(company))
      .replace(/\r\n?/g, "\n")
      .trim()
      .slice(0, 14000);
  }

  function createEmptyWorkspace() {
    return {
      selectedDocumentId: "",
      selectedCustomerId: "",
      company: createDefaultCompany(),
      customers: [],
      documents: [],
      catalog: [],
      counters: {
        quote: {},
        order: {},
        invoice: {}
      }
    };
  }

  function clone(value) {
    return JSON.parse(JSON.stringify(value));
  }

  function purgeLegacyState() {
    LEGACY_STORAGE_KEYS.forEach((key) => {
      try {
        localStorage.removeItem(key);
      } catch {}
    });
  }

  function normalizeCode(value) {
    return String(value || "")
      .toUpperCase()
      .replace(/[^A-Z0-9-]/g, "")
      .slice(0, 64);
  }

  function normalizeView(value) {
    return ["documents", "customers", "settings"].includes(value) ? value : "documents";
  }

  function normalizeDocumentType(value) {
    return DOCUMENT_TYPES.some((item) => item.id === value) ? value : "quote";
  }

  function normalizeDocumentStatus(value) {
    return DOCUMENT_STATUS_LABELS[value] ? value : "draft";
  }

  function normalizeCustomerType(value) {
    return CUSTOMER_TYPE_LABELS[value] ? value : "individual";
  }

  function normalizePaymentMethod(value) {
    const raw = String(value || "").trim();
    return PAYMENT_METHODS.includes(raw) ? raw : "Virement";
  }

  function normalizeMoney(value) {
    const amount = Number(value);
    if (!Number.isFinite(amount)) return 0;
    return Math.round(amount * 100) / 100;
  }

  function hasInputValue(value) {
    return value !== undefined && value !== null && value !== "";
  }

  function normalizeQty(value) {
    const qty = Number(value);
    if (!Number.isFinite(qty) || qty < 0) return 1;
    return Math.round(qty * 100) / 100;
  }

  function normalizeVatRate(value) {
    const rate = Number(value);
    if (!Number.isFinite(rate) || rate < 0) return 20;
    return Math.min(rate, 100);
  }

  function normalizeMarginRate(value) {
    const rate = Number(value);
    if (!Number.isFinite(rate)) return DEFAULT_MARGIN_RATE;
    return Math.min(Math.max(Math.round(rate * 100) / 100, -100), 1000);
  }

  function purchaseHtFromTtc(value) {
    return normalizeMoney(Number(value || 0) / (1 + PURCHASE_VAT_RATE / 100));
  }

  function purchaseTtcFromHt(value) {
    return normalizeMoney(Number(value || 0) * (1 + PURCHASE_VAT_RATE / 100));
  }

  function resolvePurchaseValues(input = {}) {
    const purchaseHt = hasInputValue(input.purchaseHt)
      ? normalizeMoney(input.purchaseHt)
      : purchaseHtFromTtc(input.purchaseTtc);
    return {
      purchaseHt,
      purchaseTtc: purchaseTtcFromHt(purchaseHt)
    };
  }

  function saleHtFromMargin(purchaseTtc, marginRate) {
    return normalizeMoney(Number(purchaseTtc || 0) * (1 + normalizeMarginRate(marginRate) / 100));
  }

  function marginRateFromSale(purchaseTtc, unitHt) {
    const cost = normalizeMoney(purchaseTtc);
    if (cost <= 0) return DEFAULT_MARGIN_RATE;
    return normalizeMarginRate(((normalizeMoney(unitHt) - cost) / cost) * 100);
  }

  function createId(prefix) {
    if (crypto.randomUUID) {
      return `${prefix}-${crypto.randomUUID()}`;
    }
    return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
  }

  function currentIso() {
    return new Date().toISOString();
  }

  function isoDateOnly(value = new Date()) {
    const date = value instanceof Date ? value : new Date(value);
    if (Number.isNaN(date.getTime())) return new Date().toISOString().slice(0, 10);
    const offset = date.getTimezoneOffset() * 60000;
    return new Date(date.getTime() - offset).toISOString().slice(0, 10);
  }

  function addDaysIsoDate(value, days) {
    const date = new Date(value || currentIso());
    if (Number.isNaN(date.getTime())) return isoDateOnly();
    date.setDate(date.getDate() + days);
    return isoDateOnly(date);
  }

  function normalizeCompany(raw) {
    const input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
    const base = createDefaultCompany();
    const legacyPenalty = "Penalites de retard exigibles selon les conditions convenues entre professionnels.";
    const legacyFooter = "Document prepare par ReFone.";
    const normalized = {
      legalName: String(input.legalName || base.legalName).slice(0, 180),
      businessType: String(input.businessType || base.businessType).slice(0, 180),
      address: String(input.address || base.address).slice(0, 220),
      locality: String(input.locality || base.locality).slice(0, 160),
      email: String(input.email || base.email).slice(0, 220),
      phone: String(input.phone || base.phone).slice(0, 80),
      siret: String(input.siret || base.siret).replace(/\s+/g, "").slice(0, 20),
      siren: String(input.siren || base.siren).replace(/\s+/g, "").slice(0, 20),
      rib: String(input.rib || base.rib).replace(/\s+/g, "").slice(0, 40),
      vatNumber: String(input.vatNumber || "").slice(0, 40),
      vatMode: "franchise",
      vatNote: String(input.vatNote || base.vatNote).replace(/\.$/, "").slice(0, 220),
      defaultMarginRate: normalizeMarginRate(input.defaultMarginRate ?? base.defaultMarginRate),
      paymentTerms: String(input.paymentTerms || base.paymentTerms).slice(0, 240),
      paymentMethod: normalizePaymentMethod(input.paymentMethod || base.paymentMethod),
      latePenalty: String(!input.latePenalty || input.latePenalty === legacyPenalty || String(input.latePenalty).includes("40 EUR") ? base.latePenalty : input.latePenalty).slice(0, 260),
      footerNote: String(!input.footerNote || input.footerNote === legacyFooter ? base.footerNote : input.footerNote).slice(0, 260),
      eInvoiceStatus: String(input.eInvoiceStatus || base.eInvoiceStatus).slice(0, 260)
    };
    normalized.termsText = normalizeTermsText(input.termsText, normalized);
    return normalized;
  }

  function normalizeCustomer(raw, index = 0) {
    const input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
    const createdAt = String(input.createdAt || currentIso()).slice(0, 40);
    const updatedAt = String(input.updatedAt || createdAt).slice(0, 40);
    return {
      id: String(input.id || `customer-${index + 1}`).slice(0, 120),
      name: String(input.name || "Client").slice(0, 180),
      email: String(input.email || "").slice(0, 220),
      phone: String(input.phone || "").slice(0, 80),
      address: String(input.address || "").slice(0, 400),
      customerType: normalizeCustomerType(input.customerType),
      note: String(input.note || "").slice(0, 1200),
      source: String(input.source || "manual").slice(0, 60),
      workshopCode: String(input.workshopCode || "").slice(0, 120),
      deviceHint: String(input.deviceHint || "").slice(0, 160),
      createdAt,
      updatedAt
    };
  }

  function normalizeLine(raw, index = 0) {
    const input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
    const { purchaseHt, purchaseTtc } = resolvePurchaseValues(input);
    const marginRate = hasInputValue(input.marginRate)
      ? normalizeMarginRate(input.marginRate)
      : marginRateFromSale(purchaseTtc, input.unitHt);
    const hasSale = hasInputValue(input.unitHt);
    return {
      id: String(input.id || `line-${index + 1}`).slice(0, 120),
      description: String(input.description || "").slice(0, 260),
      reference: String(input.reference || "").slice(0, 120),
      qty: normalizeQty(input.qty),
      purchaseTtc,
      purchaseHt,
      marginRate,
      unitHt: hasSale ? normalizeMoney(input.unitHt) : saleHtFromMargin(purchaseTtc, marginRate),
      vatRate: normalizeVatRate(input.vatRate)
    };
  }

  function normalizeDocument(raw, index = 0) {
    const input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
    const createdAt = String(input.createdAt || currentIso()).slice(0, 40);
    const updatedAt = String(input.updatedAt || createdAt).slice(0, 40);
    const lines = Array.isArray(input.lines) && input.lines.length
      ? input.lines.slice(0, 80).map(normalizeLine)
      : [normalizeLine({ id: createId("line"), qty: 1, vatRate: 20 }, 0)];

    return {
      id: String(input.id || `document-${index + 1}`).slice(0, 120),
      type: normalizeDocumentType(input.type),
      number: String(input.number || "").slice(0, 80),
      status: normalizeDocumentStatus(input.status),
      title: String(input.title || "").slice(0, 220),
      customerId: String(input.customerId || "").slice(0, 120),
      customerName: String(input.customerName || "").slice(0, 180),
      customerEmail: String(input.customerEmail || "").slice(0, 220),
      customerPhone: String(input.customerPhone || "").slice(0, 80),
      customerAddress: String(input.customerAddress || "").slice(0, 400),
      customerType: normalizeCustomerType(input.customerType),
      deliveryAddress: String(input.deliveryAddress || "").slice(0, 400),
      issueDate: String(input.issueDate || isoDateOnly()).slice(0, 10),
      dueDate: String(input.dueDate || isoDateOnly()).slice(0, 10),
      noteToCustomer: String(input.noteToCustomer || "").slice(0, 1800),
      internalNote: String(input.internalNote || "").slice(0, 1400),
      paymentMethod: input.paymentMethod ? normalizePaymentMethod(input.paymentMethod) : "",
      paymentTerms: String(input.paymentTerms || "").slice(0, 220),
      lines,
      createdAt,
      updatedAt
    };
  }

  function normalizeCatalogItem(raw, index = 0) {
    const input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
    const { purchaseHt, purchaseTtc } = resolvePurchaseValues(input);
    const marginRate = hasInputValue(input.marginRate)
      ? normalizeMarginRate(input.marginRate)
      : marginRateFromSale(purchaseTtc, input.unitHt);
    const hasSale = hasInputValue(input.unitHt);
    return {
      id: String(input.id || `catalog-${index + 1}`).slice(0, 120),
      label: String(input.label || "Prestation").slice(0, 220),
      reference: String(input.reference || "").slice(0, 120),
      purchaseTtc,
      purchaseHt,
      marginRate,
      unitHt: hasSale ? normalizeMoney(input.unitHt) : saleHtFromMargin(purchaseTtc, marginRate),
      vatRate: normalizeVatRate(input.vatRate)
    };
  }

  function normalizeCounters(raw) {
    const input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
    const base = { quote: {}, order: {}, invoice: {} };
    for (const type of Object.keys(base)) {
      const bucket = input[type] && typeof input[type] === "object" && !Array.isArray(input[type]) ? input[type] : {};
      for (const [year, value] of Object.entries(bucket)) {
        base[type][String(year).slice(0, 4)] = Math.max(0, Math.round(Number(value || 0)));
      }
    }
    return base;
  }

  function normalizeWorkspace(raw) {
    const input = raw && typeof raw === "object" && !Array.isArray(raw) ? clone(raw) : {};
    const customers = Array.isArray(input.customers) ? input.customers.slice(0, 1200).map(normalizeCustomer) : [];
    const documents = Array.isArray(input.documents) ? input.documents.slice(0, 1800).map(normalizeDocument) : [];
    const catalog = Array.isArray(input.catalog) ? input.catalog.slice(0, 300).map(normalizeCatalogItem) : [];
    const selectedDocumentId = String(input.selectedDocumentId || "").slice(0, 120);
    const selectedCustomerId = String(input.selectedCustomerId || "").slice(0, 120);
    return {
      selectedDocumentId: documents.some((item) => item.id === selectedDocumentId) ? selectedDocumentId : (documents[0]?.id || ""),
      selectedCustomerId: customers.some((item) => item.id === selectedCustomerId) ? selectedCustomerId : (customers[0]?.id || ""),
      company: normalizeCompany(input.company),
      customers,
      documents,
      catalog,
      counters: normalizeCounters(input.counters)
    };
  }

  function loadState() {
    purgeLegacyState();
    try {
      const saved = localStorage.getItem(STORAGE_KEY);
      if (saved) {
        return normalizeWorkspace(JSON.parse(saved));
      }
    } catch (error) {
      console.warn("Impossible de charger FactureOS", error);
    }
    return createEmptyWorkspace();
  }

  function saveState() {
    try {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(normalizeWorkspace(state)));
    } catch (error) {
      console.warn("Impossible d'enregistrer FactureOS", error);
    }
  }

  function resolveApiUrl(path) {
    const host = String(window.location.hostname || "").toLowerCase();
    const isRemoteHttp = /^https?:$/.test(window.location.protocol) && host && host !== "127.0.0.1" && host !== "localhost";
    const base = isRemoteHttp ? window.location.origin : REMOTE_ORIGIN;
    return new URL(path, base).toString();
  }

  function resolveSiteUrl(path) {
    const host = String(window.location.hostname || "").toLowerCase();
    const isRemoteHttp = /^https?:$/.test(window.location.protocol) && host && host !== "127.0.0.1" && host !== "localhost";
    const base = isRemoteHttp ? window.location.origin : REMOTE_ORIGIN;
    return new URL(path, base).toString();
  }

  function utf8Bytes(value) {
    if (typeof TextEncoder === "function") {
      return new TextEncoder().encode(String(value || ""));
    }
    const encoded = unescape(encodeURIComponent(String(value || "")));
    return Uint8Array.from(encoded, (character) => character.charCodeAt(0));
  }

  async function sha256Hex(value) {
    const digest = await crypto.subtle.digest("SHA-256", utf8Bytes(value));
    return [...new Uint8Array(digest)]
      .map((item) => item.toString(16).padStart(2, "0"))
      .join("");
  }

  function isSessionUnlocked() {
    try {
      return sessionStorage.getItem(AUTH_SESSION_KEY) === "ok";
    } catch {
      return false;
    }
  }

  function storeSessionUnlocked() {
    try {
      sessionStorage.setItem(AUTH_SESSION_KEY, "ok");
    } catch {}
  }

  function clearUnlockedSession() {
    try {
      sessionStorage.removeItem(AUTH_SESSION_KEY);
    } catch {}
  }

  function setAuthError(message = "") {
    if (!elements.authError) return;
    elements.authError.textContent = message;
    elements.authError.hidden = !message;
  }

  function showAuthGate(message = "") {
    document.body.classList.add("is-locked");
    if (elements.authGate) elements.authGate.hidden = false;
    if (elements.authShell) elements.authShell.hidden = true;
    if (elements.authUsername && !elements.authUsername.value) {
      elements.authUsername.value = AUTH_USERNAME;
    }
    if (elements.authPassword) {
      elements.authPassword.value = "";
    }
    setAuthError(message);
    window.setTimeout(() => {
      if (elements.authPassword) {
        elements.authPassword.focus();
      }
    }, 0);
  }

  function hideAuthGate() {
    document.body.classList.remove("is-locked");
    if (elements.authGate) elements.authGate.hidden = true;
    if (elements.authShell) elements.authShell.hidden = false;
    setAuthError("");
  }

  function isPublicWebVersion() {
    const host = String(window.location.hostname || "").toLowerCase();
    return window.location.protocol === "https:" && host !== "localhost" && host !== "127.0.0.1";
  }

  function escapeHtml(value) {
    return String(value ?? "")
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#39;");
  }

  function nl2br(value) {
    return escapeHtml(value).replace(/\n/g, "<br>");
  }

  function currency(value) {
    return new Intl.NumberFormat("fr-FR", {
      style: "currency",
      currency: "EUR"
    }).format(Number(value || 0));
  }

  function shortDate(value) {
    if (!value) return "Non renseigne";
    const date = new Date(value);
    if (Number.isNaN(date.getTime())) return "Non renseigne";
    return new Intl.DateTimeFormat("fr-FR", { dateStyle: "medium" }).format(date);
  }

  function longDate(value) {
    if (!value) return "Non renseigne";
    const date = new Date(value);
    if (Number.isNaN(date.getTime())) return "Non renseigne";
    return new Intl.DateTimeFormat("fr-FR", { dateStyle: "medium", timeStyle: "short" }).format(date);
  }

  function formatDateInput(value) {
    return String(value || "").slice(0, 10);
  }

  function documentTypeMeta(type) {
    return DOCUMENT_TYPES.find((item) => item.id === type) || DOCUMENT_TYPES[0];
  }

  function escapeRegExp(value) {
    return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  }

  function knownDocumentPrefixes() {
    return [
      ...DOCUMENT_TYPES.map((item) => item.prefix),
      ...DOCUMENT_LEGACY_PREFIXES
    ].filter(Boolean).sort((first, second) => second.length - first.length);
  }

  function stripKnownDocumentPrefix(value) {
    let raw = String(value || "").trim();
    for (const prefix of knownDocumentPrefixes()) {
      const pattern = new RegExp(`^${escapeRegExp(prefix)}[-_\\s]*`, "i");
      if (pattern.test(raw)) {
        raw = raw.replace(pattern, "");
        break;
      }
    }
    return raw.replace(/^[-_\s]+/, "").trim();
  }

  function documentDisplayNumber(documentItem) {
    const type = normalizeDocumentType(documentItem?.type);
    const suffix = stripKnownDocumentPrefix(documentItem?.number || "");
    if (!suffix) return "";
    return `${documentTypeMeta(type).prefix}-${suffix}`;
  }

  function documentExportFilename(documentItem) {
    const type = normalizeDocumentType(documentItem?.type);
    const suffix = stripKnownDocumentPrefix(documentItem?.number || documentItem?.title || isoDateOnly());
    return `${documentTypeMeta(type).prefix}-${sanitizeFilename(suffix || isoDateOnly())}.pdf`;
  }

  function selectedDocument() {
    return state.documents.find((item) => item.id === state.selectedDocumentId) || null;
  }

  function selectedCustomer() {
    return state.customers.find((item) => item.id === state.selectedCustomerId) || null;
  }

  function normalizeSelections() {
    if (!state.documents.some((item) => item.id === state.selectedDocumentId)) {
      state.selectedDocumentId = state.documents[0]?.id || "";
    }
    if (!state.customers.some((item) => item.id === state.selectedCustomerId)) {
      state.selectedCustomerId = state.customers[0]?.id || "";
    }
  }

  function customerNameForDocument(documentItem) {
    return documentItem.customerName || "Client non renseigne";
  }

  function searchHaystack(parts) {
    return parts.filter(Boolean).join(" ").toLowerCase();
  }

  function filteredDocuments() {
    const query = ui.search.trim().toLowerCase();
    return clone(state.documents)
      .filter((item) => ui.documentFilter === "all" || item.type === ui.documentFilter)
      .filter((item) => {
        if (!query) return true;
        return searchHaystack([
          documentDisplayNumber(item) || item.number,
          documentDisplayTitle(item) || item.title,
          item.customerName,
          item.customerEmail,
          item.customerPhone,
          item.customerAddress,
          item.noteToCustomer
        ]).includes(query);
      })
      .sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime());
  }

  function filteredCustomers() {
    const query = ui.search.trim().toLowerCase();
    return clone(state.customers)
      .filter((item) => {
        if (!query) return true;
        return searchHaystack([
          item.name,
          item.email,
          item.phone,
          item.address,
          item.note,
          item.workshopCode,
          item.deviceHint
        ]).includes(query);
      })
      .sort((left, right) => left.name.localeCompare(right.name, "fr"));
  }

  function documentTotals(documentItem) {
    const lines = Array.isArray(documentItem?.lines) ? documentItem.lines : [];
    let totalHt = 0;
    let totalVat = 0;
    let totalPurchase = 0;

    for (const line of lines) {
      const qty = normalizeQty(line.qty);
      const saleHt = normalizeMoney(line.unitHt) * qty;
      const { purchaseTtc } = resolvePurchaseValues(line);
      const purchaseCost = purchaseTtc * qty;
      const vat = saleHt * (normalizeVatRate(line.vatRate) / 100);
      totalHt += saleHt;
      totalVat += vat;
      totalPurchase += purchaseCost;
    }

    totalHt = normalizeMoney(totalHt);
    totalVat = state.company.vatMode === "franchise" ? 0 : normalizeMoney(totalVat);
    const totalTtc = normalizeMoney(totalHt + totalVat);
    const marginHt = normalizeMoney(totalHt - totalPurchase);
    return {
      totalHt,
      totalVat,
      totalTtc,
      totalPurchase: normalizeMoney(totalPurchase),
      marginHt
    };
  }

  function statsModel() {
    const totals = state.documents.reduce((acc, item) => {
      const docTotals = documentTotals(item);
      acc.totalTtc += docTotals.totalTtc;
      acc.marginHt += docTotals.marginHt;
      if (item.status === "paid") acc.paidCount += 1;
      if (item.status === "sent") acc.sentCount += 1;
      return acc;
    }, { totalTtc: 0, marginHt: 0, paidCount: 0, sentCount: 0 });

    return [
      {
        label: "Documents",
        value: state.documents.length,
        meta: "Devis, bons de commande et factures."
      },
      {
        label: "Clients",
        value: state.customers.length,
        meta: "Base client reutilisable."
      },
      {
        label: "Total TTC",
        value: currency(totals.totalTtc),
        meta: "Cumul de tous les documents."
      },
      {
        label: "Marge interne",
        value: currency(totals.marginHt),
        meta: `${totals.paidCount} regles · ${totals.sentCount} envoyes`
      }
    ];
  }

  function documentWarnings(documentItem) {
    if (!documentItem) return [];
    const warnings = [];
    if (!state.company.legalName) warnings.push("Nom de l'entreprise manquant.");
    if (!state.company.address) warnings.push("Adresse de facturation ReFone a completer.");
    if (!state.company.siret) warnings.push("SIRET manquant.");
    if (!documentItem.customerName) warnings.push("Client non renseigne.");
    if (!documentItem.customerAddress) warnings.push("Adresse client non renseignee.");
    if (!documentItem.title) warnings.push("Objet du document vide.");
    if (!documentItem.lines.some((line) => line.description && normalizeQty(line.qty) > 0 && normalizeMoney(line.unitHt) >= 0)) {
      warnings.push("Ajoutez au moins une ligne de prestation ou de piece.");
    }
    if (documentItem.type === "invoice" && state.company.vatMode === "normal" && !state.company.vatNumber) {
      warnings.push("Numero de TVA a verifier si ReFone facture avec TVA.");
    }
    return warnings;
  }

  function renderStats() {
    elements.stats.innerHTML = statsModel()
      .map((item) => `
        <article class="facture-stat-card">
          <span class="facture-stat-label">${escapeHtml(item.label)}</span>
          <p class="facture-stat-value">${escapeHtml(item.value)}</p>
          <p class="facture-stat-meta">${escapeHtml(item.meta)}</p>
        </article>
      `)
      .join("");
  }

  function updateViewMeta() {
    const meta = VIEW_META[ui.view] || VIEW_META.documents;
    if (elements.panelKicker) elements.panelKicker.textContent = meta.kicker;
    if (elements.panelTitle) elements.panelTitle.textContent = meta.title;
    if (elements.panelCopy) elements.panelCopy.textContent = meta.copy;

    document.querySelectorAll("[data-view]").forEach((button) => {
      button.classList.toggle("is-active", button.dataset.view === ui.view);
    });

    document.querySelectorAll("[data-document-filter]").forEach((button) => {
      button.classList.toggle("is-active", button.dataset.documentFilter === ui.documentFilter);
    });

    if (elements.documentFilters) {
      elements.documentFilters.hidden = ui.view !== "documents";
    }

    if (elements.searchInput) {
      elements.searchInput.placeholder = ui.view === "settings"
        ? "Recherche rapide dans les reglages et le catalogue..."
        : ui.view === "customers"
          ? "Nom, email, telephone, adresse..."
          : "Client, numero, objet, email...";
    }
  }

  function documentListCountLabel(count) {
    return `${count} ${count > 1 ? "documents" : "document"}`;
  }

  function customerListCountLabel(count) {
    return `${count} ${count > 1 ? "clients" : "client"}`;
  }

  function settingsCountLabel() {
    return "4 blocs";
  }

  function renderDocumentList() {
    const documents = filteredDocuments();
    if (elements.listCount) {
      elements.listCount.textContent = documentListCountLabel(documents.length);
    }

    if (!documents.length) {
      elements.list.innerHTML = `
        <article class="facture-empty-card">
          <h3>Aucun document pour le moment</h3>
          <p class="facture-empty-text">Crez un devis, un bon de commande ou une facture propre, puis synchronisez-le sur tous vos supports.</p>
          <div class="facture-inline-actions">
            <button type="button" class="facture-primary-button" data-open-document="quote">Creer un devis</button>
            <button type="button" class="facture-secondary-button" data-open-customer-modal>Nouveau client</button>
          </div>
        </article>
      `;
      return;
    }

    elements.list.innerHTML = documents
      .map((item) => {
        const totals = documentTotals(item);
        const displayNumber = documentDisplayNumber(item) || item.number || documentTypeMeta(item.type).prefix;
        const displayTitle = documentDisplayTitle(item) || DOCUMENT_TYPE_LABELS[item.type];
        return `
          <button type="button" class="facture-card ${item.id === state.selectedDocumentId ? "is-selected" : ""}" data-select-document="${escapeHtml(item.id)}">
            <div class="facture-card-top">
              <div class="facture-card-main">
                <span class="facture-card-code">${escapeHtml(displayNumber)}</span>
                <strong class="facture-card-title">${escapeHtml(displayTitle)}</strong>
                <span class="facture-card-meta">${escapeHtml(customerNameForDocument(item))}</span>
              </div>
              <div class="facture-detail-stack">
                <span class="facture-chip" data-type="${escapeHtml(item.type)}">${escapeHtml(DOCUMENT_TYPE_LABELS[item.type])}</span>
                <span class="facture-status-chip" data-status="${escapeHtml(item.status)}">${escapeHtml(DOCUMENT_STATUS_LABELS[item.status])}</span>
              </div>
            </div>
            <div class="facture-card-bottom">
              <span class="facture-card-meta">${escapeHtml(shortDate(item.issueDate))}</span>
              <strong>${escapeHtml(currency(totals.totalTtc))}</strong>
            </div>
          </button>
        `;
      })
      .join("");
  }

  function renderCustomerList() {
    const customers = filteredCustomers();
    if (elements.listCount) {
      elements.listCount.textContent = customerListCountLabel(customers.length);
    }

    elements.list.innerHTML = `
      <button type="button" class="facture-card" data-open-customer-modal>
        <div class="facture-card-main">
          <span class="facture-card-code">BASE CLIENT</span>
          <strong class="facture-card-title">Nouveau client</strong>
          <span class="facture-card-meta">Creer une fiche propre a reutiliser dans chaque devis ou facture.</span>
        </div>
      </button>
      ${customers.length ? customers.map((item) => {
        const docCount = state.documents.filter((documentItem) => documentItem.customerId === item.id).length;
        return `
          <button type="button" class="facture-card ${item.id === state.selectedCustomerId ? "is-selected" : ""}" data-select-customer="${escapeHtml(item.id)}">
            <div class="facture-card-top">
              <div class="facture-card-main">
                <span class="facture-card-code">${escapeHtml(item.source === "atelier-os" ? "ATELIEROS" : "CLIENT")}</span>
                <strong class="facture-card-title">${escapeHtml(item.name)}</strong>
                <span class="facture-card-meta">${escapeHtml(item.email || item.phone || "Coordonnees a completer")}</span>
              </div>
              <span class="facture-alert-chip">${escapeHtml(`${docCount} doc${docCount > 1 ? "s" : ""}`)}</span>
            </div>
            <div class="facture-card-bottom">
              <span class="facture-card-meta">${escapeHtml(item.deviceHint || item.workshopCode || "Base client")}</span>
              <span class="facture-card-meta">${escapeHtml(shortDate(item.updatedAt))}</span>
            </div>
          </button>
        `;
      }).join("") : `
        <article class="facture-empty-card">
          <h3>Aucun client enregistre</h3>
          <p class="facture-empty-text">Importez les clients AtelierOS ou creez une fiche manuellement.</p>
          <div class="facture-inline-actions">
            <button type="button" class="facture-primary-button" data-import-atelier>Importer AtelierOS</button>
          </div>
        </article>
      `}
    `;
  }

  function complianceQuickItems() {
    const warnings = [];
    if (!state.company.address) warnings.push("Adresse ReFone a completer.");
    if (!state.company.siret) warnings.push("SIRET manquant.");
    if (!state.company.email) warnings.push("Email entreprise manquant.");
    if (!state.company.phone) warnings.push("Telephone entreprise manquant.");
    if (state.company.vatMode === "normal" && !state.company.vatNumber) {
      warnings.push("Numero de TVA a verifier si necessaire.");
    }
    return warnings;
  }

  function renderSettingsList() {
    const warnings = complianceQuickItems();
    if (elements.listCount) {
      elements.listCount.textContent = settingsCountLabel();
    }
    elements.list.innerHTML = `
      <article class="facture-summary-card">
        <h3>Base ReFone</h3>
        <div class="facture-summary-list">
          <div class="facture-summary-row"><span class="facture-summary-meta">Clients</span><strong>${escapeHtml(state.customers.length)}</strong></div>
          <div class="facture-summary-row"><span class="facture-summary-meta">Documents</span><strong>${escapeHtml(state.documents.length)}</strong></div>
          <div class="facture-summary-row"><span class="facture-summary-meta">Catalogue</span><strong>${escapeHtml(state.catalog.length)}</strong></div>
        </div>
      </article>
      <article class="facture-summary-card">
        <h3>Connexion AtelierOS</h3>
        <p class="facture-empty-text">Les fiches clients peuvent etre aspirees depuis la base atelier partagee avec le meme code prive.</p>
        <div class="facture-inline-actions">
          <button type="button" class="facture-primary-button" data-import-atelier>Importer AtelierOS</button>
          <button type="button" class="facture-secondary-button" data-open-sync>Code sync</button>
        </div>
      </article>
      <article class="facture-warning-card">
        <h3>Conformite facture</h3>
        ${warnings.length ? `
          <ul class="facture-warning-list">
            ${warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join("")}
          </ul>
        ` : `
          <p class="facture-empty-text">Les informations principales de base sont presentes.</p>
        `}
      </article>
      <article class="facture-summary-card">
        <h3>Reforme e-facturation</h3>
        <p class="facture-empty-text">${escapeHtml(state.company.eInvoiceStatus)}</p>
      </article>
    `;
  }

  function renderList() {
    if (ui.view === "customers") {
      renderCustomerList();
      return;
    }
    if (ui.view === "settings") {
      renderSettingsList();
      return;
    }
    renderDocumentList();
  }

  function customerOptionsHtml(selectedId = "") {
    const options = [`<option value="">Aucun client rattache</option>`];
    clone(state.customers)
      .sort((left, right) => left.name.localeCompare(right.name, "fr"))
      .forEach((item) => {
        options.push(`<option value="${escapeHtml(item.id)}" ${item.id === selectedId ? "selected" : ""}>${escapeHtml(item.name)}</option>`);
      });
    return options.join("");
  }

  function documentStatusOptions(selectedStatus) {
    return Object.entries(DOCUMENT_STATUS_LABELS)
      .map(([key, label]) => `<option value="${escapeHtml(key)}" ${selectedStatus === key ? "selected" : ""}>${escapeHtml(label)}</option>`)
      .join("");
  }

  function documentTypeOptions(selectedType) {
    return DOCUMENT_TYPES
      .map((item) => `<option value="${escapeHtml(item.id)}" ${selectedType === item.id ? "selected" : ""}>${escapeHtml(item.label)}</option>`)
      .join("");
  }

  function customerTypeOptions(selectedType) {
    const normalized = normalizeCustomerType(selectedType);
    return Object.entries(CUSTOMER_TYPE_LABELS)
      .map(([key, label]) => `<option value="${escapeHtml(key)}" ${normalized === key ? "selected" : ""}>${escapeHtml(label)}</option>`)
      .join("");
  }

  function paymentMethodOptions(selectedMethod) {
    const normalized = normalizePaymentMethod(selectedMethod || state.company.paymentMethod);
    return PAYMENT_METHODS
      .map((method) => `<option value="${escapeHtml(method)}" ${normalized === method ? "selected" : ""}>${escapeHtml(method)}</option>`)
      .join("");
  }

  function catalogReferenceDatalist() {
    const references = clone(state.catalog)
      .filter((item) => String(item.reference || "").trim())
      .sort((a, b) => String(a.reference || "").localeCompare(String(b.reference || ""), "fr"))
      .map((item) => {
        const label = [item.label, item.unitHt ? `${currency(item.unitHt)} HT` : ""]
          .filter(Boolean)
          .join(" - ");
        return `<option value="${escapeHtml(item.reference)}" label="${escapeHtml(label)}"></option>`;
      })
      .join("");
    return `<datalist id="facture-catalog-references">${references}</datalist>`;
  }

  function renderDocumentLines(documentItem) {
    return `${catalogReferenceDatalist()}${documentItem.lines.map((line, index) => {
      const qty = normalizeQty(line.qty);
      const { purchaseHt, purchaseTtc } = resolvePurchaseValues(line);
      const marginRate = normalizeMarginRate(line.marginRate);
      const lineHt = normalizeMoney(line.unitHt * qty);
      const linePurchaseTtc = normalizeMoney(purchaseTtc * qty);
      const linePurchaseHt = normalizeMoney(purchaseHt * qty);
      const lineVat = state.company.vatMode === "franchise" ? 0 : normalizeMoney(lineHt * (normalizeVatRate(line.vatRate) / 100));
      const lineTtc = normalizeMoney(lineHt + lineVat);
      const lineMargin = normalizeMoney(lineHt - linePurchaseTtc);

      return `
        <article class="facture-line-card">
          <div class="facture-line-top">
            <div class="facture-card-main">
              <span class="facture-small-code">LIGNE ${index + 1}</span>
              <strong>${escapeHtml(line.description || "Prestation a renseigner")}</strong>
            </div>
            <button type="button" class="facture-secondary-button" data-remove-line="${escapeHtml(line.id)}">Supprimer</button>
          </div>
          <div class="facture-line-body">
            <label class="facture-field">
              <span>Description</span>
              <input type="text" data-line-field="${escapeHtml(line.id)}" name="description" value="${escapeHtml(line.description)}">
            </label>
            <label class="facture-field">
              <span>Reference</span>
              <input type="text" data-line-field="${escapeHtml(line.id)}" name="reference" list="facture-catalog-references" autocomplete="off" value="${escapeHtml(line.reference)}">
            </label>
            <label class="facture-field">
              <span>Qté</span>
              <input type="number" data-line-field="${escapeHtml(line.id)}" name="qty" min="0" step="0.01" value="${escapeHtml(qty)}">
            </label>
            <label class="facture-field">
              <span>Achat HT</span>
              <input type="number" data-line-field="${escapeHtml(line.id)}" name="purchaseHt" min="0" step="0.01" value="${escapeHtml(purchaseHt)}">
            </label>
            <label class="facture-field">
              <span>Marge %</span>
              <input type="number" data-line-field="${escapeHtml(line.id)}" name="marginRate" min="-100" max="1000" step="0.01" value="${escapeHtml(marginRate)}">
            </label>
            <label class="facture-field">
              <span>Vente HT</span>
              <input type="number" data-line-field="${escapeHtml(line.id)}" name="unitHt" min="0" step="0.01" value="${escapeHtml(line.unitHt)}">
            </label>
            <label class="facture-field">
              <span>TVA %</span>
              <input type="number" data-line-field="${escapeHtml(line.id)}" name="vatRate" min="0" step="0.01" value="${escapeHtml(line.vatRate)}" ${state.company.vatMode === "franchise" ? "disabled" : ""}>
            </label>
          </div>
          <div class="facture-line-totals">
            <article class="facture-line-total-box">
              <span class="facture-meta-label">Achat HT</span>
              <strong>${escapeHtml(currency(linePurchaseHt))}</strong>
              <span class="facture-line-note">Prix fournisseur saisi.</span>
            </article>
            <article class="facture-line-total-box">
              <span class="facture-meta-label">Achat TTC auto</span>
              <strong>${escapeHtml(currency(linePurchaseTtc))}</strong>
              <span class="facture-line-note">HT + TVA achat ${escapeHtml(PURCHASE_VAT_RATE)}%.</span>
            </article>
            <article class="facture-line-total-box">
              <span class="facture-meta-label">Total client</span>
              <strong>${escapeHtml(currency(lineTtc))}</strong>
              <span class="facture-line-note">HT: ${escapeHtml(currency(lineHt))} · TVA: ${escapeHtml(currency(lineVat))}</span>
            </article>
            <article class="facture-line-total-box">
              <span class="facture-meta-label">Marge interne</span>
              <strong>${escapeHtml(currency(lineMargin))}</strong>
              <span class="facture-line-note">${escapeHtml(marginRate)}% sur achat TTC.</span>
            </article>
          </div>
        </article>
      `;
    }).join("")}`;
  }

  function renderCatalogList() {
    if (!state.catalog.length) {
      return `
        <article class="facture-empty-card">
          <p class="facture-empty-text">Aucun produit pour le moment. Ajoutez vos reparations, pieces ou services ici pour les recharger en un clic.</p>
        </article>
      `;
    }

    return clone(state.catalog)
      .sort((left, right) => left.label.localeCompare(right.label, "fr"))
      .map((item) => {
        const { purchaseHt, purchaseTtc } = resolvePurchaseValues(item);
        const marginRate = normalizeMarginRate(item.marginRate);
        return `
          <article class="facture-catalog-card">
            <div class="facture-catalog-top">
              <div class="facture-card-main">
                <span class="facture-small-code">${escapeHtml(item.reference || "CATALOGUE")}</span>
                <strong>${escapeHtml(item.label)}</strong>
                <span class="facture-card-meta">Vente ${escapeHtml(currency(item.unitHt))} HT · Achat ${escapeHtml(currency(purchaseHt))} HT · TTC auto ${escapeHtml(currency(purchaseTtc))} · Marge ${escapeHtml(marginRate)}%</span>
              </div>
              <div class="facture-catalog-actions">
                <button type="button" class="facture-link-button" data-use-catalog="${escapeHtml(item.id)}">Ajouter au document</button>
                <button type="button" class="facture-secondary-button" data-delete-catalog="${escapeHtml(item.id)}">Supprimer</button>
              </div>
            </div>
          </article>
        `;
      })
      .join("");
  }

  function renderDocumentDetail() {
    const documentItem = selectedDocument();
    if (!documentItem) {
      elements.detail.innerHTML = `
        <article class="facture-empty-card">
          <h3>Base prete a facturer</h3>
          <p class="facture-empty-text">Commencez par un nouveau devis, puis transformez-le si besoin en bon de commande ou en facture.</p>
          <div class="facture-inline-actions">
            <button type="button" class="facture-primary-button" data-open-document="quote">Nouveau devis</button>
            <button type="button" class="facture-secondary-button" data-open-customer-modal>Nouveau client</button>
          </div>
        </article>
      `;
      return;
    }

    const totals = documentTotals(documentItem);
    const warnings = documentWarnings(documentItem);
    const linkedCustomer = state.customers.find((item) => item.id === documentItem.customerId) || null;
    const displayNumber = documentDisplayNumber(documentItem) || documentItem.number || documentTypeMeta(documentItem.type).prefix;
    const displayTitle = documentDisplayTitle(documentItem) || DOCUMENT_TYPE_LABELS[documentItem.type];
    const displayNote = documentDisplayNote(documentItem);

    elements.detail.innerHTML = `
      <div class="facture-detail-stack">
        <header class="facture-detail-head">
          <div class="facture-card-main">
            <span class="facture-card-code">${escapeHtml(displayNumber)}</span>
            <h2 class="facture-detail-title">${escapeHtml(displayTitle)}</h2>
            <p class="facture-detail-subtitle">${escapeHtml(customerNameForDocument(documentItem))} · ${escapeHtml(DOCUMENT_TYPE_LABELS[documentItem.type])}</p>
          </div>
          <div class="facture-detail-stack">
            <span class="facture-chip" data-type="${escapeHtml(documentItem.type)}">${escapeHtml(DOCUMENT_TYPE_LABELS[documentItem.type])}</span>
            <span class="facture-status-chip" data-status="${escapeHtml(documentItem.status)}">${escapeHtml(DOCUMENT_STATUS_LABELS[documentItem.status])}</span>
            <span class="facture-alert-chip">${escapeHtml(currency(totals.totalTtc))}</span>
          </div>
        </header>

        <div class="facture-details-grid">
          <article class="facture-detail-box">
            <span class="facture-meta-label">Date emission</span>
            <strong>${escapeHtml(shortDate(documentItem.issueDate))}</strong>
          </article>
          <article class="facture-detail-box">
            <span class="facture-meta-label">Echeance</span>
            <strong>${escapeHtml(shortDate(documentItem.dueDate))}</strong>
          </article>
          <article class="facture-detail-box">
            <span class="facture-meta-label">Client</span>
            <strong>${escapeHtml(customerNameForDocument(documentItem))}</strong>
          </article>
        </div>

        <section class="facture-subpanel">
          <div class="facture-company-head">
            <div>
              <span class="facture-panel-kicker">DOCUMENT</span>
              <h3 class="facture-section-title">Informations generales</h3>
            </div>
            ${linkedCustomer ? `<span class="facture-atelier-badge">${escapeHtml(linkedCustomer.source === "atelier-os" ? "Liaison AtelierOS" : "Fiche client liee")}</span>` : ""}
          </div>
          <form class="facture-detail-form" data-document-form>
            <div class="facture-form-grid">
              <label class="facture-field">
                <span>Type</span>
                <select name="type">${documentTypeOptions(documentItem.type)}</select>
              </label>
              <label class="facture-field">
                <span>Statut</span>
                <select name="status">${documentStatusOptions(documentItem.status)}</select>
              </label>
              <label class="facture-field facture-field-wide">
                <span>Objet</span>
                <input type="text" name="title" value="${escapeHtml(displayTitle)}" placeholder="Exemple: Remplacement ecran iPhone 11">
              </label>
              <label class="facture-field">
                <span>Client rattache</span>
                <select name="customerId">${customerOptionsHtml(documentItem.customerId)}</select>
              </label>
              <label class="facture-field">
                <span>Date emission</span>
                <input type="date" name="issueDate" value="${escapeHtml(formatDateInput(documentItem.issueDate))}">
              </label>
              <label class="facture-field">
                <span>Echeance</span>
                <input type="date" name="dueDate" value="${escapeHtml(formatDateInput(documentItem.dueDate))}">
              </label>
              <label class="facture-field">
                <span>Mode de paiement</span>
                <select name="paymentMethod">${paymentMethodOptions(documentItem.paymentMethod || state.company.paymentMethod)}</select>
              </label>
              <label class="facture-field facture-field-wide">
                <span>Conditions de paiement</span>
                <input type="text" name="paymentTerms" value="${escapeHtml(documentItem.paymentTerms || state.company.paymentTerms)}" placeholder="Paiement a reception...">
              </label>
              <label class="facture-field">
                <span>Type client</span>
                <select name="customerType">${customerTypeOptions(documentItem.customerType)}</select>
              </label>
              <label class="facture-field">
                <span>Nom client</span>
                <input type="text" name="customerName" value="${escapeHtml(documentItem.customerName)}" placeholder="Nom ou societe">
              </label>
              <label class="facture-field">
                <span>Email client</span>
                <input type="email" name="customerEmail" value="${escapeHtml(documentItem.customerEmail)}" placeholder="client@email.com">
              </label>
              <label class="facture-field">
                <span>Telephone client</span>
                <input type="text" name="customerPhone" value="${escapeHtml(documentItem.customerPhone)}" placeholder="06 00 00 00 00">
              </label>
              <label class="facture-field facture-field-wide">
                <span>Adresse client</span>
                <textarea name="customerAddress" rows="3" autocomplete="street-address" placeholder="Adresse de facturation">${escapeHtml(documentItem.customerAddress)}</textarea>
              </label>
              <label class="facture-field facture-field-wide">
                <span>Adresse de livraison</span>
                <textarea name="deliveryAddress" rows="3" autocomplete="street-address" placeholder="Si differente de l'adresse de facturation">${escapeHtml(documentItem.deliveryAddress)}</textarea>
              </label>
              <label class="facture-field facture-field-wide">
                <span>Message visible par le client</span>
                <textarea name="noteToCustomer" rows="4" placeholder="Informations utiles sur la reparation ou la commande">${escapeHtml(displayNote)}</textarea>
              </label>
              <label class="facture-field facture-field-wide">
                <span>Note interne</span>
                <textarea name="internalNote" rows="3" placeholder="Visible uniquement dans FactureOS">${escapeHtml(documentItem.internalNote)}</textarea>
              </label>
            </div>

            <div class="facture-form-actions">
              <button type="submit" class="facture-primary-button">Enregistrer le document</button>
              <button type="button" class="facture-secondary-button" data-create-customer-from-document>Creer / mettre a jour la fiche client</button>
              <button type="button" class="facture-secondary-button" data-refresh-customer>Recharger depuis la fiche client</button>
              <button type="button" class="facture-secondary-button" data-duplicate-document>Dupliquer</button>
              ${documentItem.type !== "invoice" ? `<button type="button" class="facture-secondary-button" data-convert-document="invoice">Transformer en facture</button>` : ""}
              <button type="button" class="facture-danger-button" data-delete-document>Supprimer</button>
            </div>
          </form>
        </section>

        <section class="facture-subpanel">
          <div class="facture-company-head">
            <div>
              <span class="facture-panel-kicker">LIGNES</span>
              <h3 class="facture-section-title">Prestations et pieces</h3>
            </div>
            <div class="facture-inline-actions">
              <button type="button" class="facture-primary-button" data-add-line>Ajouter une ligne</button>
            </div>
          </div>
          <div class="facture-detail-stack">
            ${renderDocumentLines(documentItem)}
          </div>
        </section>

        <section class="facture-subpanel">
          <div class="facture-company-head">
            <div>
              <span class="facture-panel-kicker">TOTALS</span>
              <h3 class="facture-section-title">Calcul type SumUp</h3>
            </div>
          </div>
          <div class="facture-summary-grid">
            <article class="facture-summary-box">
              <span class="facture-meta-label">Total HT</span>
              <strong>${escapeHtml(currency(totals.totalHt))}</strong>
            </article>
            <article class="facture-summary-box">
              <span class="facture-meta-label">TVA</span>
              <strong>${escapeHtml(currency(totals.totalVat))}</strong>
            </article>
            <article class="facture-summary-box">
              <span class="facture-meta-label">Total TTC</span>
              <strong>${escapeHtml(currency(totals.totalTtc))}</strong>
            </article>
            <article class="facture-summary-box">
              <span class="facture-meta-label">Achat TTC interne</span>
              <strong>${escapeHtml(currency(totals.totalPurchase))}</strong>
            </article>
            <article class="facture-summary-box">
              <span class="facture-meta-label">Marge interne</span>
              <strong>${escapeHtml(currency(totals.marginHt))}</strong>
            </article>
            <article class="facture-summary-box">
              <span class="facture-meta-label">Mise a jour</span>
              <strong>${escapeHtml(longDate(documentItem.updatedAt))}</strong>
            </article>
          </div>
        </section>

        <section class="facture-pdf-grid">
          <article class="facture-summary-card">
            <h3>Export PDF</h3>
            <p class="facture-empty-text">Genere un PDF propre aux couleurs ReFone avec logo, mentions entreprise, lignes detaillees et totaux.</p>
            <div class="facture-pdf-actions">
              <button type="button" class="facture-primary-button" data-export-pdf>Telecharger le PDF</button>
              <button type="button" class="facture-secondary-button" data-open-pdf>Ouvrir le PDF</button>
              <button type="button" class="facture-secondary-button" data-open-mail-pro>Ouvrir dans MailPro</button>
            </div>
          </article>
          <article class="facture-warning-card">
            <h3>Points a verifier</h3>
            ${warnings.length ? `
              <ul class="facture-warning-list">
                ${warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join("")}
              </ul>
            ` : `
              <p class="facture-empty-text">Les champs principaux sont remplis pour sortir un document propre.</p>
            `}
          </article>
        </section>
      </div>
    `;
  }

  function renderLinkedCustomerDocuments(customerId) {
    const documents = state.documents
      .filter((item) => item.customerId === customerId)
      .sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime());

    if (!documents.length) {
      return `
        <article class="facture-empty-card">
          <p class="facture-empty-text">Aucun document lie a ce client pour le moment.</p>
        </article>
      `;
    }

    return documents.map((item) => {
      const totals = documentTotals(item);
      const displayNumber = documentDisplayNumber(item) || item.number || documentTypeMeta(item.type).prefix;
      const displayTitle = documentDisplayTitle(item) || DOCUMENT_TYPE_LABELS[item.type];
      return `
        <button type="button" class="facture-card" data-open-document-from-customer="${escapeHtml(item.id)}">
          <div class="facture-card-top">
            <div class="facture-card-main">
              <span class="facture-card-code">${escapeHtml(displayNumber)}</span>
              <strong class="facture-card-title">${escapeHtml(displayTitle)}</strong>
              <span class="facture-card-meta">${escapeHtml(shortDate(item.issueDate))}</span>
            </div>
            <div class="facture-detail-stack">
              <span class="facture-chip" data-type="${escapeHtml(item.type)}">${escapeHtml(DOCUMENT_TYPE_LABELS[item.type])}</span>
              <span class="facture-status-chip" data-status="${escapeHtml(item.status)}">${escapeHtml(DOCUMENT_STATUS_LABELS[item.status])}</span>
            </div>
          </div>
          <div class="facture-card-bottom">
            <span class="facture-card-meta">${escapeHtml(currency(totals.totalTtc))}</span>
            <span class="facture-card-meta">Ouvrir</span>
          </div>
        </button>
      `;
    }).join("");
  }

  function renderCustomerDetail() {
    const customer = selectedCustomer();
    if (!customer) {
      elements.detail.innerHTML = `
        <article class="facture-empty-card">
          <h3>Base clients vide</h3>
          <p class="facture-empty-text">Crez une fiche manuellement ou importez vos clients existants depuis AtelierOS.</p>
          <div class="facture-inline-actions">
            <button type="button" class="facture-primary-button" data-open-customer-modal>Nouveau client</button>
            <button type="button" class="facture-secondary-button" data-import-atelier>Importer AtelierOS</button>
          </div>
        </article>
      `;
      return;
    }

    const linkedDocsCount = state.documents.filter((item) => item.customerId === customer.id).length;

    elements.detail.innerHTML = `
      <div class="facture-detail-stack">
        <header class="facture-detail-head">
          <div class="facture-card-main">
            <span class="facture-card-code">${escapeHtml(customer.source === "atelier-os" ? "IMPORTE ATELIEROS" : "CLIENT REFONE")}</span>
            <h2 class="facture-detail-title">${escapeHtml(customer.name)}</h2>
            <p class="facture-detail-subtitle">${escapeHtml(customer.email || customer.phone || "Coordonnees a completer")}</p>
          </div>
          <div class="facture-detail-stack">
            <span class="facture-alert-chip">${escapeHtml(`${linkedDocsCount} document${linkedDocsCount > 1 ? "s" : ""}`)}</span>
            ${customer.deviceHint ? `<span class="facture-chip" data-type="quote">${escapeHtml(customer.deviceHint)}</span>` : ""}
          </div>
        </header>

        <div class="facture-details-grid">
          <article class="facture-detail-box">
            <span class="facture-meta-label">Telephone</span>
            <strong>${escapeHtml(customer.phone || "Non renseigne")}</strong>
          </article>
          <article class="facture-detail-box">
            <span class="facture-meta-label">Email</span>
            <strong>${escapeHtml(customer.email || "Non renseigne")}</strong>
          </article>
          <article class="facture-detail-box">
            <span class="facture-meta-label">Type client</span>
            <strong>${escapeHtml(CUSTOMER_TYPE_LABELS[customer.customerType] || CUSTOMER_TYPE_LABELS.individual)}</strong>
          </article>
          <article class="facture-detail-box">
            <span class="facture-meta-label">Dernier lien atelier</span>
            <strong>${escapeHtml(customer.workshopCode || "Aucun")}</strong>
          </article>
        </div>

        <section class="facture-subpanel">
          <div class="facture-company-head">
            <div>
              <span class="facture-panel-kicker">FICHE CLIENT</span>
              <h3 class="facture-section-title">Coordonnees</h3>
            </div>
          </div>
          <form class="facture-detail-form" data-customer-form>
            <div class="facture-form-grid">
              <label class="facture-field">
                <span>Nom / societe</span>
                <input type="text" name="name" value="${escapeHtml(customer.name)}">
              </label>
              <label class="facture-field">
                <span>Email</span>
                <input type="email" name="email" value="${escapeHtml(customer.email)}">
              </label>
              <label class="facture-field">
                <span>Telephone</span>
                <input type="text" name="phone" value="${escapeHtml(customer.phone)}">
              </label>
              <label class="facture-field">
                <span>Type client</span>
                <select name="customerType">${customerTypeOptions(customer.customerType)}</select>
              </label>
              <label class="facture-field">
                <span>Appareil / repere</span>
                <input type="text" name="deviceHint" value="${escapeHtml(customer.deviceHint)}" placeholder="Exemple: iPhone 11">
              </label>
              <label class="facture-field facture-field-wide">
                <span>Adresse</span>
                <textarea name="address" rows="3" autocomplete="street-address">${escapeHtml(customer.address)}</textarea>
              </label>
              <label class="facture-field facture-field-wide">
                <span>Note</span>
                <textarea name="note" rows="4">${escapeHtml(customer.note)}</textarea>
              </label>
            </div>
            <div class="facture-form-actions">
              <button type="submit" class="facture-primary-button">Enregistrer la fiche</button>
              <button type="button" class="facture-secondary-button" data-create-document-from-customer="quote">Nouveau devis</button>
              <button type="button" class="facture-danger-button" data-delete-customer>Supprimer le client</button>
            </div>
          </form>
        </section>

        <section class="facture-subpanel">
          <div class="facture-company-head">
            <div>
              <span class="facture-panel-kicker">HISTORIQUE</span>
              <h3 class="facture-section-title">Documents lies</h3>
            </div>
          </div>
          <div class="facture-detail-stack">
            ${renderLinkedCustomerDocuments(customer.id)}
          </div>
        </section>
      </div>
    `;
  }

  function renderSettingsDetail() {
    const company = state.company;
    elements.detail.innerHTML = `
      <div class="facture-detail-stack">
        <section class="facture-subpanel">
          <div class="facture-company-head">
            <div class="facture-brand">
              <img class="facture-company-logo" src="assets/media/refone-logo.png" alt="ReFone">
              <div class="facture-card-main">
                <span class="facture-panel-kicker">IDENTITE REFONE</span>
                <h3 class="facture-section-title">Mentions entreprise</h3>
                <p class="facture-company-note">Ces champs alimentent les PDF de devis, bon de commande et facture.</p>
              </div>
            </div>
          </div>
          <form class="facture-settings-form" data-settings-form>
            <div class="facture-company-grid">
              <label class="facture-field">
                <span>Nom commercial</span>
                <input type="text" name="legalName" value="${escapeHtml(company.legalName)}">
              </label>
              <label class="facture-field">
                <span>Forme</span>
                <input type="text" name="businessType" value="${escapeHtml(company.businessType)}">
              </label>
              <label class="facture-field facture-field-wide">
                <span>Adresse</span>
                <textarea name="address" rows="3" autocomplete="street-address">${escapeHtml(company.address)}</textarea>
              </label>
              <label class="facture-field">
                <span>Commune / ville</span>
                <input type="text" name="locality" value="${escapeHtml(company.locality)}">
              </label>
              <label class="facture-field">
                <span>Email</span>
                <input type="email" name="email" value="${escapeHtml(company.email)}">
              </label>
              <label class="facture-field">
                <span>Telephone</span>
                <input type="text" name="phone" value="${escapeHtml(company.phone)}">
              </label>
              <label class="facture-field">
                <span>SIRET</span>
                <input type="text" name="siret" value="${escapeHtml(company.siret)}">
              </label>
              <label class="facture-field">
                <span>SIREN</span>
                <input type="text" name="siren" value="${escapeHtml(company.siren)}">
              </label>
              <label class="facture-field facture-field-wide">
                <span>RIB</span>
                <input type="text" name="rib" value="${escapeHtml(company.rib)}" placeholder="FR76...">
              </label>
              <label class="facture-field">
                <span>Mode TVA</span>
                <select name="vatMode">
                  <option value="franchise" selected>Franchise en base - art. 293 B du CGI</option>
                </select>
                <small class="facture-help-text">Facturation sans TVA pour micro-entreprise.</small>
              </label>
              <label class="facture-field">
                <span>Numero TVA</span>
                <input type="text" name="vatNumber" value="" placeholder="Non applicable" disabled>
              </label>
              <label class="facture-field facture-field-wide">
                <span>Mention TVA</span>
                <input type="text" name="vatNote" value="${escapeHtml(company.vatNote)}" placeholder="Exemple: TVA non applicable, art. 293 B du CGI">
              </label>
              <label class="facture-field facture-field-wide">
                <span>Conditions de paiement</span>
                <input type="text" name="paymentTerms" value="${escapeHtml(company.paymentTerms)}">
              </label>
              <label class="facture-field facture-field-wide">
                <span>Mode de paiement par defaut</span>
                <select name="paymentMethod">${paymentMethodOptions(company.paymentMethod)}</select>
              </label>
              <label class="facture-field">
                <span>Marge par defaut %</span>
                <input type="number" name="defaultMarginRate" min="-100" max="1000" step="0.01" value="${escapeHtml(company.defaultMarginRate)}">
                <small class="facture-help-text">Appliquee aux nouvelles lignes et aux nouveaux produits du catalogue.</small>
              </label>
              <label class="facture-field facture-field-wide">
                <span>Penalites / retard</span>
                <input type="text" name="latePenalty" value="${escapeHtml(company.latePenalty)}">
              </label>
              <label class="facture-field facture-field-wide">
                <span>Pied de page</span>
                <input type="text" name="footerNote" value="${escapeHtml(company.footerNote)}">
              </label>
              <label class="facture-field facture-field-wide">
                <span>Statut e-facturation</span>
                <textarea name="eInvoiceStatus" rows="3">${escapeHtml(company.eInvoiceStatus)}</textarea>
              </label>
              <label class="facture-field facture-field-wide">
                <span>CGV / derniere page PDF</span>
                <textarea class="facture-terms-editor" name="termsText" rows="18" spellcheck="true">${escapeHtml(company.termsText || defaultTermsText(company))}</textarea>
                <small class="facture-help-text">Ce texte est ajoute en derniere page de chaque devis, facture et bon de commande. Il se synchronise avec la base RF-ERWANN entre web, Mac et Windows.</small>
              </label>
            </div>
            <div class="facture-form-actions">
              <button type="submit" class="facture-primary-button">Enregistrer les reglages</button>
            </div>
          </form>
        </section>

        <section class="facture-subpanel">
          <div class="facture-company-head">
            <div>
              <span class="facture-panel-kicker">CATALOGUE</span>
              <h3 class="facture-section-title">Produits et prestations predefinis</h3>
              <p class="facture-company-note">Ajoutez vos pieces, forfaits et services avec reference, achat HT, TTC calcule, marge et prix de vente.</p>
            </div>
          </div>
          <div class="facture-catalog-list">
            ${renderCatalogList()}
          </div>
          <div class="facture-divider"></div>
          <form class="facture-settings-form" data-catalog-form>
            <div class="facture-company-grid">
              <label class="facture-field">
                <span>Libelle</span>
                <input type="text" name="label" placeholder="Remplacement ecran iPhone 11">
              </label>
              <label class="facture-field">
                <span>Reference</span>
                <input type="text" name="reference" placeholder="IP11-SCREEN">
              </label>
              <label class="facture-field">
                <span>Achat HT</span>
                <input type="number" name="purchaseHt" min="0" step="0.01" placeholder="40" data-catalog-calc>
              </label>
              <label class="facture-field">
                <span>Achat TTC auto</span>
                <input type="number" name="purchaseTtcPreview" min="0" step="0.01" placeholder="48" readonly data-catalog-purchase-ttc>
              </label>
              <label class="facture-field">
                <span>Marge %</span>
                <input type="number" name="marginRate" min="-100" max="1000" step="0.01" value="${escapeHtml(company.defaultMarginRate)}" data-catalog-calc>
              </label>
              <label class="facture-field">
                <span>Vente HT</span>
                <input type="number" name="unitHt" min="0" step="0.01" placeholder="Auto" data-catalog-unit-ht>
              </label>
              <label class="facture-field">
                <span>TVA %</span>
                <input type="number" name="vatRate" min="0" step="0.01" value="${escapeHtml(company.vatMode === "franchise" ? 0 : 20)}" ${company.vatMode === "franchise" ? "disabled" : ""}>
              </label>
            </div>
            <div class="facture-form-actions">
              <button type="submit" class="facture-primary-button">Ajouter au catalogue</button>
            </div>
          </form>
        </section>
      </div>
    `;
  }

  function renderDetail() {
    if (ui.view === "customers") {
      renderCustomerDetail();
      return;
    }
    if (ui.view === "settings") {
      renderSettingsDetail();
      return;
    }
    renderDocumentDetail();
  }

  function renderDocumentCreateCustomerOptions() {
    if (!elements.documentCreateCustomer) return;
    elements.documentCreateCustomer.innerHTML = customerOptionsHtml(state.selectedCustomerId);
  }

  function render() {
    normalizeSelections();
    updateViewMeta();
    renderStats();
    renderList();
    renderDetail();
    renderDocumentCreateCustomerOptions();
    saveState();
    updateSyncIndicator();
  }

  function setView(nextView) {
    ui.view = normalizeView(nextView);
    render();
  }

  function setSelectedDocument(id) {
    state.selectedDocumentId = String(id || "");
    ui.view = "documents";
    render();
  }

  function setSelectedCustomer(id) {
    state.selectedCustomerId = String(id || "");
    ui.view = "customers";
    render();
  }

  function nextDocumentNumber(type, dateValue) {
    const normalizedType = normalizeDocumentType(type);
    const year = String(new Date(dateValue || currentIso()).getFullYear());
    if (!state.counters[normalizedType]) {
      state.counters[normalizedType] = {};
    }
    const current = Math.max(0, Number(state.counters[normalizedType]?.[year] || 0)) + 1;
    state.counters[normalizedType][year] = current;
    return `${documentTypeMeta(normalizedType).prefix}-${year}-${String(current).padStart(4, "0")}`;
  }

  function defaultDueDateForType(type, issueDate) {
    const normalizedType = normalizeDocumentType(type);
    if (normalizedType === "quote" || normalizedType === "invoice") {
      return addDaysIsoDate(issueDate || isoDateOnly(), 30);
    }
    return formatDateInput(issueDate) || isoDateOnly();
  }

  function customerSnapshotFromId(customerId) {
    const customer = state.customers.find((item) => item.id === customerId);
    if (!customer) {
      return {
        customerId: "",
        customerName: "",
        customerEmail: "",
        customerPhone: "",
        customerAddress: "",
        customerType: "individual",
        deliveryAddress: ""
      };
    }
    return {
      customerId: customer.id,
      customerName: customer.name,
      customerEmail: customer.email,
      customerPhone: customer.phone,
      customerAddress: customer.address,
      customerType: customer.customerType,
      deliveryAddress: customer.address
    };
  }

  function applyCustomerSnapshotToDocument(documentItem, customerId) {
    Object.assign(documentItem, customerSnapshotFromId(customerId));
    documentItem.updatedAt = currentIso();
  }

  function documentDefaultTitle(type, snapshot = {}) {
    const device = state.customers.find((item) => item.id === snapshot.customerId)?.deviceHint || "";
    if (device) {
      return `${DOCUMENT_TYPE_LABELS[type]} - ${device}`;
    }
    if (type === "invoice") return "Facture reparation appareil";
    if (type === "order") return "Bon de commande reparation appareil";
    return "Devis reparation appareil";
  }

  function documentSnapshot(documentItem = {}) {
    return {
      customerId: documentItem.customerId || "",
      customerName: documentItem.customerName || "",
      customerEmail: documentItem.customerEmail || "",
      customerPhone: documentItem.customerPhone || "",
      customerAddress: documentItem.customerAddress || "",
      customerType: documentItem.customerType || "individual",
      deliveryAddress: documentItem.deliveryAddress || ""
    };
  }

  function customerMatchKey(values = {}) {
    const email = String(values.email || values.customerEmail || "").trim().toLowerCase();
    const phone = String(values.phone || values.customerPhone || "").replace(/\s+/g, "");
    const name = String(values.name || values.customerName || "").trim().toLowerCase();
    return email || phone || name;
  }

  function findMatchingCustomer(values = {}) {
    const key = customerMatchKey(values);
    if (!key) return null;
    return state.customers.find((customer) => customerMatchKey(customer) === key) || null;
  }

  function upsertCustomerFromData(values = {}, options = {}) {
    const name = String(values.name || values.customerName || "").trim();
    if (!name) return null;

    const patch = {
      name,
      email: String(values.email || values.customerEmail || "").trim(),
      phone: String(values.phone || values.customerPhone || "").trim(),
      address: String(values.address || values.customerAddress || "").trim(),
      customerType: normalizeCustomerType(values.customerType),
      note: String(values.note || "").trim(),
      deviceHint: String(values.deviceHint || "").trim(),
      source: options.source || "facture-os",
      updatedAt: currentIso()
    };

    let customer = null;
    if (options.customerId) {
      customer = state.customers.find((item) => item.id === options.customerId) || null;
    }
    if (!customer) {
      customer = findMatchingCustomer(patch);
    }

    if (customer) {
      customer.name = patch.name || customer.name;
      customer.email = patch.email || customer.email;
      customer.phone = patch.phone || customer.phone;
      customer.address = patch.address || customer.address;
      customer.customerType = patch.customerType;
      if (patch.note && !String(customer.note || "").includes(patch.note)) {
        customer.note = [customer.note, patch.note].filter(Boolean).join("\n").slice(0, 1200);
      }
      customer.deviceHint = patch.deviceHint || customer.deviceHint;
      customer.source = customer.source || patch.source;
      customer.updatedAt = patch.updatedAt;
      return customer;
    }

    customer = normalizeCustomer({
      id: createId("customer"),
      ...patch,
      createdAt: currentIso()
    });
    state.customers.unshift(customer);
    return customer;
  }

  function upsertCustomerFromDocument(documentItem, options = {}) {
    if (!documentItem) return null;
    const customer = upsertCustomerFromData({
      customerName: documentItem.customerName,
      customerEmail: documentItem.customerEmail,
      customerPhone: documentItem.customerPhone,
      customerAddress: documentItem.customerAddress,
      customerType: documentItem.customerType,
      deviceHint: documentItem.title || documentItem.lines?.[0]?.description || "",
      note: documentItem.internalNote || ""
    }, {
      customerId: documentItem.customerId,
      source: "facture-os"
    });

    if (customer && options.link !== false) {
      documentItem.customerId = customer.id;
      state.selectedCustomerId = customer.id;
    }
    return customer;
  }

  function rewriteDocumentTitleForType(title, nextType, snapshot = {}) {
    const raw = String(title || "").trim();
    const normalizedType = normalizeDocumentType(nextType);
    if (!raw) return documentDefaultTitle(normalizedType, snapshot);

    for (const item of DOCUMENT_TYPES) {
      const label = item.label;
      const rawLower = raw.toLowerCase();
      const labelLower = label.toLowerCase();
      if (rawLower === labelLower) {
        return DOCUMENT_TYPE_LABELS[normalizedType];
      }
      if (rawLower.startsWith(`${labelLower} - `)) {
        return `${DOCUMENT_TYPE_LABELS[normalizedType]}${raw.slice(label.length)}`;
      }
      if (rawLower === documentDefaultTitle(item.id, snapshot).toLowerCase()) {
        return documentDefaultTitle(normalizedType, snapshot);
      }
    }

    return raw;
  }

  function documentDisplayTitle(documentItem) {
    return rewriteDocumentTitleForType(documentItem?.title || "", documentItem?.type || "quote", documentSnapshot(documentItem));
  }

  function documentDefaultNote(type) {
    if (type === "invoice") {
      return "Merci pour votre confiance. Le document recapitule les prestations realisees par ReFone.";
    }
    if (type === "order") {
      return "Bon pour accord avant intervention. Les pieces restent soumises a disponibilite atelier.";
    }
    return "Merci pour votre passage chez ReFone. Ce devis est valable 30 jours, sous reserve de disponibilite des pieces.";
  }

  function documentDisplayNote(documentItem) {
    const raw = String(documentItem?.noteToCustomer || "").trim();
    if (!raw) return documentDefaultNote(documentItem?.type || "quote");
    for (const item of DOCUMENT_TYPES) {
      if (raw === documentDefaultNote(item.id)) {
        return documentDefaultNote(documentItem?.type || "quote");
      }
    }
    return raw;
  }

  function documentMailPreset(type) {
    if (type === "invoice") return "devis";
    if (type === "order") return "rdv";
    return "devis";
  }

  function buildMailProDraftUrl(documentItem) {
    const customer = state.customers.find((item) => item.id === documentItem.customerId) || null;
    const totals = documentTotals(documentItem);
    const displayNumber = documentDisplayNumber(documentItem) || documentItem.number || documentTypeMeta(documentItem.type).prefix;
    const displayTitle = documentDisplayTitle(documentItem) || DOCUMENT_TYPE_LABELS[documentItem.type];
    const customerName = customerNameForDocument(documentItem);
    const customerEmail = String(documentItem.customerEmail || customer?.email || "").trim();
    const preset = documentMailPreset(documentItem?.type || "quote");
    const mailTitleByType = {
      quote: "Votre devis est pret",
      order: "Votre bon de commande est pret",
      invoice: "Votre facture est disponible"
    };
    const introByType = {
      quote: `Bonjour ${customerName},\n\nVotre devis ReFone est pret et le recapitulatif ci-dessous reprend les points utiles.`,
      order: `Bonjour ${customerName},\n\nVotre bon de commande ReFone est pret et le recapitulatif ci-dessous reprend les points utiles.`,
      invoice: `Bonjour ${customerName},\n\nVotre facture ReFone est disponible et le recapitulatif ci-dessous reprend les points utiles.`
    };
    const bodyByType = {
      quote: "Si vous souhaitez valider ou poser une question, vous pouvez repondre directement a ce message.",
      order: "Si vous souhaitez confirmer la commande ou poser une question, vous pouvez repondre directement a ce message.",
      invoice: "Si vous avez besoin d'un complement d'information, vous pouvez repondre directement a ce message."
    };

    const params = new URLSearchParams({
      source: "facture-os",
      activePreset: preset,
      recipientName: customerName,
      recipientEmail: customerEmail,
      subject: `${DOCUMENT_TYPE_LABELS[documentItem.type]} ${displayNumber} ReFone`,
      preheader: `${displayTitle} · ${currency(totals.totalTtc)} TTC`,
      mailTitle: mailTitleByType[documentItem.type] || mailTitleByType.quote,
      mailIntro: introByType[documentItem.type] || introByType.quote,
      mailBody: bodyByType[documentItem.type] || bodyByType.quote,
      highlight: `Document ${displayNumber} · ${currency(totals.totalTtc)} TTC`,
      detailLabel1: "Document",
      detailValue1: displayNumber,
      detailLabel2: "Client",
      detailValue2: customerName,
      detailLabel3: "Montant",
      detailValue3: currency(totals.totalTtc),
      detailLabel4: "Echeance",
      detailValue4: shortDate(documentItem.dueDate),
      ctaLabel: "Repondre a ce mail",
      ctaUrl: customerEmail ? `mailto:${customerEmail}` : "mailto:contact@refonetech.com",
      closingNote: `Vous pouvez aussi nous repondre directement si quelque chose doit etre ajuste sur ${displayTitle}.`,
      logoUrl: resolveSiteUrl("photos/relog-180.png?v=20260605-logo-clean"),
      accentPrimary: "#2f7cf6",
      accentSecondary: "#7a5cff",
      titleFont: '-apple-system, BlinkMacSystemFont, "SF Pro Display", "Helvetica Neue", Arial, sans-serif',
      bodyFont: '-apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Arial, sans-serif',
      businessName: state.company.legalName || "ReFone",
      businessTagline: "Reparation appareils · iPhone, iPad, Mac",
      senderName: state.company.legalName || "ReFone",
      senderRole: state.company.businessType || "Atelier ReFone",
      senderPhone: state.company.phone || "",
      senderEmail: state.company.email || "contact@refonetech.com",
      footerNote: state.company.footerNote || "Merci pour votre confiance. Ce message a ete prepare depuis FactureOS."
    });

    return resolveSiteUrl(`/mail-pro.html?${params.toString()}`);
  }

  function documentDefaultLine(type, snapshot = {}) {
    const device = state.customers.find((item) => item.id === snapshot.customerId)?.deviceHint || "appareil client";
    const label = type === "invoice" ? "Intervention atelier" : "Diagnostic et intervention atelier";
    const marginRate = normalizeMarginRate(state.company.defaultMarginRate);
    return {
      id: createId("line"),
      description: `${label} - ${device}`,
      reference: "REFONE-ATELIER",
      qty: 1,
      purchaseTtc: 0,
      purchaseHt: 0,
      marginRate,
      unitHt: 0,
      vatRate: state.company.vatMode === "franchise" ? 0 : 20
    };
  }

  function createDocument(payload) {
    const type = normalizeDocumentType(payload.type);
    const issueDate = payload.issueDate || isoDateOnly();
    const dueDate = payload.dueDate || defaultDueDateForType(type, issueDate);
    let customerId = String(payload.customerId || "").slice(0, 120);
    if (!customerId && String(payload.customerName || "").trim()) {
      const customer = upsertCustomerFromData(payload, { source: "facture-os" });
      customerId = customer?.id || "";
    }
    const snapshot = customerSnapshotFromId(customerId);
    const defaultLines = Array.isArray(payload.lines)
      ? payload.lines
      : [documentDefaultLine(type, snapshot)];
    const documentItem = normalizeDocument({
      id: createId("document"),
      type,
      number: nextDocumentNumber(type, issueDate),
      status: "draft",
      title: payload.title || documentDefaultTitle(type, snapshot),
      ...snapshot,
      customerName: payload.customerName || snapshot.customerName,
      customerEmail: payload.customerEmail || snapshot.customerEmail,
      customerPhone: payload.customerPhone || snapshot.customerPhone,
      customerAddress: payload.customerAddress || snapshot.customerAddress,
      customerType: payload.customerType || snapshot.customerType,
      issueDate,
      dueDate,
      paymentMethod: state.company.paymentMethod,
      paymentTerms: state.company.paymentTerms,
      noteToCustomer: payload.noteToCustomer || documentDefaultNote(type),
      internalNote: payload.internalNote || "",
      lines: defaultLines.length ? defaultLines : [documentDefaultLine(type, snapshot)],
      createdAt: currentIso(),
      updatedAt: currentIso()
    });

    state.documents.unshift(documentItem);
    state.selectedDocumentId = documentItem.id;
    const linkedCustomer = upsertCustomerFromDocument(documentItem, { link: true });
    if (linkedCustomer) {
      state.selectedCustomerId = linkedCustomer.id;
    }
    ui.view = "documents";
    render();
    queueAutoPush();
    showToast(`${DOCUMENT_TYPE_LABELS[type]} cree`);
    return documentItem;
  }

  function createCustomer(payload) {
    const customer = normalizeCustomer({
      id: createId("customer"),
      name: payload.name,
      email: payload.email,
      phone: payload.phone,
      address: payload.address,
      customerType: payload.customerType,
      note: payload.note,
      source: "manual",
      createdAt: currentIso(),
      updatedAt: currentIso()
    });
    state.customers.unshift(customer);
    state.selectedCustomerId = customer.id;
    ui.view = "customers";
    render();
    queueAutoPush();
    showToast("Client cree");
  }

  function updateSelectedDocument(values) {
    const documentItem = selectedDocument();
    if (!documentItem) return;
    const previousCustomerId = documentItem.customerId;
    const previousType = documentItem.type;
    const previousDueDate = formatDateInput(documentItem.dueDate);
    documentItem.type = normalizeDocumentType(values.type);
    documentItem.status = normalizeDocumentStatus(values.status);
    documentItem.title = String(values.title || "").slice(0, 220);
    documentItem.customerId = String(values.customerId || "").slice(0, 120);
    documentItem.issueDate = formatDateInput(values.issueDate) || isoDateOnly();
    documentItem.dueDate = formatDateInput(values.dueDate) || defaultDueDateForType(documentItem.type, documentItem.issueDate);
    documentItem.paymentMethod = String(values.paymentMethod || "").slice(0, 220);
    documentItem.paymentTerms = String(values.paymentTerms || "").slice(0, 220);
    documentItem.customerName = String(values.customerName || "").slice(0, 180);
    documentItem.customerEmail = String(values.customerEmail || "").slice(0, 220);
    documentItem.customerPhone = String(values.customerPhone || "").slice(0, 80);
    documentItem.customerAddress = String(values.customerAddress || "").slice(0, 400);
    documentItem.customerType = normalizeCustomerType(values.customerType);
    documentItem.deliveryAddress = String(values.deliveryAddress || "").slice(0, 400);
    documentItem.noteToCustomer = String(values.noteToCustomer || "").slice(0, 1800);
    documentItem.internalNote = String(values.internalNote || "").slice(0, 1400);
    documentItem.updatedAt = currentIso();
    if (documentItem.customerId && documentItem.customerId !== previousCustomerId && !values.customerName) {
      applyCustomerSnapshotToDocument(documentItem, documentItem.customerId);
    }
    if (!documentItem.number) {
      documentItem.number = nextDocumentNumber(documentItem.type, documentItem.issueDate);
    }
    if (previousType !== documentItem.type) {
      const displayNumber = documentDisplayNumber(documentItem);
      documentItem.number = displayNumber || nextDocumentNumber(documentItem.type, documentItem.issueDate);
      documentItem.title = rewriteDocumentTitleForType(documentItem.title, documentItem.type, documentSnapshot(documentItem));
      if (!documentItem.noteToCustomer || documentItem.noteToCustomer === documentDefaultNote(previousType)) {
        documentItem.noteToCustomer = documentDefaultNote(documentItem.type);
      }
      const previousDefaultDueDate = defaultDueDateForType(previousType, documentItem.issueDate);
      if (!previousDueDate || previousDueDate === documentItem.issueDate || previousDueDate === previousDefaultDueDate) {
        documentItem.dueDate = defaultDueDateForType(documentItem.type, documentItem.issueDate);
      }
    } else {
      documentItem.title = rewriteDocumentTitleForType(documentItem.title, documentItem.type, documentSnapshot(documentItem));
    }
    upsertCustomerFromDocument(documentItem, { link: true });
    render();
    queueAutoPush();
    showToast("Document enregistre");
  }

  function updateSelectedCustomer(values) {
    const customer = selectedCustomer();
    if (!customer) return;
    customer.name = String(values.name || "").slice(0, 180);
    customer.email = String(values.email || "").slice(0, 220);
    customer.phone = String(values.phone || "").slice(0, 80);
    customer.address = String(values.address || "").slice(0, 400);
    customer.customerType = normalizeCustomerType(values.customerType);
    customer.note = String(values.note || "").slice(0, 1200);
    customer.deviceHint = String(values.deviceHint || "").slice(0, 160);
    customer.updatedAt = currentIso();
    render();
    queueAutoPush();
    showToast("Client enregistre");
  }

  function updateCompany(values) {
    state.company = normalizeCompany({
      ...state.company,
      ...values
    });
    if (state.company.vatMode === "franchise") {
      state.documents.forEach((documentItem) => {
        documentItem.lines.forEach((line) => {
          line.vatRate = 0;
        });
        documentItem.updatedAt = currentIso();
      });
    }
    render();
    queueAutoPush();
    showToast("Reglages enregistres");
  }

  function updateLine(lineId, fieldName, rawValue) {
    const documentItem = selectedDocument();
    if (!documentItem) return;
    const line = documentItem.lines.find((item) => item.id === lineId);
    if (!line) return;
    if (fieldName === "qty") line.qty = normalizeQty(rawValue);
    if (fieldName === "purchaseTtc") {
      line.purchaseTtc = normalizeMoney(rawValue);
      line.purchaseHt = purchaseHtFromTtc(line.purchaseTtc);
      line.unitHt = saleHtFromMargin(line.purchaseTtc, line.marginRate ?? state.company.defaultMarginRate);
    }
    if (fieldName === "purchaseHt") {
      line.purchaseHt = normalizeMoney(rawValue);
      line.purchaseTtc = purchaseTtcFromHt(line.purchaseHt);
      line.unitHt = saleHtFromMargin(line.purchaseTtc, line.marginRate ?? state.company.defaultMarginRate);
    }
    if (fieldName === "marginRate") {
      line.marginRate = normalizeMarginRate(rawValue);
      const { purchaseHt, purchaseTtc } = resolvePurchaseValues(line);
      line.purchaseHt = purchaseHt;
      line.purchaseTtc = purchaseTtc;
      line.unitHt = saleHtFromMargin(purchaseTtc, line.marginRate);
    }
    if (fieldName === "unitHt") {
      line.unitHt = normalizeMoney(rawValue);
      const { purchaseHt, purchaseTtc } = resolvePurchaseValues(line);
      line.purchaseHt = purchaseHt;
      line.purchaseTtc = purchaseTtc;
      line.marginRate = marginRateFromSale(purchaseTtc, line.unitHt);
    }
    if (fieldName === "vatRate") line.vatRate = state.company.vatMode === "franchise" ? 0 : normalizeVatRate(rawValue);
    if (fieldName === "description") line.description = String(rawValue || "").slice(0, 260);
    if (fieldName === "reference") {
      line.reference = String(rawValue || "").slice(0, 120);
      const catalogItem = findCatalogItemByReference(line.reference);
      if (catalogItem) {
        applyCatalogItemToLine(line, catalogItem);
        showToast(`Reference ${catalogItem.reference} appliquee`);
      }
    }
    documentItem.updatedAt = currentIso();
    render();
    queueAutoPush();
  }

  function addLineToSelectedDocument(lineData = {}) {
    const documentItem = selectedDocument();
    if (!documentItem) return;
    documentItem.lines.push(normalizeLine({
      id: createId("line"),
      qty: 1,
      purchaseHt: 0,
      purchaseTtc: 0,
      marginRate: state.company.defaultMarginRate,
      unitHt: 0,
      vatRate: state.company.vatMode === "franchise" ? 0 : 20,
      ...lineData
    }));
    documentItem.updatedAt = currentIso();
    render();
    queueAutoPush();
  }

  function removeLineFromSelectedDocument(lineId) {
    const documentItem = selectedDocument();
    if (!documentItem) return;
    const line = documentItem.lines.find((item) => item.id === lineId);
    if (!line) return;
    const lineLabel = line.description || line.reference || "cette ligne";
    if (!window.confirm(`Supprimer ${lineLabel} ?`)) return;
    documentItem.lines = documentItem.lines.filter((line) => line.id !== lineId);
    if (!documentItem.lines.length) {
      documentItem.lines = [normalizeLine({ id: createId("line"), qty: 1, purchaseHt: 0, marginRate: state.company.defaultMarginRate, vatRate: state.company.vatMode === "franchise" ? 0 : 20 })];
    }
    documentItem.updatedAt = currentIso();
    render();
    queueAutoPush();
    showToast("Ligne supprimee");
  }

  function duplicateSelectedDocument(targetType = "") {
    const documentItem = selectedDocument();
    if (!documentItem) return;
    const nextType = normalizeDocumentType(targetType || documentItem.type);
    const cloneDoc = normalizeDocument({
      ...clone(documentItem),
      id: createId("document"),
      type: nextType,
      number: nextDocumentNumber(nextType, isoDateOnly()),
      status: "draft",
      issueDate: isoDateOnly(),
      dueDate: documentItem.dueDate || isoDateOnly(),
      createdAt: currentIso(),
      updatedAt: currentIso(),
      lines: documentItem.lines.map((line) => ({ ...clone(line), id: createId("line") }))
    });
    state.documents.unshift(cloneDoc);
    state.selectedDocumentId = cloneDoc.id;
    ui.view = "documents";
    render();
    queueAutoPush();
    showToast(`${DOCUMENT_TYPE_LABELS[nextType]} cree depuis le document`);
  }

  function deleteSelectedDocument() {
    const documentItem = selectedDocument();
    if (!documentItem) return;
    const displayNumber = documentDisplayNumber(documentItem) || documentItem.number || DOCUMENT_TYPE_LABELS[documentItem.type];
    if (!window.confirm(`Supprimer ${displayNumber} ?`)) return;
    state.documents = state.documents.filter((item) => item.id !== documentItem.id);
    normalizeSelections();
    render();
    queueAutoPush();
    showToast("Document supprime");
  }

  function deleteSelectedCustomer() {
    const customer = selectedCustomer();
    if (!customer) return;
    if (!window.confirm(`Supprimer le client ${customer.name} ?`)) return;
    state.documents.forEach((documentItem) => {
      if (documentItem.customerId === customer.id) {
        documentItem.customerId = "";
      }
    });
    state.customers = state.customers.filter((item) => item.id !== customer.id);
    normalizeSelections();
    render();
    queueAutoPush();
    showToast("Client supprime");
  }

  function addCatalogItem(values) {
    const label = String(values.label || "").trim();
    if (!label) {
      showToast("Libelle catalogue manquant");
      return;
    }
    const purchaseHt = normalizeMoney(values.purchaseHt);
    const purchaseTtc = purchaseTtcFromHt(purchaseHt);
    const marginRate = normalizeMarginRate(values.marginRate || state.company.defaultMarginRate);
    const unitHt = values.unitHt !== undefined && values.unitHt !== null && values.unitHt !== ""
      ? normalizeMoney(values.unitHt)
      : saleHtFromMargin(purchaseTtc, marginRate);
    const item = normalizeCatalogItem({
      id: createId("catalog"),
      label,
      reference: values.reference,
      purchaseHt,
      purchaseTtc,
      marginRate,
      unitHt,
      vatRate: state.company.vatMode === "franchise" ? 0 : values.vatRate
    });
    state.catalog.unshift(item);
    render();
    queueAutoPush();
    showToast("Produit catalogue ajoute");
  }

  function removeCatalogItem(id) {
    const item = state.catalog.find((entry) => entry.id === id);
    if (!item) return;
    if (!window.confirm(`Supprimer le produit "${item.label}" ?`)) return;
    state.catalog = state.catalog.filter((item) => item.id !== id);
    render();
    queueAutoPush();
    showToast("Produit supprime");
  }

  function injectCatalogItem(id) {
    const catalogItem = state.catalog.find((item) => item.id === id);
    if (!catalogItem) return;
    if (ui.view !== "documents" || !selectedDocument()) {
      const customerId = state.selectedCustomerId || "";
      createDocument({
        type: "quote",
        customerId,
        title: catalogItem.label,
        issueDate: isoDateOnly(),
        dueDate: isoDateOnly(),
        lines: [{
          id: createId("line"),
          description: catalogItem.label,
          reference: catalogItem.reference,
          purchaseTtc: catalogItem.purchaseTtc,
          purchaseHt: catalogItem.purchaseHt,
          marginRate: catalogItem.marginRate,
          unitHt: catalogItem.unitHt,
          qty: 1,
          vatRate: state.company.vatMode === "franchise" ? 0 : catalogItem.vatRate
        }]
      });
      showToast("Devis cree avec le produit");
      return;
    }
    addLineToSelectedDocument({
      description: catalogItem.label,
      reference: catalogItem.reference,
      purchaseTtc: catalogItem.purchaseTtc,
      purchaseHt: catalogItem.purchaseHt,
      marginRate: catalogItem.marginRate,
      unitHt: catalogItem.unitHt,
      qty: 1,
      vatRate: state.company.vatMode === "franchise" ? 0 : catalogItem.vatRate
    });
    showToast("Produit ajoute au document");
  }

  function findCatalogItemByReference(reference) {
    const normalized = String(reference || "").trim().toLowerCase();
    if (!normalized) return null;
    return state.catalog.find((item) => String(item.reference || "").trim().toLowerCase() === normalized) || null;
  }

  function applyCatalogItemToLine(line, catalogItem) {
    const { purchaseHt, purchaseTtc } = resolvePurchaseValues(catalogItem);
    line.description = catalogItem.label || line.description;
    line.reference = catalogItem.reference || line.reference;
    line.purchaseHt = purchaseHt;
    line.purchaseTtc = purchaseTtc;
    line.marginRate = normalizeMarginRate(catalogItem.marginRate ?? state.company.defaultMarginRate);
    line.unitHt = normalizeMoney(catalogItem.unitHt || saleHtFromMargin(purchaseTtc, line.marginRate));
    line.vatRate = state.company.vatMode === "franchise" ? 0 : normalizeVatRate(catalogItem.vatRate);
  }

  function updateCatalogFormCalculations(form, sourceName = "") {
    if (!form) return;
    const purchaseHtInput = form.elements.purchaseHt;
    const purchaseTtcPreview = form.elements.purchaseTtcPreview;
    const marginRateInput = form.elements.marginRate;
    const unitHtInput = form.elements.unitHt;
    const purchaseHt = normalizeMoney(purchaseHtInput?.value);
    const purchaseTtc = purchaseTtcFromHt(purchaseHt);

    if (purchaseTtcPreview) {
      purchaseTtcPreview.value = purchaseTtc ? String(purchaseTtc) : "";
    }

    if (sourceName === "unitHt") {
      if (marginRateInput) {
        marginRateInput.value = String(marginRateFromSale(purchaseTtc, unitHtInput?.value));
      }
      return;
    }

    if (unitHtInput) {
      const marginRate = normalizeMarginRate(marginRateInput?.value || state.company.defaultMarginRate);
      unitHtInput.value = purchaseTtc ? String(saleHtFromMargin(purchaseTtc, marginRate)) : "";
    }
  }

  function refreshDocumentFromLinkedCustomer() {
    const documentItem = selectedDocument();
    if (!documentItem || !documentItem.customerId) return;
    applyCustomerSnapshotToDocument(documentItem, documentItem.customerId);
    render();
    queueAutoPush();
    showToast("Infos client rechargees");
  }

  function createOrUpdateCustomerFromSelectedDocument() {
    const documentItem = selectedDocument();
    if (!documentItem) return;
    if (!String(documentItem.customerName || "").trim()) {
      showToast("Nom client manquant");
      return;
    }
    const customer = upsertCustomerFromDocument(documentItem, { link: true });
    if (!customer) {
      showToast("Fiche client impossible");
      return;
    }
    documentItem.updatedAt = currentIso();
    ui.view = "customers";
    state.selectedCustomerId = customer.id;
    render();
    queueAutoPush();
    showToast("Fiche client mise a jour");
  }

  function mergeImportedCustomer(ticket) {
    const name = String(ticket.customerName || "").trim();
    if (!name) return false;
    const email = String(ticket.customerEmail || "").trim().toLowerCase();
    const phone = String(ticket.customerPhone || "").replace(/\s+/g, "");
    const key = email || phone || name.toLowerCase();
    const existing = state.customers.find((item) => {
      const itemKey = String(item.email || "").trim().toLowerCase()
        || String(item.phone || "").replace(/\s+/g, "")
        || String(item.name || "").trim().toLowerCase();
      return itemKey && itemKey === key;
    });

    const patch = {
      name,
      email: ticket.customerEmail || "",
      phone: ticket.customerPhone || "",
      address: ticket.customerAddress || "",
      customerType: ticket.customerType || "individual",
      note: [ticket.issue, ticket.technician ? `Technicien: ${ticket.technician}` : ""].filter(Boolean).join("\n"),
      source: "atelier-os",
      workshopCode: ticket.code || "",
      deviceHint: ticket.device || "",
      updatedAt: currentIso()
    };

    if (existing) {
      existing.name = existing.name || patch.name;
      existing.email = existing.email || patch.email;
      existing.phone = existing.phone || patch.phone;
      existing.address = existing.address || patch.address;
      existing.customerType = normalizeCustomerType(existing.customerType || patch.customerType);
      if (patch.note && !String(existing.note || "").includes(patch.note)) {
        existing.note = [existing.note, patch.note].filter(Boolean).join("\n").slice(0, 1200);
      }
      existing.source = "atelier-os";
      existing.workshopCode = patch.workshopCode || existing.workshopCode;
      existing.deviceHint = patch.deviceHint || existing.deviceHint;
      existing.updatedAt = patch.updatedAt;
      return false;
    }

    state.customers.unshift(normalizeCustomer({
      id: createId("customer"),
      ...patch,
      createdAt: currentIso()
    }));
    return true;
  }

  async function importCustomersFromAtelier(options = {}) {
    const code = validateSyncCode();
    if (!code) return;
    try {
      setSyncStatus("Import des clients AtelierOS...");
      const response = await fetch(`${syncRuntime.atelierApiUrl}?code=${encodeURIComponent(code)}`, {
        headers: { accept: "application/json" }
      });
      const result = await response.json();
      if (!response.ok) {
        throw new Error(result.error || "Import AtelierOS impossible.");
      }
      const payload = result.snapshot?.payload;
      if (!payload || payload.schema !== ATELIER_SYNC_SCHEMA) {
        throw new Error("Base AtelierOS illisible.");
      }
      const tickets = Array.isArray(payload.workspace?.tickets) ? payload.workspace.tickets : [];
      let created = 0;
      tickets.forEach((ticket) => {
        if (mergeImportedCustomer(ticket)) {
          created += 1;
        }
      });
      if (state.customers.length && !state.selectedCustomerId) {
        state.selectedCustomerId = state.customers[0].id;
      }
      render();
      queueAutoPush();
      setSyncStatus(`Import AtelierOS termine - ${created} nouveau${created > 1 ? "x" : ""} client${created > 1 ? "s" : ""}.`);
      if (!options.silent || created) {
        showToast(`${created} client${created > 1 ? "s" : ""} ajoute${created > 1 ? "s" : ""} depuis AtelierOS`);
      }
    } catch (error) {
      setSyncStatus(error.message || "Import AtelierOS impossible.", true);
      if (!options.silent) {
        showToast(error.message || "Import AtelierOS impossible");
      }
    }
  }

  function showToast(message) {
    if (!elements.toast) return;
    window.clearTimeout(toastTimer);
    elements.toast.textContent = message;
    elements.toast.hidden = false;
    toastTimer = window.setTimeout(() => {
      elements.toast.hidden = true;
      elements.toast.textContent = "";
    }, 2600);
  }

  function openModal(node) {
    if (node) node.hidden = false;
  }

  function closeModal(node) {
    if (node) node.hidden = true;
  }

  function lockedSyncCode() {
    return normalizeCode(DEFAULT_SYNC_CODE);
  }

  function setSyncStatus(message, isError = false) {
    if (!elements.syncStatus) return;
    elements.syncStatus.textContent = message;
    elements.syncStatus.classList.toggle("is-error", isError);
  }

  function readSyncCode() {
    return lockedSyncCode();
  }

  function storeSyncCode(code = DEFAULT_SYNC_CODE) {
    const normalized = lockedSyncCode();
    if (elements.syncInput) {
      elements.syncInput.value = normalized;
    }
    try {
      localStorage.setItem(SYNC_CODE_KEY, normalized);
    } catch {}
    updateSyncIndicator();
    restartSyncTransport();
    return normalized;
  }

  function validateSyncCode() {
    const code = storeSyncCode(readSyncCode());
    if (code.length < MIN_SYNC_CODE_LENGTH) {
      setSyncStatus(`Le code prive doit contenir au moins ${MIN_SYNC_CODE_LENGTH} caracteres.`, true);
      elements.syncInput?.focus();
      return "";
    }
    return code;
  }

  function generateSyncCode() {
    storeSyncCode(DEFAULT_SYNC_CODE);
    setSyncStatus(`Code ReFone restaure : ${lockedSyncCode()}`);
    showToast("Code RF-ERWANN actif");
  }

  function supportsLiveSync() {
    return false;
  }

  function liveSyncUrl(code) {
    const url = new URL(syncRuntime.apiUrl);
    url.searchParams.set("code", code);
    url.searchParams.set("stream", "1");
    return url.toString();
  }

  function closeLiveSync() {
    if (syncRuntime.eventSource) {
      syncRuntime.eventSource.close();
      syncRuntime.eventSource = null;
    }
    syncRuntime.streamConnected = false;
    updateSyncIndicator();
  }

  function formatSyncMoment(value) {
    if (!value) return "";
    try {
      return new Intl.DateTimeFormat("fr-FR", {
        day: "2-digit",
        month: "2-digit",
        hour: "2-digit",
        minute: "2-digit"
      }).format(new Date(value));
    } catch {
      return "";
    }
  }

  function updateSyncIndicator() {
    const hasCode = readSyncCode().length >= MIN_SYNC_CODE_LENGTH;
    document.querySelectorAll("[data-sync-dot]").forEach((dot) => dot.classList.toggle("is-ready", hasCode));
    document.querySelectorAll("[data-sync-label]").forEach((label) => {
      label.textContent = hasCode ? "Sync active" : "Synchroniser";
    });

    if (!elements.syncSummary) return;
    if (!hasCode) {
      elements.syncSummary.textContent = "Base locale uniquement.";
      return;
    }
    if (syncRuntime.streamConnected && syncRuntime.lastRemoteUpdatedAt) {
      elements.syncSummary.textContent = `Sync active - derniere base visible ${formatSyncMoment(syncRuntime.lastRemoteUpdatedAt)}.`;
      return;
    }
    if (syncRuntime.streamConnected) {
      elements.syncSummary.textContent = "Sync active - en attente de la premiere base.";
      return;
    }
    if (syncRuntime.lastRemoteUpdatedAt) {
      elements.syncSummary.textContent = `Sync active - derniere base visible ${formatSyncMoment(syncRuntime.lastRemoteUpdatedAt)}.`;
      return;
    }
    elements.syncSummary.textContent = "Code actif - liaison en cours.";
  }

  function currentSyncPayload() {
    return {
      schema: SYNC_SCHEMA,
      version: 1,
      source: window.location.protocol === "file:" ? "facture-os-desktop" : "facture-os-web",
      updatedAt: currentIso(),
      workspace: normalizeWorkspace(state)
    };
  }

  function normalizeSyncPayload(raw) {
    const input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
    return {
      schema: String(input.schema || SYNC_SCHEMA),
      version: Number(input.version || 1),
      source: String(input.source || "unknown").slice(0, 60),
      updatedAt: String(input.updatedAt || currentIso()).slice(0, 40),
      workspace: normalizeWorkspace(input.workspace || input.payload || {})
    };
  }

  function applyWorkspace(workspace) {
    const normalized = normalizeWorkspace(workspace);
    state.selectedDocumentId = normalized.selectedDocumentId;
    state.selectedCustomerId = normalized.selectedCustomerId;
    state.company = normalized.company;
    state.customers = normalized.customers;
    state.documents = normalized.documents;
    state.catalog = normalized.catalog;
    state.counters = normalized.counters;
  }

  function applyRemoteSnapshot(snapshot, statusMessage) {
    const payload = normalizeSyncPayload(snapshot?.payload);
    syncRuntime.isApplyingRemote = true;
    applyWorkspace(payload.workspace);
    syncRuntime.lastRemoteUpdatedAt = String(snapshot?.updatedAt || payload.updatedAt || currentIso());
    syncRuntime.isApplyingRemote = false;
    render();
    setSyncStatus(`${statusMessage} - ${formatSyncMoment(syncRuntime.lastRemoteUpdatedAt)}`);
  }

  async function pushSync(options = {}) {
    const code = validateSyncCode();
    if (!code || syncRuntime.isApplyingRemote) return;
    if (!options.silent) {
      setSyncStatus("Envoi de la base FactureOS...");
    }
    syncRuntime.isPushing = true;
    try {
      const response = await fetch(syncRuntime.apiUrl, {
        method: "PUT",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          code,
          payload: currentSyncPayload()
        }),
        keepalive: options.keepalive === true
      });
      const result = await response.json();
      if (!response.ok) {
        throw new Error(result.error || "Synchronisation impossible.");
      }
      syncRuntime.lastRemoteUpdatedAt = String(result.updatedAt || currentIso());
      updateSyncIndicator();
      setSyncStatus(`Base envoyee - ${formatSyncMoment(syncRuntime.lastRemoteUpdatedAt)}`);
      if (!options.silent) showToast("Base FactureOS synchronisee");
    } catch (error) {
      if (!options.silent) {
        setSyncStatus(error.message || "Synchronisation impossible.", true);
      }
    } finally {
      syncRuntime.isPushing = false;
    }
  }

  async function pullSync(options = {}) {
    const code = validateSyncCode();
    if (!code) return;
    if (!options.silent) {
      setSyncStatus("Recuperation de la base distante...");
    }
    try {
      const response = await fetch(`${syncRuntime.apiUrl}?code=${encodeURIComponent(code)}`, {
        headers: { accept: "application/json" }
      });
      const result = await response.json();
      if (!response.ok) {
        if (options.allowMissing && response.status === 404) {
          if (!options.silent) setSyncStatus("Aucune base distante pour ce code pour le moment.");
          return;
        }
        throw new Error(result.error || "Recuperation impossible.");
      }
      const incomingAt = String(result.snapshot?.updatedAt || "");
      if (!options.force && incomingAt && syncRuntime.lastRemoteUpdatedAt && incomingAt <= syncRuntime.lastRemoteUpdatedAt) {
        if (!options.silent) {
          setSyncStatus(`Base deja a jour - ${formatSyncMoment(syncRuntime.lastRemoteUpdatedAt)}`);
        }
        return;
      }
      applyRemoteSnapshot(result.snapshot, "Base recue");
      if (!options.silent) {
        showToast("Base FactureOS recuperee");
      }
    } catch (error) {
      if (!options.silent) {
        setSyncStatus(error.message || "Recuperation impossible.", true);
      }
    }
  }

  function startLiveSync() {
    const code = readSyncCode();
    if (!supportsLiveSync() || code.length < MIN_SYNC_CODE_LENGTH) return false;

    closeLiveSync();
    const source = new EventSource(liveSyncUrl(code));
    syncRuntime.eventSource = source;

    source.onopen = () => {
      if (syncRuntime.eventSource !== source) return;
      syncRuntime.streamConnected = true;
      updateSyncIndicator();
      setSyncStatus("Connexion live active.");
    };

    source.addEventListener("snapshot", (event) => {
      if (syncRuntime.eventSource !== source) return;
      try {
        const data = JSON.parse(event.data || "{}");
        const snapshot = data?.snapshot;
        const incomingAt = String(snapshot?.updatedAt || "");
        if (!incomingAt) return;
        if (syncRuntime.lastRemoteUpdatedAt && incomingAt <= syncRuntime.lastRemoteUpdatedAt) {
          syncRuntime.streamConnected = true;
          updateSyncIndicator();
          return;
        }
        applyRemoteSnapshot(snapshot, "Base recue en direct");
      } catch (error) {
        console.warn("Flux FactureOS illisible", error);
      }
    });

    source.addEventListener("keepalive", () => {
      if (syncRuntime.eventSource !== source) return;
      syncRuntime.streamConnected = true;
      updateSyncIndicator();
    });

    source.onerror = () => {
      if (syncRuntime.eventSource !== source) return;
      syncRuntime.streamConnected = false;
      updateSyncIndicator();
    };

    return true;
  }

  function queueAutoPush() {
    const code = readSyncCode();
    if (code.length < MIN_SYNC_CODE_LENGTH || syncRuntime.isApplyingRemote) return;
    window.clearTimeout(syncRuntime.pushTimer);
    syncRuntime.pushTimer = window.setTimeout(() => {
      pushSync({ silent: true });
    }, SYNC_PUSH_DEBOUNCE_MS);
  }

  function restartSyncTransport() {
    window.clearInterval(syncRuntime.pollTimer);
    closeLiveSync();
    const code = readSyncCode();
    if (code.length < MIN_SYNC_CODE_LENGTH) {
      updateSyncIndicator();
      return;
    }
    if (startLiveSync()) {
      return;
    }
    syncRuntime.pollTimer = window.setInterval(() => {
      if (document.visibilityState !== "visible") return;
      pullSync({ silent: true, allowMissing: true });
    }, SYNC_POLL_INTERVAL_MS);
    updateSyncIndicator();
  }

  function formValues(form) {
    return Object.fromEntries(new FormData(form).entries());
  }

  function sanitizeFilename(value) {
    return String(value || "")
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, "-")
      .replace(/^-+|-+$/g, "")
      .slice(0, 80) || "document";
  }

  function loadImage(src) {
    return new Promise((resolve, reject) => {
      const image = new Image();
      if (/^https?:\/\//i.test(src)) {
        image.crossOrigin = "anonymous";
      }
      image.onload = () => resolve(image);
      image.onerror = () => reject(new Error("Chargement image impossible."));
      image.src = src;
    });
  }

  function dataUrlToBytes(dataUrl) {
    const value = String(dataUrl || "");
    const commaIndex = value.indexOf(",");
    if (!value.startsWith("data:") || commaIndex < 0) {
      throw new Error("Logo data URL invalide.");
    }
    const header = value.slice(0, commaIndex);
    const payload = value.slice(commaIndex + 1);
    if (header.includes(";base64")) {
      const binary = atob(payload);
      const bytes = new Uint8Array(binary.length);
      for (let index = 0; index < binary.length; index += 1) {
        bytes[index] = binary.charCodeAt(index);
      }
      return bytes;
    }
    return new TextEncoder().encode(decodeURIComponent(payload));
  }

  async function fetchBytes(source) {
    const response = await fetch(source, { cache: "no-store" });
    if (!response.ok) {
      throw new Error(`Logo introuvable (${response.status}).`);
    }
    return new Uint8Array(await response.arrayBuffer());
  }

  async function imageToPngBytes(source) {
    const image = await loadImage(source);
    const canvas = document.createElement("canvas");
    canvas.width = image.naturalWidth || 512;
    canvas.height = image.naturalHeight || 512;
    const context = canvas.getContext("2d");
    if (!context) throw new Error("Canvas indisponible.");
    context.drawImage(image, 0, 0, canvas.width, canvas.height);
    const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
    if (!blob) throw new Error("Conversion logo impossible.");
    return new Uint8Array(await blob.arrayBuffer());
  }

  async function logoPngBytes(source = "") {
    const candidates = [
      window.__REFONE_PDF_LOGO_DATA_URL,
      source,
      document.querySelector(".facture-brand-mark img")?.src,
      "assets/media/refone-logo.png"
    ].filter(Boolean);

    let lastError = null;
    for (const candidate of candidates) {
      try {
        if (String(candidate).startsWith("data:")) {
          return dataUrlToBytes(candidate);
        }
        try {
          return await fetchBytes(candidate);
        } catch (error) {
          lastError = error;
        }
        return await imageToPngBytes(candidate);
      } catch (error) {
        lastError = error;
      }
    }

    throw lastError || new Error("Chargement logo impossible.");
  }

  function wrapText(text, font, size, maxWidth) {
    const paragraphs = pdfSafeText(text).split(/\n+/);
    const lines = [];
    for (const paragraph of paragraphs) {
      const words = paragraph.split(/\s+/).filter(Boolean);
      if (!words.length) {
        lines.push("");
        continue;
      }
      let current = words[0];
      for (let index = 1; index < words.length; index += 1) {
        const candidate = `${current} ${words[index]}`;
        if (font.widthOfTextAtSize(candidate, size) <= maxWidth) {
          current = candidate;
        } else {
          lines.push(current);
          current = words[index];
        }
      }
      lines.push(current);
    }
    return lines;
  }

  function pdfSafeText(value) {
    return String(value ?? "")
      .normalize("NFKC")
      .replace(/[\u00a0\u2007\u202f]/g, " ")
      .replace(/[\u2010\u2011\u2012\u2013\u2014\u2212]/g, "-")
      .replace(/[\u2018\u2019\u201a\u2032]/g, "'")
      .replace(/[\u201c\u201d\u201e\u2033]/g, '"')
      .replace(/\u2026/g, "...")
      .replace(/\u20ac/g, "EUR")
      .replace(/\u0152/g, "OE")
      .replace(/\u0153/g, "oe")
      .replace(/[^\n\r\t\x20-\x7e\u00a1-\u00ff]/g, "");
  }

  function pdfText(page, text, x, y, options) {
    page.drawText(pdfSafeText(text), {
      x,
      y,
      size: options.size,
      font: options.font,
      color: options.color
    });
  }

  function drawWrappedText(page, text, x, y, maxWidth, lineHeight, options) {
    const lines = wrapText(text, options.font, options.size, maxWidth);
    let cursor = y;
    lines.forEach((line) => {
      pdfText(page, line, x, cursor, options);
      cursor -= lineHeight;
    });
    return cursor;
  }

  function rgbColor(red, green, blue) {
    return PDFLib.rgb(red / 255, green / 255, blue / 255);
  }

  function ensurePdfPageDrawingApi(pdfPage) {
    if (typeof pdfPage.drawRoundedRectangle === "function") {
      return pdfPage;
    }
    pdfPage.drawRoundedRectangle = (options) => {
      const { borderRadius, ...rectangleOptions } = options;
      return pdfPage.drawRectangle(rectangleOptions);
    };
    return pdfPage;
  }

  async function buildPdfBlob(documentItem) {
    const { PDFDocument, StandardFonts } = PDFLib;
    const pdf = await PDFDocument.create();
    const fontRegular = await pdf.embedFont(StandardFonts.Helvetica);
    const fontBold = await pdf.embedFont(StandardFonts.HelveticaBold);
    let pdfLogoImage = null;

    try {
      const pdfLogoBytes = await logoPngBytes("assets/media/refone-pdf-logo.png");
      pdfLogoImage = await pdf.embedPng(pdfLogoBytes);
    } catch {}

    const ink = rgbColor(15, 17, 21);
    const muted = rgbColor(86, 91, 101);
    const faint = rgbColor(145, 150, 160);
    const line = rgbColor(215, 218, 224);
    const softFill = rgbColor(247, 248, 250);
    const white = PDFLib.rgb(1, 1, 1);

    const totals = documentTotals(documentItem);
    const typeLabel = DOCUMENT_TYPE_LABELS[documentItem.type];
    const displayNumber = documentDisplayNumber(documentItem) || documentItem.number || '';
    const displayTitle = documentDisplayTitle(documentItem);
    const displayNote = documentDisplayNote(documentItem);
    const company = state.company;
    const pageSize = [595.28, 841.89];
    const pageWidth = pageSize[0];
    const pageHeight = pageSize[1];
    const marginX = 42;
    const pageRight = pageWidth - marginX;
    const tableLeft = marginX;
    const tableRight = pageRight;
    const tableWidth = tableRight - tableLeft;
    const bottomLimit = 92;
    const tableColumns = {
      qtyRight: 328,
      unitLeft: 348,
      priceRight: 438,
      vatRight: 484,
      amountRight: tableRight
    };
    const descriptionColumnWidth = 238;
    const tableHeaderSize = 9.4;
    const tableValueSize = 9.1;
    let page = pdf.addPage(pageSize);
    let y = 770;

    const drawText = (targetPage, text, x, posY, options = {}) => {
      pdfText(targetPage, text, x, posY, {
        font: options.font || fontRegular,
        size: options.size || 10,
        color: options.color || ink
      });
    };

    const textWidth = (text, font = fontRegular, size = 10) => font.widthOfTextAtSize(pdfSafeText(text), size);

    const drawRightText = (targetPage, text, rightX, posY, options = {}) => {
      const font = options.font || fontRegular;
      const size = options.size || 10;
      drawText(targetPage, text, rightX - textWidth(text, font, size), posY, { ...options, font, size });
    };

    const drawLine = (targetPage, x1, posY, x2, options = {}) => {
      targetPage.drawLine({
        start: { x: x1, y: posY },
        end: { x: x2, y: posY },
        thickness: options.thickness || 0.8,
        color: options.color || line
      });
    };

    const splitLines = (value, fallback = '', limit = 5) => {
      const rawLines = pdfSafeText(value || fallback || '').split(/\n+/).map((item) => item.trim()).filter(Boolean);
      return rawLines.length ? rawLines.slice(0, limit) : [];
    };

    const compactContactLine = (...values) => values.map((value) => pdfSafeText(value).trim()).filter(Boolean).join(' - ');

    const validVatNumber = (value) => {
      const normalized = String(value || '').replace(/\s+/g, '').toUpperCase();
      return /^FR[A-Z0-9]{2}\d{9}$/.test(normalized) ? normalized : '';
    };

    const drawBrand = (targetPage, compact = false) => {
      if (pdfLogoImage) {
        const naturalSize = pdfLogoImage.scale(1);
        const logoWidth = compact ? 84 : 124;
        const logoHeight = logoWidth * (naturalSize.height / naturalSize.width);
        const logoY = compact ? pageHeight - logoHeight - 11 : pageHeight - logoHeight - 30;
        targetPage.drawImage(pdfLogoImage, {
          x: marginX,
          y: logoY,
          width: logoWidth,
          height: logoHeight
        });
        return;
      }

      const brandY = compact ? pageHeight - 52 : pageHeight - 86;
      drawText(targetPage, company.legalName || 'ReFone', marginX, brandY, {
        font: fontBold,
        size: compact ? 18 : 23,
        color: ink
      });
      drawText(targetPage, company.businessType || 'Reparation appareils', marginX, brandY - 17, {
        font: fontRegular,
        size: compact ? 8.5 : 9.5,
        color: muted
      });
    };

    const drawCompanyBlock = (targetPage) => {
      const addressLines = splitLines(company.address || company.locality, '', 2);
      const companyLines = [
        company.legalName || 'ReFone',
        company.businessType || '',
        ...addressLines,
        company.email || '',
        company.phone || '',
        'refonetech.com'
      ].filter(Boolean);
      let cursor = pageHeight - 60;
      companyLines.forEach((item, index) => {
        drawRightText(targetPage, item, pageRight, cursor, {
          font: index === 0 ? fontBold : fontRegular,
          size: index === 0 ? 11 : 10.5,
          color: index === 0 ? ink : muted
        });
        cursor -= 15;
      });
    };

    const drawFirstPageHeader = () => {
      drawBrand(page, false);
      drawCompanyBlock(page);
      drawLine(page, 0, 684, pageWidth, { thickness: 0.8, color: ink });

      const clientLines = [
        documentItem.customerName || 'Client a completer',
        ...splitLines(documentItem.customerAddress, 'Adresse a completer', 4),
        compactContactLine(documentItem.customerEmail, documentItem.customerPhone)
      ].filter(Boolean);

      let clientY = 642;
      clientLines.forEach((item, index) => {
        drawText(page, item, marginX, clientY, {
          font: index === 0 ? fontBold : fontRegular,
          size: index === 0 ? 11 : 10.5,
          color: index === 0 ? ink : muted
        });
        clientY -= 15;
      });

      const dateLabel = documentItem.type === 'quote' ? 'Validite' : 'Echeance';
      const documentLines = [
        typeLabel,
        `${typeLabel} n°: ${displayNumber || '-'}`,
        `Date: ${shortDate(documentItem.issueDate)}`,
        `${dateLabel}: ${shortDate(documentItem.dueDate)}`
      ];
      let documentY = 642;
      documentLines.forEach((item, index) => {
        drawRightText(page, item, pageRight, documentY, {
          font: index === 0 ? fontBold : fontRegular,
          size: index === 0 ? 13 : 10.5,
          color: index === 0 ? ink : muted
        });
        documentY -= index === 0 ? 28 : 20;
      });

      y = 540;
    };

    const drawContinuationHeader = () => {
      drawBrand(page, true);
      drawRightText(page, `${typeLabel} - ${displayNumber || '-'}`, pageRight, pageHeight - 52, {
        font: fontBold,
        size: 10.5,
        color: ink
      });
      drawLine(page, marginX, pageHeight - 76, pageRight, { thickness: 0.8, color: ink });
      y = pageHeight - 104;
    };

    const drawObject = () => {
      if (!displayTitle) return;
      drawText(page, 'Objet', marginX, y, { font: fontBold, size: 9.5, color: faint });
      y -= 18;
      y = drawWrappedText(page, displayTitle, marginX, y, tableWidth, 13, {
        font: fontRegular,
        size: 10.5,
        color: ink
      }) - 10;
    };

    const drawTableHeader = () => {
      drawLine(page, tableLeft, y, tableRight, { thickness: 1, color: ink });
      y -= 26;
      drawText(page, 'Description', tableLeft, y, { font: fontBold, size: 10.5, color: ink });
      drawRightText(page, 'Quantite', tableColumns.qtyRight, y, { font: fontBold, size: tableHeaderSize, color: ink });
      drawText(page, 'Unite', tableColumns.unitLeft, y, { font: fontBold, size: tableHeaderSize, color: ink });
      drawRightText(page, 'Prix', tableColumns.priceRight, y, { font: fontBold, size: tableHeaderSize, color: ink });
      drawRightText(page, 'TVA', tableColumns.vatRight, y, { font: fontBold, size: tableHeaderSize, color: ink });
      drawRightText(page, 'Montant', tableColumns.amountRight, y, { font: fontBold, size: tableHeaderSize, color: ink });
      y -= 20;
      drawLine(page, tableLeft, y, tableRight, { thickness: 0.8, color: ink });
      y -= 24;
    };

    const addPage = (includeTableHeader = true) => {
      page = pdf.addPage(pageSize);
      drawContinuationHeader();
      if (includeTableHeader) {
        drawTableHeader();
      }
    };

    const ensureSpace = (spaceNeeded) => {
      if (y - spaceNeeded < bottomLimit) {
        addPage();
      }
    };

    const drawTableRows = () => {
      const rows = Array.isArray(documentItem.lines) && documentItem.lines.length ? documentItem.lines : [];
      rows.forEach((lineItem) => {
        const qty = normalizeQty(lineItem.qty);
        const unitHt = normalizeMoney(lineItem.unitHt);
        const totalHt = normalizeMoney(unitHt * qty);
        const vatRate = state.company.vatMode === 'franchise' ? 0 : normalizeVatRate(lineItem.vatRate);
        const description = lineItem.description || lineItem.reference || 'Prestation';
        const descriptionLines = wrapText(description, fontRegular, 10.5, descriptionColumnWidth);
        let lineIndex = 0;
        let isFirstChunk = true;

        while (lineIndex < descriptionLines.length || isFirstChunk) {
          if (y - 48 < bottomLimit) {
            addPage();
          }

          const availableLines = Math.max(1, Math.floor((y - bottomLimit - 24) / 13));
          const chunkLines = descriptionLines.slice(lineIndex, lineIndex + availableLines);
          const visibleLines = chunkLines.length ? chunkLines : [""];
          const rowHeight = Math.max(48, visibleLines.length * 13 + 24);

          let rowTextY = y;
          visibleLines.forEach((textLine) => {
            drawText(page, textLine, tableLeft, rowTextY, { font: fontRegular, size: 10.5, color: ink });
            rowTextY -= 13;
          });

          if (isFirstChunk) {
            drawRightText(page, String(qty).replace('.', ','), tableColumns.qtyRight, y, { font: fontRegular, size: tableValueSize, color: ink });
            drawText(page, 'pce', tableColumns.unitLeft, y, { font: fontRegular, size: tableValueSize, color: muted });
            drawRightText(page, currency(unitHt), tableColumns.priceRight, y, { font: fontRegular, size: tableValueSize, color: ink });
            drawRightText(page, `${vatRate}%`, tableColumns.vatRight, y, { font: fontRegular, size: tableValueSize, color: ink });
            drawRightText(page, currency(totalHt), tableColumns.amountRight, y, { font: fontRegular, size: tableValueSize, color: ink });
          }

          y -= rowHeight;
          drawLine(page, tableLeft, y + 12, tableRight, { thickness: 0.6, color: line });
          lineIndex += visibleLines.length;
          isFirstChunk = false;
        }
      });
    };

    const drawTotals = () => {
      ensureSpace(106);
      drawLine(page, 310, y, tableRight, { thickness: 0.8, color: line });
      y -= 22;
      const totalRows = [
        ['Sous-total HT', currency(totals.totalHt), false],
        [state.company.vatMode === 'franchise' ? 'TVA non applicable' : 'TVA', currency(totals.totalVat), false],
        ['Montant Total EUR', currency(totals.totalTtc), true]
      ];
      totalRows.forEach(([label, value, isStrong]) => {
        drawText(page, label, 310, y, {
          font: isStrong ? fontBold : fontRegular,
          size: isStrong ? 11 : 10.5,
          color: ink
        });
        drawRightText(page, value, tableRight, y, {
          font: isStrong ? fontBold : fontRegular,
          size: isStrong ? 11 : 10.5,
          color: ink
        });
        y -= isStrong ? 24 : 20;
      });
      y -= 10;
    };

    const drawPaymentAndNotes = () => {
      const vatLine = company.vatMode === 'franchise'
        ? (company.vatNote || TVA_FRANCHISE_NOTE)
        : (validVatNumber(company.vatNumber) ? `Numero de TVA intracommunautaire : ${validVatNumber(company.vatNumber)}` : '');
      const legacyPaymentTerms = 'Paiement a reception du document.';
      const footerLines = [
        displayNote,
        !documentItem.paymentTerms || documentItem.paymentTerms === legacyPaymentTerms ? company.paymentTerms : documentItem.paymentTerms,
        documentItem.paymentMethod || company.paymentMethod,
        vatLine,
        company.footerNote,
        company.latePenalty
      ].filter(Boolean);

      if (!footerLines.length) return;
      ensureSpace(38);
      drawLine(page, tableLeft, y, tableRight, { thickness: 0.8, color: line });
      y -= 18;
      footerLines.forEach((item) => {
        const lines = wrapText(item, fontRegular, 8.8, tableWidth);
        lines.forEach((textLine) => {
          if (y - 12 < bottomLimit) {
            addPage(false);
            drawLine(page, tableLeft, y, tableRight, { thickness: 0.8, color: line });
            y -= 18;
          }
          drawText(page, textLine, tableLeft, y, { font: fontRegular, size: 8.8, color: muted });
          y -= 11.5;
        });
        y -= 5;
      });
    };

    const drawSignature = () => {
      if (documentItem.type !== 'quote' && documentItem.type !== 'order') return;
      if (y - 72 < bottomLimit) {
        addPage(false);
      }
      y -= 4;
      drawText(page, documentItem.type === 'quote' ? 'Bon pour accord' : 'Validation commande', tableLeft, y, {
        font: fontBold,
        size: 9.5,
        color: ink
      });
      drawText(page, 'Nom, date et signature du client', tableLeft, y - 18, {
        font: fontRegular,
        size: 9,
        color: muted
      });
      drawLine(page, 330, y - 34, tableRight, { thickness: 0.8, color: line });
      y -= 58;
    };

    const drawTermsPage = () => {
      page = pdf.addPage(pageSize);
      drawBrand(page, true);
      drawRightText(page, 'Conditions generales de vente', pageRight, pageHeight - 52, {
        font: fontBold,
        size: 10.5,
        color: ink
      });
      drawLine(page, marginX, pageHeight - 76, pageRight, { thickness: 0.8, color: ink });

      const parseTermsText = () => {
        const blocks = normalizeTermsText(company.termsText, company)
          .split(/\n{2,}/)
          .map((block) => block.trim())
          .filter(Boolean);
        let title = 'CONDITIONS GENERALES DE VENTE - ReFone';
        if (blocks.length && /^conditions/i.test(pdfSafeText(blocks[0]))) {
          title = blocks.shift();
        }
        const sections = blocks.map((block) => {
          const lines = block.split(/\n+/).map((lineText) => lineText.trim()).filter(Boolean);
          const sectionTitle = lines.shift() || 'Condition';
          return {
            title: sectionTitle,
            body: lines.join(' ')
          };
        }).filter((section) => section.title || section.body);
        return {
          title,
          sections: sections.length ? sections : [{ title: title, body: '' }]
        };
      };

      const parsedTerms = parseTermsText();
      drawText(page, parsedTerms.title, marginX, pageHeight - 104, {
        font: fontBold,
        size: 13.5,
        color: ink
      });

      const columnGap = 22;
      const columnWidth = (tableWidth - columnGap) / 2;
      const columnLefts = [marginX, marginX + columnWidth + columnGap];
      const topY = pageHeight - 132;
      const bottomY = 84;
      const presets = [
        { titleSize: 7.5, bodySize: 6.85, titleLineHeight: 8.2, bodyLineHeight: 7.2, gap: 6 },
        { titleSize: 7.05, bodySize: 6.35, titleLineHeight: 7.7, bodyLineHeight: 6.75, gap: 5 },
        { titleSize: 6.6, bodySize: 5.95, titleLineHeight: 7.2, bodyLineHeight: 6.25, gap: 4.5 }
      ];

      const layoutTerms = (preset, shouldDraw = false) => {
        let columnIndex = 0;
        let cursor = topY;
        const splitIndex = Math.ceil(parsedTerms.sections.length / 2);

        for (let index = 0; index < parsedTerms.sections.length; index += 1) {
          const section = parsedTerms.sections[index];
          const titleLines = wrapText(section.title, fontBold, preset.titleSize, columnWidth);
          const bodyLines = wrapText(section.body, fontRegular, preset.bodySize, columnWidth);
          const blockHeight = titleLines.length * preset.titleLineHeight + bodyLines.length * preset.bodyLineHeight + preset.gap;
          if ((index === splitIndex || cursor - blockHeight < bottomY) && columnIndex === 0) {
            columnIndex = 1;
            cursor = topY;
          }
          if (cursor - blockHeight < bottomY && columnIndex === 1) {
            return false;
          }

          if (shouldDraw) {
            const x = columnLefts[columnIndex];
            titleLines.forEach((lineText) => {
              drawText(page, lineText, x, cursor, { font: fontBold, size: preset.titleSize, color: ink });
              cursor -= preset.titleLineHeight;
            });
            bodyLines.forEach((lineText) => {
              drawText(page, lineText, x, cursor, { font: fontRegular, size: preset.bodySize, color: muted });
              cursor -= preset.bodyLineHeight;
            });
          } else {
            cursor -= blockHeight - preset.gap;
          }
          cursor -= preset.gap;
        }
        return true;
      };

      const preset = presets.find((item) => layoutTerms(item, false)) || presets[presets.length - 1];
      layoutTerms(preset, true);
    };

    drawFirstPageHeader();
    drawObject();
    drawTableHeader();
    drawTableRows();
    drawTotals();
    drawSignature();
    drawPaymentAndNotes();
    drawTermsPage();

    const pages = pdf.getPages();
    pages.forEach((targetPage, index) => {
      targetPage.drawRectangle({ x: 0, y: 0, width: pageWidth, height: 74, color: white });
      drawLine(targetPage, marginX, 66, pageRight, { thickness: 0.6, color: line });
      const legalLine = [
        company.legalName || 'ReFone',
        company.siret ? `SIRET ${company.siret}` : '',
        company.rib ? `RIB ${company.rib}` : '',
        company.email || '',
        company.phone || ''
      ].filter(Boolean).join(' - ');
      drawText(targetPage, legalLine, marginX, 45, { font: fontRegular, size: 7.8, color: muted });
      drawRightText(targetPage, `Page ${index + 1}/${pages.length}`, pageRight, 28, {
        font: fontRegular,
        size: 8,
        color: faint
      });
    });

    const bytes = await pdf.save();
    return new Blob([bytes], { type: 'application/pdf' });
  }

  function blobToDataUrl(blob) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve(String(reader.result || ""));
      reader.onerror = () => reject(new Error("Lecture du PDF impossible."));
      reader.readAsDataURL(blob);
    });
  }

  async function sendNativeDownload(blob, filename) {
    const payload = {
      type: "refone-download",
      filename,
      dataUrl: await blobToDataUrl(blob)
    };

    try {
      const webkitHandler = window.webkit?.messageHandlers?.refoneDownload;
      if (webkitHandler?.postMessage) {
        webkitHandler.postMessage(payload);
        return true;
      }
    } catch {}

    try {
      if (window.chrome?.webview?.postMessage) {
        window.chrome.webview.postMessage(payload);
        return true;
      }
    } catch {}

    return false;
  }

  async function downloadBlob(blob, filename) {
    if (await sendNativeDownload(blob, filename)) {
      return;
    }

    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = filename;
    document.body.appendChild(link);
    link.click();
    link.remove();
    window.setTimeout(() => URL.revokeObjectURL(url), 1200);
  }

  async function exportSelectedPdf({ open = false } = {}) {
    const documentItem = selectedDocument();
    if (!documentItem) return;
    try {
      showToast("Generation du PDF...");
      const blob = await buildPdfBlob(documentItem);
      const filename = documentExportFilename(documentItem);
      if (open) {
        const url = URL.createObjectURL(blob);
        window.open(url, "_blank", "noopener");
        window.setTimeout(() => URL.revokeObjectURL(url), 3000);
        showToast("PDF ouvert");
        return;
      }
      await downloadBlob(blob, filename);
      showToast("PDF telecharge");
    } catch (error) {
      showToast(error.message || "PDF impossible");
    }
  }

  function openMailProFromSelectedDocument() {
    const documentItem = selectedDocument();
    if (!documentItem) return;
    const url = buildMailProDraftUrl(documentItem);
    window.open(url, "_blank", "noopener");
    showToast("Brouillon MailPro ouvert");
  }

  function handleGlobalClick(event) {
    const target = event.target instanceof Element ? event.target : null;
    if (!target) return;

    const removeLineButton = target.closest("[data-remove-line]");
    if (removeLineButton) {
      removeLineFromSelectedDocument(removeLineButton.dataset.removeLine);
      return;
    }

    if (target.closest("[data-refresh-customer]")) {
      refreshDocumentFromLinkedCustomer();
      return;
    }

    if (target.closest("[data-create-customer-from-document]")) {
      createOrUpdateCustomerFromSelectedDocument();
      return;
    }

    if (target.closest("[data-duplicate-document]")) {
      duplicateSelectedDocument();
      return;
    }

    const convertButton = target.closest("[data-convert-document]");
    if (convertButton) {
      duplicateSelectedDocument(convertButton.dataset.convertDocument || "invoice");
      return;
    }

    if (target.closest("[data-delete-document]")) {
      deleteSelectedDocument();
      return;
    }

    if (target.closest("[data-export-pdf]")) {
      exportSelectedPdf();
      return;
    }

    if (target.closest("[data-open-pdf]")) {
      exportSelectedPdf({ open: true });
      return;
    }

    if (target.closest("[data-open-mail-pro]")) {
      openMailProFromSelectedDocument();
      return;
    }

    if (target.closest("[data-delete-customer]")) {
      deleteSelectedCustomer();
      return;
    }

    const deleteCatalogButton = target.closest("[data-delete-catalog]");
    if (deleteCatalogButton) {
      removeCatalogItem(deleteCatalogButton.dataset.deleteCatalog);
      return;
    }

    const viewButton = target.closest("[data-view]");
    if (viewButton) {
      setView(viewButton.dataset.view);
      return;
    }

    const filterButton = target.closest("[data-document-filter]");
    if (filterButton) {
      ui.documentFilter = filterButton.dataset.documentFilter || "all";
      render();
      return;
    }

    const selectDocumentButton = target.closest("[data-select-document]");
    if (selectDocumentButton) {
      setSelectedDocument(selectDocumentButton.dataset.selectDocument);
      return;
    }

    const selectCustomerButton = target.closest("[data-select-customer]");
    if (selectCustomerButton) {
      setSelectedCustomer(selectCustomerButton.dataset.selectCustomer);
      return;
    }

    const openDocumentButton = target.closest("[data-open-document]");
    if (openDocumentButton) {
      openDocumentModal(openDocumentButton.dataset.openDocument || "quote");
      return;
    }

    const openCustomerModalButton = target.closest("[data-open-customer-modal]");
    if (openCustomerModalButton) {
      openModal(elements.customerModal);
      return;
    }

    if (target.closest("[data-close-document-modal]")) {
      closeModal(elements.documentModal);
      return;
    }

    if (target.closest("[data-close-customer-modal]")) {
      closeModal(elements.customerModal);
      return;
    }

    if (target.closest("[data-open-sync]")) {
      openModal(elements.syncModal);
      return;
    }

    if (target.closest("[data-close-sync]")) {
      closeModal(elements.syncModal);
      return;
    }

    if (target.closest("[data-import-atelier]")) {
      importCustomersFromAtelier();
      return;
    }

    if (target.closest("[data-generate-sync]")) {
      generateSyncCode();
      return;
    }

    if (target.closest("[data-push-sync]")) {
      pushSync();
      return;
    }

    if (target.closest("[data-pull-sync]")) {
      pullSync({ force: true });
      return;
    }

    if (target.closest("[data-lock-session]")) {
      clearUnlockedSession();
      showAuthGate();
      return;
    }

    if (target.closest("[data-add-line]")) {
      addLineToSelectedDocument();
      return;
    }

    const fromCustomerButton = target.closest("[data-create-document-from-customer]");
    if (fromCustomerButton) {
      openDocumentModal(fromCustomerButton.dataset.createDocumentFromCustomer || "quote", state.selectedCustomerId);
      return;
    }

    const openDocFromCustomer = target.closest("[data-open-document-from-customer]");
    if (openDocFromCustomer) {
      setSelectedDocument(openDocFromCustomer.dataset.openDocumentFromCustomer);
      return;
    }

    const useCatalogButton = target.closest("[data-use-catalog]");
    if (useCatalogButton) {
      injectCatalogItem(useCatalogButton.dataset.useCatalog);
      return;
    }
  }

  function openDocumentModal(type = "quote", customerId = "") {
    const form = elements.documentCreateForm;
    if (!form) return;
    const normalizedType = normalizeDocumentType(type);
    const selectedCustomerId = customerId || state.selectedCustomerId || "";
    const issueDate = isoDateOnly();
    form.reset();
    form.elements.type.value = normalizedType;
    form.elements.customerId.value = selectedCustomerId;
    form.elements.issueDate.value = issueDate;
    form.elements.dueDate.value = defaultDueDateForType(normalizedType, issueDate);
    form.elements.title.value = documentDefaultTitle(normalizedType, customerSnapshotFromId(selectedCustomerId));
    fillDocumentCreateCustomerFields(selectedCustomerId);
    openModal(elements.documentModal);
  }

  function fillDocumentCreateCustomerFields(customerId) {
    const form = elements.documentCreateForm;
    if (!form) return;
    const snapshot = customerSnapshotFromId(customerId);
    if (form.elements.customerName) form.elements.customerName.value = snapshot.customerName;
    if (form.elements.customerEmail) form.elements.customerEmail.value = snapshot.customerEmail;
    if (form.elements.customerPhone) form.elements.customerPhone.value = snapshot.customerPhone;
    if (form.elements.customerAddress) form.elements.customerAddress.value = snapshot.customerAddress;
    if (form.elements.customerType) form.elements.customerType.value = normalizeCustomerType(snapshot.customerType);
  }

  function bootstrapSyncInput() {
    if (!elements.syncInput) return;
    elements.syncInput.value = lockedSyncCode();
    elements.syncInput.readOnly = true;
    elements.syncInput.setAttribute("aria-readonly", "true");
    storeSyncCode(DEFAULT_SYNC_CODE);
    elements.syncInput.addEventListener("focus", () => {
      elements.syncInput.select();
    });
    elements.syncInput.addEventListener("input", () => {
      storeSyncCode(DEFAULT_SYNC_CODE);
    });
  }

  async function handleAuthSubmit(event) {
    event.preventDefault();
    const username = String(elements.authUsername?.value || "").trim();
    const password = String(elements.authPassword?.value || "");
    const passwordHash = await sha256Hex(password);
    if (username !== AUTH_USERNAME || passwordHash !== AUTH_PASSWORD_HASH) {
      showAuthGate("Identifiant ou mot de passe incorrect.");
      return;
    }
    storeSessionUnlocked();
    hideAuthGate();
    await bootFactureOS();
  }

  function bindAuthForm() {
    if (authFormBound) return;
    authFormBound = true;
    elements.authForm?.addEventListener("submit", handleAuthSubmit);
  }

  function bindForms() {
    bindAuthForm();
    if (formsBound) return;
    formsBound = true;

    elements.documentCreateForm?.addEventListener("submit", (event) => {
      event.preventDefault();
      const values = formValues(event.currentTarget);
      if (!String(values.title || "").trim()) {
        showToast("Objet du document manquant");
        return;
      }
      closeModal(elements.documentModal);
      createDocument(values);
    });

    elements.documentCreateForm?.elements?.type?.addEventListener("change", (event) => {
      const form = elements.documentCreateForm;
      if (!form) return;
      const nextType = normalizeDocumentType(event.target.value);
      const issueDate = form.elements.issueDate.value || isoDateOnly();
      const snapshot = customerSnapshotFromId(form.elements.customerId.value || "");
      form.elements.title.value = rewriteDocumentTitleForType(form.elements.title.value, nextType, snapshot);
      form.elements.dueDate.value = defaultDueDateForType(nextType, issueDate);
    });

    elements.documentCreateForm?.elements?.customerId?.addEventListener("change", (event) => {
      const form = elements.documentCreateForm;
      if (!form) return;
      const customerId = event.target.value || "";
      const nextType = normalizeDocumentType(form.elements.type.value);
      fillDocumentCreateCustomerFields(customerId);
      if (customerId) {
        form.elements.title.value = documentDefaultTitle(nextType, customerSnapshotFromId(customerId));
      }
    });

    elements.documentCreateForm?.elements?.issueDate?.addEventListener("change", () => {
      const form = elements.documentCreateForm;
      if (!form) return;
      form.elements.dueDate.value = defaultDueDateForType(form.elements.type.value, form.elements.issueDate.value || isoDateOnly());
    });

    elements.customerCreateForm?.addEventListener("submit", (event) => {
      event.preventDefault();
      const values = formValues(event.currentTarget);
      if (!String(values.name || "").trim()) {
        showToast("Nom client manquant");
        return;
      }
      closeModal(elements.customerModal);
      createCustomer(values);
    });

    document.addEventListener("submit", (event) => {
      const target = event.target instanceof HTMLFormElement ? event.target : null;
      if (!target) return;

      if (target.matches("[data-document-form]")) {
        event.preventDefault();
        updateSelectedDocument(formValues(target));
        return;
      }

      if (target.matches("[data-customer-form]")) {
        event.preventDefault();
        updateSelectedCustomer(formValues(target));
        return;
      }

      if (target.matches("[data-settings-form]")) {
        event.preventDefault();
        updateCompany(formValues(target));
        return;
      }

      if (target.matches("[data-catalog-form]")) {
        event.preventDefault();
        addCatalogItem(formValues(target));
        target.reset();
        if (target.elements.marginRate) {
          target.elements.marginRate.value = String(state.company.defaultMarginRate);
        }
        updateCatalogFormCalculations(target);
        const vatInput = target.querySelector('[name="vatRate"]');
        if (vatInput) {
          vatInput.value = state.company.vatMode === "franchise" ? "0" : "20";
        }
      }
    });

    document.addEventListener("input", (event) => {
      const target = event.target instanceof HTMLInputElement ? event.target : null;
      if (!target) return;

      const catalogForm = target.closest("[data-catalog-form]");
      if (catalogForm && (target.matches("[data-catalog-calc]") || target.matches("[data-catalog-unit-ht]"))) {
        updateCatalogFormCalculations(catalogForm, target.name);
      }
    });

    document.addEventListener("change", (event) => {
      const target = event.target instanceof HTMLElement ? event.target : null;
      if (!target) return;

      const lineId = target.dataset?.lineField;
      if (lineId && target instanceof HTMLInputElement) {
        updateLine(lineId, target.name, target.value);
        return;
      }
    });

    document.addEventListener("click", handleGlobalClick);
    document.addEventListener("keydown", (event) => {
      if (event.key !== "Escape") return;
      if (!elements.syncModal?.hidden) {
        closeModal(elements.syncModal);
      }
      if (!elements.documentModal?.hidden) {
        closeModal(elements.documentModal);
      }
      if (!elements.customerModal?.hidden) {
        closeModal(elements.customerModal);
      }
    });

    elements.searchInput?.addEventListener("input", (event) => {
      ui.search = event.target.value || "";
      render();
    });

    window.addEventListener("online", () => {
      restartSyncTransport();
      if (readSyncCode().length >= MIN_SYNC_CODE_LENGTH) {
        pullSync({ silent: true, allowMissing: true, force: true });
      }
    });

    window.addEventListener("offline", () => {
      syncRuntime.streamConnected = false;
      updateSyncIndicator();
      setSyncStatus("Hors ligne. La synchro reprendra des le retour reseau.");
    });

    document.addEventListener("visibilitychange", () => {
      if (document.visibilityState !== "visible") return;
      if (readSyncCode().length >= MIN_SYNC_CODE_LENGTH) {
        restartSyncTransport();
        pullSync({ silent: true, allowMissing: true, force: true });
      }
    });
  }

  async function autoImportIfNeeded() {
    if (state.customers.length) return;
    const code = readSyncCode();
    if (code !== DEFAULT_SYNC_CODE) return;
    await importCustomersFromAtelier({ silent: true });
  }

  async function bootFactureOS() {
    if (appBooted) return;
    appBooted = true;
    bootstrapSyncInput();
    closeModal(elements.syncModal);
    closeModal(elements.documentModal);
    closeModal(elements.customerModal);
    bindForms();
    render();
    restartSyncTransport();
    window.addEventListener("beforeunload", () => {
      pushSync({ silent: true, keepalive: true });
    });
    await pullSync({ silent: true, allowMissing: true });
    await autoImportIfNeeded();
  }

  async function startFactureOS() {
    bindAuthForm();
    if (isPublicWebVersion() || isSessionUnlocked()) {
      storeSessionUnlocked();
      hideAuthGate();
      await bootFactureOS();
      return;
    }
    showAuthGate();
  }

  await startFactureOS();
})();
