{"id":3856,"date":"2026-01-15T11:06:38","date_gmt":"2026-01-15T11:06:38","guid":{"rendered":"https:\/\/jourx.fr\/?page_id=3856"},"modified":"2026-02-17T14:23:57","modified_gmt":"2026-02-17T13:23:57","slug":"home","status":"publish","type":"page","link":"https:\/\/jourx.fr\/en\/home\/","title":{"rendered":"JourX Home"},"content":{"rendered":"<div data-elementor-type=\"wp-page\" data-elementor-id=\"3856\" class=\"elementor elementor-3856\">\n\t\t\t\t<div class=\"elementor-element elementor-element-5bd79d6 e-flex e-con-boxed e-con e-parent\" data-id=\"5bd79d6\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-36315ca elementor-widget elementor-widget-html\" data-id=\"36315ca\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!-- JourX Home \u2014 WIDGET HTML (page \/home\/ ID 3856) \u2014 EDGE-TO-EDGE + LANG via BLOC #8 -->\r\n<!-- \u2705 Compatible widget HTML : 0 PHP \/ 0 add_action -->\r\n<!-- \u2705 Anti-scroll limit\u00e9 \u00e0 \/home\/ (ID 3856) via d\u00e9tection BODY class\/id + URL fallback -->\r\n<!-- \u2705 Meta viewport-fit=cover inject\u00e9 en JS (si absent) -->\r\n<!-- \u2705 100% I18N (FR\/EN) + compatible switch langue BLOC #8 (jx_lang_v1 + events) -->\r\n<!-- \u26a0\ufe0f Colle ce widget sur la page Home, mais le script re-v\u00e9rifie quand m\u00eame -->\r\n\r\n<div id=\"jx-home\"><\/div>\r\n\r\n<style>\r\n\/* ============================================================\r\n   JourX Home \u2014 THEME + LAYOUT\r\n   ============================================================ *\/\r\n#jx-home{\r\n  --bg:#0f1944;--card:#151f52;--card2:#101842;--ink:#eaf0ff;--muted:#b7c2e6;\r\n  --accent:#1fb6ff;--ok:#2ecc71;--warn:#f39c12;--danger:#e74c3c;--line:rgba(255,255,255,.12);\r\n\r\n  background:var(--bg);\r\n  color:var(--ink);\r\n  font:16px\/1.45 Inter,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;\r\n\r\n  \/* Safe areas iOS *\/\r\n  --sat: env(safe-area-inset-top);\r\n  --sab: env(safe-area-inset-bottom);\r\n}\r\n#jx-home *{box-sizing:border-box}\r\n#jx-home a{color:var(--accent);text-decoration:none}\r\n\r\n\/* \u2705 LOCK SCROLL (activ\u00e9 en JS uniquement sur \/home\/ ID 3856) *\/\r\nhtml.jxh-lock, body.jxh-lock{ height:100%; overflow:hidden !important; }\r\n\r\n\/* \u2705 Fullscreen app *\/\r\n#jx-home{\r\n  position:fixed;\r\n  inset:0;\r\n  height:100svh;\r\n  overflow:hidden;\r\n  display:flex;\r\n  flex-direction:column;\r\n  overscroll-behavior:none;\r\n  touch-action:pan-y;\r\n}\r\n\r\n\/* \u2705 Wrap edge-to-edge *\/\r\n#jx-home .wrap{\r\n  width:100%;\r\n  max-width:none;\r\n  margin:0;\r\n  padding:0;\r\n\r\n  height:100%;\r\n  min-height:0;               \/* \u2705 FIX IMPORTANT *\/\r\n  overflow:hidden;\r\n  display:flex;\r\n  flex-direction:column;\r\n  -webkit-overflow-scrolling:auto;\r\n}\r\n\r\n\/* \u2705 Header safe-top *\/\r\n#jx-home .jxh-head{\r\n  flex:0 0 auto;\r\n  padding-top: calc(14px + var(--sat));\r\n  padding-left:14px;\r\n  padding-right:14px;\r\n  padding-bottom:10px;\r\n}\r\n\r\n\/* \u2705 Scroll interne safe-bottom *\/\r\n#jx-home .jxh-scroll{\r\n  flex:1 1 auto;\r\n  min-height:0;               \/* \u2705 FIX IMPORTANT *\/\r\n  overflow:auto;\r\n  padding-left:14px;\r\n  padding-right:14px;\r\n  padding-top:0;\r\n  padding-bottom: calc(14px + var(--sab));\r\n  -webkit-overflow-scrolling:touch;\r\n  overscroll-behavior:contain;\r\n}\r\n\r\n\/* Scrollbar *\/\r\n#jx-home .jxh-scroll{\r\n  scrollbar-width:thin;\r\n  scrollbar-color: rgba(255,255,255,.22) rgba(255,255,255,.06);\r\n}\r\n#jx-home .jxh-scroll::-webkit-scrollbar{ width:10px; }\r\n#jx-home .jxh-scroll::-webkit-scrollbar-track{\r\n  background: rgba(255,255,255,.06);\r\n  border-radius: 999px;\r\n}\r\n#jx-home .jxh-scroll::-webkit-scrollbar-thumb{\r\n  background: rgba(255,255,255,.22);\r\n  border-radius: 999px;\r\n  border:2px solid rgba(255,255,255,.06);\r\n}\r\n#jx-home .jxh-scroll::-webkit-scrollbar-thumb:hover{\r\n  background: rgba(255,255,255,.30);\r\n}\r\n\r\n\/* \u2705 Supprimer \u201ccadre blanc WP\u201d MAIS scopp\u00e9 \u00e0 body.jxh-home-page (pos\u00e9 en JS) *\/\r\nbody.jxh-home-page{ background:#0f1944 !important; }\r\nbody.jxh-home-page #page,\r\nbody.jxh-home-page #content,\r\nbody.jxh-home-page #primary,\r\nbody.jxh-home-page #main{ background:transparent !important; }\r\nbody.jxh-home-page .site,\r\nbody.jxh-home-page .site-content,\r\nbody.jxh-home-page .content-area{ background:transparent !important; }\r\n\r\n\/* Components *\/\r\n#jx-home .card{\r\n  background:linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03));\r\n  border:1px solid var(--line);\r\n  border-radius:16px;\r\n  padding:14px;\r\n  box-shadow:0 10px 24px rgba(0,0,0,.18);\r\n}\r\n#jx-home .row{display:flex;gap:12px;flex-wrap:wrap}\r\n#jx-home .col{flex:1 1 260px;min-width:260px}\r\n#jx-home .h1{margin:0 0 8px;font-size:22px;line-height:1.2}\r\n#jx-home .muted{color:var(--muted)}\r\n#jx-home .pill{\r\n  display:inline-flex;align-items:center;gap:8px;\r\n  padding:6px 10px;border-radius:999px;border:1px solid var(--line);\r\n  background:rgba(255,255,255,.04);font-size:13px;color:var(--muted)\r\n}\r\n#jx-home .dot{width:10px;height:10px;border-radius:50%}\r\n#jx-home .dot.ok{background:var(--ok)}\r\n#jx-home .dot.warn{background:var(--warn)}\r\n#jx-home .dot.danger{background:var(--danger)}\r\n#jx-home .btn{\r\n  display:inline-flex;align-items:center;justify-content:center;gap:8px;\r\n  border:1px solid var(--line);\r\n  background:rgba(31,182,255,.18);\r\n  color:var(--ink);\r\n  padding:10px 12px;border-radius:12px;\r\n  font-weight:800;font-size:14px;cursor:pointer;\r\n  width:auto;\r\n}\r\n#jx-home .btn.secondary{background:rgba(255,255,255,.06)}\r\n#jx-home .btn.danger{background:rgba(231,76,60,.18)}\r\n#jx-home .btn.ok{background:rgba(46,204,113,.18)}\r\n#jx-home .btn:disabled{opacity:.55;cursor:not-allowed}\r\n#jx-home .divider{height:1px;background:var(--line);margin:10px 0}\r\n#jx-home .mini{font-size:13px;color:var(--muted)}\r\n#jx-home input, #jx-home select, #jx-home textarea{\r\n  width:100%;\r\n  background:rgba(255,255,255,.06);\r\n  border:1px solid var(--line);\r\n  color:var(--ink);\r\n  padding:10px 12px;border-radius:12px;\r\n  outline:none;\r\n}\r\n#jx-home .list{display:flex;flex-direction:column;gap:10px;margin-top:10px}\r\n#jx-home .item{\r\n  border:1px solid var(--line);\r\n  background:rgba(255,255,255,.04);\r\n  border-radius:14px;\r\n  padding:12px;\r\n}\r\n#jx-home .itemTop{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}\r\n#jx-home .itemTitle{font-weight:900}\r\n#jx-home .actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}\r\n\r\n\/* Header layout *\/\r\n#jx-home .jxh-headRow{\r\n  display:flex;\r\n  align-items:flex-start;\r\n  justify-content:space-between;\r\n  gap:12px;\r\n  margin-bottom:10px;\r\n}\r\n#jx-home .jxh-headLeft{min-width:260px;flex:1 1 auto}\r\n#jx-home .jxh-topActions{\r\n  flex:0 0 auto;\r\n  display:grid;\r\n  grid-auto-flow:column;\r\n  grid-auto-columns:1fr;\r\n  gap:10px;\r\n  align-items:stretch;\r\n  justify-items:stretch;\r\n}\r\n#jx-home .jxh-topActions .btn{width:100%}\r\n\r\n@media(max-width:760px){\r\n  #jx-home .jxh-headRow{flex-direction:column;align-items:stretch;}\r\n  #jx-home .jxh-headLeft{min-width:0}\r\n  #jx-home .jxh-topActions{\r\n    width:100%;\r\n    grid-auto-flow:row;\r\n    grid-template-columns: 1fr 1fr;\r\n  }\r\n  #jx-home .jxh-topActions .btn:nth-child(3){grid-column:1 \/ -1}\r\n}\r\n\r\n\/* Mobile full width *\/\r\n@media(max-width:760px){\r\n  #jx-home .row{flex-wrap:wrap}\r\n  #jx-home .col{flex:1 1 100%;min-width:0;}\r\n  #jx-home .card{width:100%}\r\n}\r\n\r\n\/* Nav top grid *\/\r\n#jx-home .navTop{\r\n  display:grid;\r\n  grid-template-columns: repeat(5, minmax(0, 1fr));\r\n  gap:8px;\r\n  padding:10px;\r\n  border-radius:16px;\r\n  border:1px solid var(--line);\r\n  background:rgba(0,0,0,.18);\r\n  position:sticky;top:0;z-index:10;\r\n  backdrop-filter:blur(10px);\r\n}\r\n#jx-home .tab{\r\n  width:100%;\r\n  padding:10px 6px;\r\n  border-radius:12px;\r\n  border:1px solid transparent;\r\n  background:rgba(255,255,255,.06);\r\n  color:var(--muted);\r\n  font-weight:900;\r\n  cursor:pointer;\r\n\r\n  display:flex;\r\n  flex-direction:column;\r\n  align-items:center;\r\n  justify-content:center;\r\n  text-align:center;\r\n  gap:4px;\r\n  min-height:56px;\r\n}\r\n#jx-home .tab .ico{font-size:18px;line-height:1;}\r\n#jx-home .tab .lbl{\r\n  font-size:11.5px;\r\n  line-height:1.05;\r\n  letter-spacing:-.01em;\r\n  white-space:nowrap;\r\n  overflow:hidden;\r\n  text-overflow:ellipsis;\r\n  max-width:100%;\r\n}\r\n#jx-home .tab.active{\r\n  background:rgba(31,182,255,.18);\r\n  border-color:rgba(31,182,255,.35);\r\n  color:var(--ink);\r\n}\r\n\r\n\/* Bottom nav mobile supprim\u00e9 *\/\r\n#jx-home .navBottom{ display:none !important; }\r\n\r\n\/* Coller vues en haut *\/\r\n#jx-home #jx-view{\r\n  display:block;\r\n  align-self:flex-start;\r\n  justify-self:flex-start;\r\n  margin-top:12px;\r\n  min-height:0;\r\n}\r\n<\/style>\r\n\r\n<script>\r\n(function(){\r\n  \"use strict\";\r\n\r\n  \/* ============================================================\r\n     00 \u2014 TARGET HOME ONLY (ID 3856) \u2014 widget-safe (pas de PHP)\r\n     ============================================================ *\/\r\n  const HOME_ID = 3856;\r\n\r\n  function isHome3856(){\r\n    try{\r\n      const b = document.body;\r\n      const cls = (b && b.className) ? (\" \" + b.className + \" \") : \" \";\r\n\r\n      \/\/ WP body_class usuels\r\n      if (cls.includes(\" page-id-\" + HOME_ID + \" \") || cls.includes(\" postid-\" + HOME_ID + \" \")) return true;\r\n\r\n      \/\/ Fallback URL \/home\r\n      const p = (location.pathname || \"\").replace(\/\\\/+$\/,\"\").toLowerCase();\r\n      if (p === \"\/home\") return true;\r\n\r\n      \/\/ \u2705 IMPORTANT : pas de \"return true\" par d\u00e9faut (sinon tu recasses ailleurs)\r\n      return false;\r\n    }catch(e){\r\n      return false;\r\n    }\r\n  }\r\n\r\n  \/\/ Hard safety : si pas Home => on n\u2019applique RIEN (pas de lock global)\r\n  if(!isHome3856()){\r\n    const root = document.getElementById(\"jx-home\");\r\n    if(root) root.innerHTML = \"\";\r\n    return;\r\n  }\r\n\r\n  \/* ============================================================\r\n     01 \u2014 META viewport-fit=cover (widget-safe)\r\n     ============================================================ *\/\r\n  (function ensureViewportFitCover(){\r\n    try{\r\n      const head = document.head || document.getElementsByTagName(\"head\")[0];\r\n      if(!head) return;\r\n      const metas = Array.from(head.querySelectorAll('meta[name=\"viewport\"]'));\r\n      const has = metas.some(m => (m.getAttribute(\"content\")||\"\").includes(\"viewport-fit=cover\"));\r\n      if(has) return;\r\n      const m = document.createElement(\"meta\");\r\n      m.setAttribute(\"name\",\"viewport\");\r\n      m.setAttribute(\"content\",\"width=device-width, initial-scale=1, viewport-fit=cover\");\r\n      head.appendChild(m);\r\n    }catch(e){}\r\n  })();\r\n\r\n  \/* ============================================================\r\n     00 \u2014 LANG (depuis BLOC #8) \u2014 100% i18n\r\n     ============================================================ *\/\r\n  const LANG_KEY = \"jx_lang_v1\";\r\n  function getLang(){\r\n    try{\r\n      const l = (localStorage.getItem(LANG_KEY) || \"\").toLowerCase();\r\n      return (l === \"en\" || l === \"fr\") ? l : \"fr\";\r\n    }catch(e){ return \"fr\"; }\r\n  }\r\n\r\n  const I18N = {\r\n    fr: {\r\n      \/\/ Head\r\n      title: \"Tableau de bord du foyer\",\r\n      subtitle: \"Synchro foyer \u2022 Courses \u2022 Repas\",\r\n      sync: \"Synchroniser\",\r\n      store: \"Mode magasin\",\r\n      back: \"\u21a9\ufe0e Retour JourX\",\r\n\r\n      \/\/ Nav\r\n      nav_dashboard: \"Dashboard\",\r\n      nav_courses: \"Courses\",\r\n      nav_meals: \"Repas\",\r\n      nav_foyer: \"Foyer\",\r\n      nav_help: \"Aide\",\r\n\r\n      \/\/ Install banner\r\n      install_one_tap_title: \"Acc\u00e8s 1 tap\",\r\n      install_one_tap_desc: \"Ajoute Home\u2122 \u00e0 l\u2019\u00e9cran d\u2019accueil pour l\u2019ouvrir comme une app.\",\r\n      install_btn: \"Installer\",\r\n      later_btn: \"Plus tard\",\r\n      ios_open_webapp: \"iPhone (ouvrir comme app web)\",\r\n      ios_cookie_tip: \"On sauvegarde maintenant le foyer en cookie pour que le raccourci retrouve toujours les donn\u00e9es.\",\r\n      got_it_btn: \"Compris\",\r\n\r\n      \/\/ Dashboard view\r\n      household_label: \"Foyer\",\r\n      members_label: \"Membres\",\r\n      last_sync_label: \"Derni\u00e8re synchro\",\r\n      essentials: \"Indispensables (\u00e0 faire)\",\r\n      openList: \"Ouvrir la liste\",\r\n      storeMode: \"Mode magasin\",\r\n      nothingPending: \"Rien en attente \u2705\",\r\n      dash_groceries_no_vault_title: \"Courses (sans Vault)\",\r\n      dash_groceries_no_vault_desc: \"Renseigne ton budget Courses et ajoute tes d\u00e9penses.\",\r\n      budget_groceries: \"Budget Courses\",\r\n      placeholder_budget: \"Ex : 400\",\r\n      save: \"Enregistrer\",\r\n      spent_fallback: \"D\u00e9pens\u00e9 (fallback)\",\r\n      remaining_fallback: \"Reste (fallback)\",\r\n      add_expense: \"Ajouter une d\u00e9pense\",\r\n      placeholder_store: \"Ex : Intermarch\u00e9\",\r\n      placeholder_amount: \"\u20ac\",\r\n      add: \"Ajouter\",\r\n      delete: \"Supprimer\",\r\n      noExpenses: \"Aucune d\u00e9pense enregistr\u00e9e.\",\r\n      expense_default: \"D\u00e9pense\",\r\n\r\n      \/\/ Courses view\r\n      groceries_title: \"Courses\",\r\n      groceries_store_suffix: \"\u2014 Mode magasin\",\r\n      groceries_store_desc: \"Uniquement le n\u00e9cessaire (non fait).\",\r\n      groceries_full_desc: \"Ajoute, attribue, coche.\",\r\n      full_view: \"Vue compl\u00e8te\",\r\n      store_view: \"Mode magasin\",\r\n      placeholder_item: \"Ex : Lait, Riz...\",\r\n      all_done: \"Tout est fait \u2705\",\r\n      no_items: \"Aucun article.\",\r\n      status_label: \"Statut\",\r\n      status_done: \"Fait\",\r\n      status_taken: \"Pris\",\r\n      status_requested: \"Demand\u00e9\",\r\n      status_open: \"\u00c0 faire\",\r\n      act_request: \"Demander\",\r\n      act_take: \"Je prends\",\r\n      act_done: \"Fait\",\r\n\r\n      \/\/ Meals view\r\n      meals_title: \"Planning des repas\",\r\n      meals_desc: \"Simple et actionnable.\",\r\n      day_label: \"Jour\",\r\n      meal_label: \"Repas\",\r\n      lunch: \"Midi\",\r\n      dinner: \"Soir\",\r\n      placeholder_dish: \"Plat (ex : Poulet\/riz)\",\r\n      responsible_label: \"Responsable\",\r\n      noMeals: \"Aucun repas planifi\u00e9.\",\r\n\r\n      \/\/ Days (UI select) + kept in FR for stored values, but UI uses i18n list\r\n      days: [\"Lundi\",\"Mardi\",\"Mercredi\",\"Jeudi\",\"Vendredi\",\"Samedi\",\"Dimanche\"],\r\n\r\n      \/\/ Foyer view\r\n      create_share_household: \"Cr\u00e9er \/ Partager le foyer\",\r\n      foyer_desc: \"Le foyer synchronise automatiquement les donn\u00e9es entre tous les membres.\",\r\n      household_id_prefix: \"Foyer :\",\r\n      share_link_full: \"Lien de partage (acc\u00e8s complet)\",\r\n      copy_link: \"Copier le lien\",\r\n      open_shortcut: \"Ouvrir le lien (puis Ajouter \u00e0 l\u2019\u00e9cran)\",\r\n      ios_shortcut_tip: \"iPhone \u201couvrir comme app web\u201d : ouvre ce lien une fois \u2192 Home\u2122 sauvegarde en cookie \u2192 le raccourci r\u00e9cup\u00e8re ensuite les donn\u00e9es.\",\r\n      notifications_title: \"Notifications (Web Push)\",\r\n      status_prefix: \"Statut :\",\r\n      perm_granted: \"\u2705 Autoris\u00e9es\",\r\n      perm_denied: \"\u26d4 Bloqu\u00e9es\",\r\n      perm_default: \"\u26a0\ufe0f Non activ\u00e9es\",\r\n      push_enable: \"Activer les notifications\",\r\n      push_activating: \"Activation en cours\u2026\",\r\n      push_enabled: \"\u2705 Notifications activ\u00e9es pour ce foyer.\",\r\n      browser_no_push: \"Ce navigateur ne supporte pas les notifications Web Push.\",\r\n      no_household_configured: \"Aucun foyer configur\u00e9.\",\r\n      create_my_household: \"Cr\u00e9er mon foyer\",\r\n      members_title: \"Membres\",\r\n      placeholder_name: \"Nom (ex : Christina)\",\r\n      placeholder_role: \"R\u00f4le (ex : Parent)\",\r\n      fast_access: \"Acc\u00e8s ultra rapide\",\r\n      already_added: \"\u2705 D\u00e9j\u00e0 ajout\u00e9 \u00e0 l\u2019\u00e9cran d\u2019accueil.\",\r\n      android_install_possible: \"Android : installation possible si le navigateur le permet.\",\r\n      install_home: \"Installer Home\u2122\",\r\n      ios_install_hint: \"iPhone : clique \u201cOuvrir le lien (puis Ajouter \u00e0 l\u2019\u00e9cran)\u201d dans l\u2019onglet Foyer.\",\r\n      ios_install_hint_2: \"Puis : Partager \u2192 Ajouter \u00e0 l\u2019\u00e9cran d\u2019accueil.\",\r\n      open_on_mobile: \"Ouvre sur mobile pour ajouter \u00e0 l\u2019\u00e9cran d\u2019accueil.\",\r\n\r\n      \/\/ Help view\r\n      help_title: \"Aide\",\r\n      help_line_1: \"\u2022 iOS \u201couvrir comme app web\u201d : Home\u2122 s\u2019appuie sur cookies (backup) pour retrouver le foyer.\",\r\n      help_line_2: \"\u2022 Budget Courses + d\u00e9penses fallback disponibles.\",\r\n      help_line_3: \"\u2022 Le foyer synchronise tout via la base (foyer_id + cl\u00e9).\",\r\n\r\n      \/\/ Errors\r\n      err_create_household: \"Erreur cr\u00e9ation foyer : \",\r\n\r\n      \/\/ Push generic errors (kept as-is but can be i18n if you want)\r\n      sw_unavailable: \"Service Worker indisponible\",\r\n      push_unsupported: \"Push non supporte sur ce navigateur\",\r\n      notif_unsupported: \"Notifications non supportees\",\r\n      foyer_not_configured: \"Foyer non configure\",\r\n      notif_denied: \"Permission notifications refusee\"\r\n    },\r\n\r\n    en: {\r\n      \/\/ Head\r\n      title: \"Household dashboard\",\r\n      subtitle: \"Sync \u2022 Groceries \u2022 Meals\",\r\n      sync: \"Sync\",\r\n      store: \"Store mode\",\r\n      back: \"\u21a9\ufe0e Back to JourX\",\r\n\r\n      \/\/ Nav\r\n      nav_dashboard: \"Dashboard\",\r\n      nav_courses: \"Groceries\",\r\n      nav_meals: \"Meals\",\r\n      nav_foyer: \"Household\",\r\n      nav_help: \"Help\",\r\n\r\n      \/\/ Install banner\r\n      install_one_tap_title: \"One-tap access\",\r\n      install_one_tap_desc: \"Add Home\u2122 to your home screen to open it like an app.\",\r\n      install_btn: \"Install\",\r\n      later_btn: \"Later\",\r\n      ios_open_webapp: \"iPhone (open as a web app)\",\r\n      ios_cookie_tip: \"We store the household in a cookie so the shortcut always finds your data.\",\r\n      got_it_btn: \"Got it\",\r\n\r\n      \/\/ Dashboard view\r\n      household_label: \"Household\",\r\n      members_label: \"Members\",\r\n      last_sync_label: \"Last sync\",\r\n      essentials: \"Essentials (to do)\",\r\n      openList: \"Open list\",\r\n      storeMode: \"Store mode\",\r\n      nothingPending: \"Nothing pending \u2705\",\r\n      dash_groceries_no_vault_title: \"Groceries (no Vault)\",\r\n      dash_groceries_no_vault_desc: \"Set your groceries budget and add expenses.\",\r\n      budget_groceries: \"Groceries budget\",\r\n      placeholder_budget: \"e.g. 400\",\r\n      save: \"Save\",\r\n      spent_fallback: \"Spent (fallback)\",\r\n      remaining_fallback: \"Remaining (fallback)\",\r\n      add_expense: \"Add an expense\",\r\n      placeholder_store: \"e.g. Walmart\",\r\n      placeholder_amount: \"\u20ac\",\r\n      add: \"Add\",\r\n      delete: \"Delete\",\r\n      noExpenses: \"No expenses yet.\",\r\n      expense_default: \"Expense\",\r\n\r\n      \/\/ Courses view\r\n      groceries_title: \"Groceries\",\r\n      groceries_store_suffix: \"\u2014 Store mode\",\r\n      groceries_store_desc: \"Only essentials (not done).\",\r\n      groceries_full_desc: \"Add, assign, check.\",\r\n      full_view: \"Full view\",\r\n      store_view: \"Store mode\",\r\n      placeholder_item: \"e.g. Milk, Rice...\",\r\n      all_done: \"All done \u2705\",\r\n      no_items: \"No items.\",\r\n      status_label: \"Status\",\r\n      status_done: \"Done\",\r\n      status_taken: \"Taken\",\r\n      status_requested: \"Requested\",\r\n      status_open: \"To do\",\r\n      act_request: \"Request\",\r\n      act_take: \"I take it\",\r\n      act_done: \"Done\",\r\n\r\n      \/\/ Meals view\r\n      meals_title: \"Meal planning\",\r\n      meals_desc: \"Simple and actionable.\",\r\n      day_label: \"Day\",\r\n      meal_label: \"Meal\",\r\n      lunch: \"Lunch\",\r\n      dinner: \"Dinner\",\r\n      placeholder_dish: \"Dish (e.g. Chicken\/rice)\",\r\n      responsible_label: \"Responsible\",\r\n      noMeals: \"No meals planned yet.\",\r\n\r\n      \/\/ Days\r\n      days: [\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\",\"Saturday\",\"Sunday\"],\r\n\r\n      \/\/ Foyer view\r\n      create_share_household: \"Create \/ Share household\",\r\n      foyer_desc: \"The household automatically syncs data between all members.\",\r\n      household_id_prefix: \"Household:\",\r\n      share_link_full: \"Share link (full access)\",\r\n      copy_link: \"Copy link\",\r\n      open_shortcut: \"Open link (then Add to Home Screen)\",\r\n      ios_shortcut_tip: \"On iPhone: open once \u2192 Home\u2122 stores the household in a cookie \u2192 the shortcut restores data.\",\r\n      notifications_title: \"Notifications (Web Push)\",\r\n      status_prefix: \"Status:\",\r\n      perm_granted: \"\u2705 Allowed\",\r\n      perm_denied: \"\u26d4 Blocked\",\r\n      perm_default: \"\u26a0\ufe0f Not enabled\",\r\n      push_enable: \"Enable notifications\",\r\n      push_activating: \"Enabling\u2026\",\r\n      push_enabled: \"\u2705 Notifications enabled for this household.\",\r\n      browser_no_push: \"This browser does not support Web Push notifications.\",\r\n      no_household_configured: \"No household configured.\",\r\n      create_my_household: \"Create my household\",\r\n      members_title: \"Members\",\r\n      placeholder_name: \"Name (e.g. Christina)\",\r\n      placeholder_role: \"Role (e.g. Parent)\",\r\n      fast_access: \"Ultra fast access\",\r\n      already_added: \"\u2705 Already added to Home Screen.\",\r\n      android_install_possible: \"Android: installation may be available if your browser supports it.\",\r\n      install_home: \"Install Home\u2122\",\r\n      ios_install_hint: \"iPhone: click \u201cOpen link (then Add to Home Screen)\u201d in the Household tab.\",\r\n      ios_install_hint_2: \"Then: Share \u2192 Add to Home Screen.\",\r\n      open_on_mobile: \"Open on mobile to add to Home Screen.\",\r\n\r\n      \/\/ Help view\r\n      help_title: \"Help\",\r\n      help_line_1: \"\u2022 iOS \u201copen as a web app\u201d: Home\u2122 uses cookies (backup) to restore your household.\",\r\n      help_line_2: \"\u2022 Groceries budget + fallback expenses are available.\",\r\n      help_line_3: \"\u2022 Household sync uses the database (foyer_id + key).\",\r\n\r\n      \/\/ Errors\r\n      err_create_household: \"Household creation error: \",\r\n\r\n      \/\/ Push generic errors (kept as-is but can be i18n if you want)\r\n      sw_unavailable: \"Service Worker unavailable\",\r\n      push_unsupported: \"Push not supported on this browser\",\r\n      notif_unsupported: \"Notifications not supported\",\r\n      foyer_not_configured: \"Household not configured\",\r\n      notif_denied: \"Notification permission denied\"\r\n    }\r\n  };\r\n\r\n  function t(key){\r\n    const l = getLang();\r\n    return (I18N[l] && I18N[l][key]) ? I18N[l][key] : (I18N.fr[key] || key);\r\n  }\r\n\r\n  \/* ============================================================\r\n     00 \u2014 EMBED MODE\r\n     ============================================================ *\/\r\n  const __JXH_URL = new URL(location.href);\r\n  const __JXH_EMBED_FLAG = (__JXH_URL.searchParams.get(\"embed\")===\"1\");\r\n  const __JXH_IS_IFRAME = (function(){\r\n    try{ return window.self !== window.top; }catch(e){ return true; }\r\n  })();\r\n  const isEmbedded = __JXH_IS_IFRAME || __JXH_EMBED_FLAG;\r\n\r\n  function postToParent(payload){\r\n    if(!isEmbedded) return;\r\n    try{\r\n      if (window.parent && window.parent !== window) window.parent.postMessage(payload, \"*\");\r\n    }catch(e){}\r\n  }\r\n\r\n  \/* ============================================================\r\n     01 \u2014 CONFIG\r\n     ============================================================ *\/\r\n  const CFG = {\r\n    appNameHTML: 'Home<sup>\u2122<\/sup>',\r\n    version: 'JXH-0.2.0',\r\n    ns: '\/wp-json\/jxhome\/v1',\r\n    vaultEnvelopeName: 'Courses',\r\n    defaultRoute: 'dashboard',\r\n    pollMs: 20000,\r\n    saveDebounceMs: 700,\r\n    cookieDays: 180\r\n  };\r\n\r\n  \/* ============================================================\r\n     05 \u2014 STATE\r\n     ============================================================ *\/\r\n  const state = {\r\n    foyerId: null,\r\n    foyerKey: null,\r\n    route: CFG.defaultRoute,\r\n    _submode: null,\r\n\r\n    members: [],\r\n    groceries: [],\r\n    meals: [],\r\n\r\n    fallback: { budget_courses: null, expenses: [] },\r\n\r\n    vault: { available: false, envelope: null },\r\n\r\n    ui: {\r\n      loading: true,\r\n      syncing: false,\r\n      lastSync: null,\r\n      error: null,\r\n      typing: false,\r\n      typingUntil: 0,\r\n      install: {\r\n        isIOS:false, isAndroid:false, isStandalone:false,\r\n        canPromptInstall:false, deferredPrompt:null, dismissed:false\r\n      }\r\n    },\r\n\r\n    _serverUpdatedAt: \"\",\r\n    _lang: getLang()\r\n  };\r\n\r\n  let __lastStateSig = \"\";\r\n  function stateSignature(){\r\n    return [\r\n      state._serverUpdatedAt || \"\",\r\n      state.members.length,\r\n      state.groceries.length,\r\n      state.meals.length,\r\n      state.fallback.expenses.length,\r\n      state.route,\r\n      state._submode || \"\",\r\n      state._lang || \"fr\"\r\n    ].join(\"|\");\r\n  }\r\n  function shouldBlockRender(){\r\n    if(state.ui.typing) return true;\r\n    if(state.ui.typingUntil && Date.now() < state.ui.typingUntil) return true;\r\n    return false;\r\n  }\r\n  function renderIfChanged(force=false){\r\n    if(!force && shouldBlockRender()) return;\r\n    const sig = stateSignature();\r\n    if(force || sig !== __lastStateSig){\r\n      __lastStateSig = sig;\r\n      render();\r\n    }\r\n  }\r\n\r\n  \/* ============================================================\r\n     06 \u2014 UTIL\r\n     ============================================================ *\/\r\n  const $ = (sel, root=document)=>root.querySelector(sel);\r\n  const uid = (p=\"id\")=> p+\"_\"+Math.random().toString(16).slice(2)+\"_\"+Date.now().toString(16);\r\n  const esc = (s)=> String(s??\"\").replaceAll(\"&\",\"&amp;\").replaceAll(\"<\",\"&lt;\").replaceAll(\">\",\"&gt;\").replaceAll('\"',\"&quot;\").replaceAll(\"'\",\"&#039;\");\r\n  const fmt = (n)=> (n===null||n===undefined||Number.isNaN(Number(n))) ? \"\u2014\" : Number(n).toLocaleString(state._lang===\"en\"?\"en-US\":\"fr-FR\",{minimumFractionDigits:2,maximumFractionDigits:2})+\" \u20ac\";\r\n  const clamp = (n,a,b)=>Math.max(a,Math.min(b,n));\r\n  const nowISO = ()=> new Date().toISOString();\r\n  const debounce = (fn, ms)=>{ let tt=null; return (...args)=>{ clearTimeout(tt); tt=setTimeout(()=>fn(...args), ms); }; };\r\n\r\n  function urlParams(){\r\n    const u=new URL(location.href);\r\n    return { foyer:(u.searchParams.get(\"foyer\")||\"\").trim(), k:(u.searchParams.get(\"k\")||\"\").trim() };\r\n  }\r\n\r\n  function setCookie(name, value, days){\r\n    try{\r\n      const d = new Date();\r\n      d.setTime(d.getTime() + (days*24*60*60*1000));\r\n      const expires = \"expires=\" + d.toUTCString();\r\n      const secure = (location.protocol === \"https:\") ? \";Secure\" : \"\";\r\n      document.cookie = name + \"=\" + encodeURIComponent(value) + \";\" + expires + \";Path=\/;SameSite=Lax\" + secure;\r\n    }catch(e){}\r\n  }\r\n  function getCookie(name){\r\n    try{\r\n      const n = name + \"=\";\r\n      const parts = (document.cookie || \"\").split(\";\");\r\n      for(let i=0;i<parts.length;i++){\r\n        let c = parts[i].trim();\r\n        if(c.indexOf(n)===0) return decodeURIComponent(c.substring(n.length));\r\n      }\r\n    }catch(e){}\r\n    return null;\r\n  }\r\n\r\n  const LS_LAST_FOYER = \"jxhome_last_foyer\";\r\n  const CK_LAST_FOYER = \"jxhome_last_foyer\";\r\n  function rememberLastFoyer(foyerId){\r\n    if(!foyerId) return;\r\n    localStorage.setItem(LS_LAST_FOYER, foyerId);\r\n    setCookie(CK_LAST_FOYER, foyerId, CFG.cookieDays);\r\n  }\r\n  function getLastFoyer(){\r\n    return (localStorage.getItem(LS_LAST_FOYER) || \"\").trim()\r\n      || (getCookie(CK_LAST_FOYER) || \"\").trim()\r\n      || null;\r\n  }\r\n  function setKeyForFoyer(foyerId, key){\r\n    if(!foyerId || !key) return;\r\n    localStorage.setItem(\"jxhome_foyer_key__\"+foyerId, key);\r\n    setCookie(\"jxhome_foyer_key__\"+foyerId, key, CFG.cookieDays);\r\n  }\r\n  function getKeyForFoyer(foyerId){\r\n    if(!foyerId) return null;\r\n    return localStorage.getItem(\"jxhome_foyer_key__\"+foyerId)\r\n      || getCookie(\"jxhome_foyer_key__\"+foyerId)\r\n      || null;\r\n  }\r\n\r\n  function cleanUrlKeepFoyer(){\r\n    const u=new URL(location.href);\r\n    u.searchParams.delete(\"k\");\r\n    history.replaceState({}, \"\", u.pathname + u.search);\r\n  }\r\n\r\n  function detectPlatform(){\r\n    const ua=navigator.userAgent||\"\";\r\n    const isIOS=\/iPhone|iPad|iPod\/i.test(ua);\r\n    const isAndroid=\/Android\/i.test(ua);\r\n    const isStandalone=(matchMedia && matchMedia('(display-mode: standalone)').matches) || (navigator.standalone===true);\r\n    state.ui.install.isIOS=isIOS;\r\n    state.ui.install.isAndroid=isAndroid;\r\n    state.ui.install.isStandalone=!!isStandalone;\r\n    state.ui.install.dismissed = localStorage.getItem(\"jxhome_install_dismissed\")===\"1\";\r\n  }\r\n\r\n  \/* ============================================================\r\n     \u2705 LANG LISTENERS (BLOC #8)\r\n     ============================================================ *\/\r\n  function refreshLang(force=false){\r\n    const l = getLang();\r\n    if(l !== state._lang){\r\n      state._lang = l;\r\n      renderIfChanged(true);\r\n    }else if(force){\r\n      renderIfChanged(true);\r\n    }\r\n  }\r\n  window.addEventListener(\"jx:lang:changed\", ()=> refreshLang(true));\r\n  window.addEventListener(\"storage\", (e)=>{ if(e && e.key === LANG_KEY) refreshLang(true); });\r\n\r\n  window.addEventListener(\"beforeinstallprompt\", (e)=>{\r\n    e.preventDefault();\r\n    state.ui.install.deferredPrompt = e;\r\n    state.ui.install.canPromptInstall = true;\r\n    renderInstallBanner();\r\n  });\r\n  window.addEventListener(\"appinstalled\", ()=>{\r\n    state.ui.install.isStandalone=true;\r\n    state.ui.install.canPromptInstall=false;\r\n    state.ui.install.deferredPrompt=null;\r\n    renderInstallBanner();\r\n  });\r\n\r\n  async function triggerAndroidInstall(){\r\n    const dp = state.ui.install.deferredPrompt;\r\n    if(!dp) return;\r\n    dp.prompt();\r\n    try{ await dp.userChoice; }catch(e){}\r\n    state.ui.install.deferredPrompt=null;\r\n    state.ui.install.canPromptInstall=false;\r\n    renderInstallBanner();\r\n  }\r\n  function dismissInstallBanner(){\r\n    localStorage.setItem(\"jxhome_install_dismissed\",\"1\");\r\n    state.ui.install.dismissed=true;\r\n    renderInstallBanner();\r\n  }\r\n\r\n  \/* ============================================================\r\n     06b \u2014 WEB PUSH\r\n     ============================================================ *\/\r\n  async function apiGetVapidPublicKey(){\r\n    const res = await fetch(CFG.ns + \"\/push\/public-key\", { method: \"GET\" });\r\n    const json = await res.json().catch(()=> ({}));\r\n    if(!json.ok || !json.publicKey) throw new Error(json.error || \"missing publicKey\");\r\n    return json.publicKey;\r\n  }\r\n  function urlBase64ToUint8Array(base64String){\r\n    const padding = '='.repeat((4 - base64String.length % 4) % 4);\r\n    const base64 = (base64String + padding).replace(\/-\/g, '+').replace(\/_\/g, '\/');\r\n    const raw = atob(base64);\r\n    const output = new Uint8Array(raw.length);\r\n    for(let i=0;i<raw.length;i++) output[i] = raw.charCodeAt(i);\r\n    return output;\r\n  }\r\n  async function ensureServiceWorker(){\r\n    if(!(\"serviceWorker\" in navigator)) throw new Error(t(\"sw_unavailable\"));\r\n    const reg = await navigator.serviceWorker.register(\"\/jxhome-sw.js\", { scope: \"\/\" });\r\n    return reg;\r\n  }\r\n  async function enablePush(){\r\n    if(!(\"PushManager\" in window)) throw new Error(t(\"push_unsupported\"));\r\n    if(!(\"Notification\" in window)) throw new Error(t(\"notif_unsupported\"));\r\n    if(!state.foyerId || !state.foyerKey) throw new Error(t(\"foyer_not_configured\"));\r\n\r\n    const reg = await ensureServiceWorker();\r\n\r\n    const perm = await Notification.requestPermission();\r\n    if(perm !== \"granted\") throw new Error(t(\"notif_denied\"));\r\n\r\n    const publicKey = await apiGetVapidPublicKey();\r\n\r\n    let sub = await reg.pushManager.getSubscription();\r\n    if(!sub){\r\n      sub = await reg.pushManager.subscribe({\r\n        userVisibleOnly: true,\r\n        applicationServerKey: urlBase64ToUint8Array(publicKey)\r\n      });\r\n    }\r\n\r\n    const resp = await fetch(CFG.ns + \"\/push\/subscribe\", {\r\n      method: \"POST\",\r\n      headers: { \"Content-Type\":\"application\/json\" },\r\n      body: JSON.stringify({ foyer_id: state.foyerId, k: state.foyerKey, subscription: sub })\r\n    });\r\n\r\n    const json = await resp.json().catch(()=> ({}));\r\n    if(!json.ok) throw new Error(json.error || \"subscribe failed\");\r\n    return true;\r\n  }\r\n\r\n  \/* ============================================================\r\n     07 \u2014 API\r\n     ============================================================ *\/\r\n  async function apiCreateFoyer(){\r\n    const res = await fetch(CFG.ns+\"\/foyer\/create\", { method:\"POST\" });\r\n    const json = await res.json();\r\n    if(!json.ok) throw new Error(json.error||\"create failed\");\r\n    return json;\r\n  }\r\n  async function apiGetFoyer(foyerId, key){\r\n    const url = new URL(location.origin + CFG.ns + \"\/foyer\/\" + encodeURIComponent(foyerId));\r\n    url.searchParams.set(\"k\", key);\r\n    const res = await fetch(url.toString(), { method:\"GET\" });\r\n    const json = await res.json();\r\n    if(!json.ok) throw new Error(json.error||\"get failed\");\r\n    return json;\r\n  }\r\n  async function apiSaveFoyer(foyerId, key, payloadState){\r\n    const res = await fetch(CFG.ns + \"\/foyer\/\" + encodeURIComponent(foyerId) + \"\/save\", {\r\n      method:\"POST\",\r\n      headers:{ \"Content-Type\":\"application\/json\" },\r\n      body: JSON.stringify({ k: key, state: payloadState })\r\n    });\r\n    const json = await res.json();\r\n    if(!json.ok) throw new Error(json.error||\"save failed\");\r\n    return json;\r\n  }\r\n\r\n  \/* ============================================================\r\n     08 \u2014 MOUNT\r\n     ============================================================ *\/\r\n  function mount(){\r\n    const root = $(\"#jx-home\");\r\n\r\n    const embedBackBtnHTML = isEmbedded ? `\r\n      <button class=\"btn secondary jxh-embedBack\" id=\"jx-embed-back\" title=\"${esc(t(\"back\"))}\">${esc(t(\"back\"))}<\/button>\r\n    ` : \"\";\r\n\r\n    root.innerHTML = `\r\n      <div class=\"wrap\">\r\n        <div class=\"jxh-head\">\r\n          <div class=\"jxh-headRow\">\r\n            <div class=\"jxh-headLeft\">\r\n              <div class=\"pill\">\r\n                <span class=\"dot ok\"><\/span>\r\n                <strong>${CFG.appNameHTML}<\/strong>\r\n                <span class=\"muted\">\u2022 ${CFG.version}<\/span>\r\n              <\/div>\r\n              <div class=\"h1\" style=\"margin-top:10px\">${esc(t(\"title\"))}<\/div>\r\n              <div class=\"mini\" id=\"jx-subtitle\">${esc(t(\"subtitle\"))}<\/div>\r\n            <\/div>\r\n\r\n            <div class=\"jxh-topActions\">\r\n              ${embedBackBtnHTML}\r\n              <button class=\"btn secondary\" id=\"jx-sync\">${esc(t(\"sync\"))}<\/button>\r\n              <button class=\"btn\" id=\"jx-store\">${esc(t(\"store\"))}<\/button>\r\n            <\/div>\r\n          <\/div>\r\n\r\n          <div id=\"jx-install\"><\/div>\r\n          <div class=\"navTop\" id=\"jx-nav-top\"><\/div>\r\n        <\/div>\r\n\r\n        <div class=\"jxh-scroll\" id=\"jx-scroll\">\r\n          <div id=\"jx-view\"><\/div>\r\n        <\/div>\r\n      <\/div>\r\n    `;\r\n\r\n    $(\"#jx-sync\").addEventListener(\"click\", ()=>syncFromServer(true));\r\n    $(\"#jx-store\").addEventListener(\"click\", ()=>{\r\n      state.route=\"courses\";\r\n      state._submode=\"store\";\r\n      renderIfChanged(true);\r\n    });\r\n\r\n    if(isEmbedded){\r\n      const back = $(\"#jx-embed-back\");\r\n      if(back) back.addEventListener(\"click\", ()=>{ postToParent({ type:\"jxhome:close\" }); });\r\n    }\r\n\r\n    root.addEventListener(\"click\", onClick);\r\n\r\n    root.addEventListener(\"focusin\", (e)=>{\r\n      const el = e.target;\r\n      if(!el) return;\r\n      const tag = (el.tagName||\"\").toUpperCase();\r\n      if(tag===\"INPUT\" || tag===\"TEXTAREA\" || tag===\"SELECT\"){\r\n        state.ui.typing = true;\r\n        state.ui.typingUntil = 0;\r\n      }\r\n    }, true);\r\n\r\n    root.addEventListener(\"focusout\", (e)=>{\r\n      const el = e.target;\r\n      if(!el) return;\r\n      const tag = (el.tagName||\"\").toUpperCase();\r\n      if(tag===\"INPUT\" || tag===\"TEXTAREA\" || tag===\"SELECT\"){\r\n        state.ui.typing = false;\r\n        state.ui.typingUntil = Date.now() + 700;\r\n      }\r\n    }, true);\r\n\r\n    root.addEventListener(\"input\", (e)=>{\r\n      const el = e.target;\r\n      if(!el) return;\r\n      const tag = (el.tagName||\"\").toUpperCase();\r\n      if(tag===\"INPUT\" || tag===\"TEXTAREA\" || tag===\"SELECT\"){\r\n        state.ui.typing = true;\r\n        state.ui.typingUntil = Date.now() + 1200;\r\n      }\r\n    }, true);\r\n  }\r\n\r\n  \/* ============================================================\r\n     09 \u2014 NAV\r\n     ============================================================ *\/\r\n  function routes(){\r\n    return [\r\n      {id:\"dashboard\", ico:\"\ud83c\udfe0\", lblKey:\"nav_dashboard\"},\r\n      {id:\"courses\",   ico:\"\ud83d\uded2\", lblKey:\"nav_courses\"},\r\n      {id:\"meals\",     ico:\"\ud83c\udf7d\", lblKey:\"nav_meals\"},\r\n      {id:\"foyer\",     ico:\"\ud83d\udc65\", lblKey:\"nav_foyer\"},\r\n      {id:\"help\",      ico:\"\u2753\", lblKey:\"nav_help\"}\r\n    ];\r\n  }\r\n  function renderNav(){\r\n    const mk = (activeId)=> routes().map(r=>`\r\n      <button class=\"tab ${activeId===r.id?\"active\":\"\"}\" data-route=\"${r.id}\">\r\n        <span class=\"ico\">${r.ico}<\/span>\r\n        <span class=\"lbl\">${esc(t(r.lblKey))}<\/span>\r\n      <\/button>\r\n    `).join(\"\");\r\n    const top = $(\"#jx-nav-top\");\r\n    if(top) top.innerHTML = mk(state.route);\r\n  }\r\n\r\n  \/* ============================================================\r\n     10 \u2014 INSTALL BANNER (100% i18n)\r\n     ============================================================ *\/\r\n  function renderInstallBanner(){\r\n    const host = $(\"#jx-install\");\r\n    if(!host) return;\r\n\r\n    if(state.ui.install.isStandalone || state.ui.install.dismissed){\r\n      host.innerHTML = \"\";\r\n      return;\r\n    }\r\n\r\n    if(state.ui.install.isAndroid){\r\n      host.innerHTML = `\r\n        <div class=\"card\" style=\"margin-bottom:12px;background:rgba(0,0,0,.18)\">\r\n          <div class=\"row\" style=\"align-items:center;justify-content:space-between\">\r\n            <div class=\"col\" style=\"min-width:240px\">\r\n              <strong>${esc(t(\"install_one_tap_title\"))}<\/strong>\r\n              <div class=\"mini\">${esc(t(\"install_one_tap_desc\"))}<\/div>\r\n            <\/div>\r\n            <div style=\"display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end\">\r\n              <button class=\"btn ${state.ui.install.canPromptInstall?\"\":\"secondary\"}\" data-install-android ${state.ui.install.canPromptInstall?\"\":\"disabled\"}>${esc(t(\"install_btn\"))}<\/button>\r\n              <button class=\"btn secondary\" data-install-dismiss>${esc(t(\"later_btn\"))}<\/button>\r\n            <\/div>\r\n          <\/div>\r\n        <\/div>\r\n      `;\r\n      return;\r\n    }\r\n\r\n    if(state.ui.install.isIOS){\r\n      host.innerHTML = `\r\n        <div class=\"card\" style=\"margin-bottom:12px;background:rgba(0,0,0,.18)\">\r\n          <strong>${esc(t(\"ios_open_webapp\"))}<\/strong>\r\n          <div class=\"mini\" style=\"margin-top:6px\">\r\n            \u2705 ${esc(t(\"ios_cookie_tip\"))}\r\n          <\/div>\r\n          <div class=\"divider\"><\/div>\r\n          <button class=\"btn secondary\" data-install-dismiss>${esc(t(\"got_it_btn\"))}<\/button>\r\n        <\/div>\r\n      `;\r\n      return;\r\n    }\r\n\r\n    host.innerHTML = \"\";\r\n  }\r\n\r\n  \/* ============================================================\r\n     11 \u2014 VIEWS (100% i18n)\r\n     ============================================================ *\/\r\n  function viewDashboard(){\r\n    return `\r\n      <div class=\"row\">\r\n        <div class=\"col\">\r\n          <div class=\"card\">\r\n            <div class=\"pill\"><span class=\"dot ok\"><\/span><strong>${esc(t(\"household_label\"))}<\/strong> <span class=\"muted\">\u2022 ${esc(state.foyerId||\"\u2014\")}<\/span><\/div>\r\n            <div class=\"divider\"><\/div>\r\n            <div class=\"mini\">\r\n              ${esc(t(\"members_label\"))} : ${state.members.map(m=>esc(m.name)).join(\" \u2022 \") || \"\u2014\"}<br>\r\n              ${esc(t(\"last_sync_label\"))} : ${state.ui.lastSync ? esc(state.ui.lastSync) : \"\u2014\"}\r\n            <\/div>\r\n          <\/div>\r\n\r\n          <div class=\"card\" style=\"margin-top:12px\">\r\n            <strong>${esc(t(\"essentials\"))}<\/strong>\r\n            <div class=\"divider\"><\/div>\r\n            <div class=\"list\">\r\n              ${state.groceries.filter(x=>x.status!==\"done\").slice(0,4).map(itemRow).join(\"\") || `<div class=\"mini\">${esc(t(\"nothingPending\"))}<\/div>`}\r\n            <\/div>\r\n            <div class=\"divider\"><\/div>\r\n            <button class=\"btn secondary\" data-route=\"courses\">${esc(t(\"openList\"))}<\/button>\r\n            <button class=\"btn\" data-route=\"courses\" data-sub=\"store\">${esc(t(\"storeMode\"))}<\/button>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div class=\"col\">\r\n          ${fallbackCard()}\r\n        <\/div>\r\n      <\/div>\r\n    `;\r\n  }\r\n\r\n  function fallbackCard(){\r\n    const b = state.fallback.budget_courses;\r\n    const spent = state.fallback.expenses.reduce((a,x)=>a+Number(x.amount||0),0);\r\n    const remaining = (b===null||b===undefined) ? null : (Number(b)-spent);\r\n\r\n    return `\r\n      <div class=\"card\">\r\n        <strong>${esc(t(\"dash_groceries_no_vault_title\"))}<\/strong>\r\n        <div class=\"mini\" style=\"margin-top:6px\">\r\n          ${esc(t(\"dash_groceries_no_vault_desc\"))}\r\n        <\/div>\r\n        <div class=\"divider\"><\/div>\r\n\r\n        <div class=\"row\">\r\n          <div class=\"col\">\r\n            <div class=\"mini\">${esc(t(\"budget_groceries\"))}<\/div>\r\n            <input type=\"number\" min=\"0\" step=\"0.01\" id=\"fb-budget\" value=\"${b!==null&&b!==undefined?esc(b):\"\"}\" placeholder=\"${esc(t(\"placeholder_budget\"))}\" \/>\r\n          <\/div>\r\n          <div style=\"align-self:end\">\r\n            <button class=\"btn\" data-fb-savebudget>${esc(t(\"save\"))}<\/button>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div class=\"divider\"><\/div>\r\n        <div class=\"row\">\r\n          <div class=\"col card\" style=\"background:rgba(0,0,0,.18)\">\r\n            <div class=\"mini\">${esc(t(\"spent_fallback\"))}<\/div>\r\n            <div style=\"font-weight:900;font-size:20px\">${fmt(spent)}<\/div>\r\n          <\/div>\r\n          <div class=\"col card\" style=\"background:rgba(0,0,0,.18)\">\r\n            <div class=\"mini\">${esc(t(\"remaining_fallback\"))}<\/div>\r\n            <div style=\"font-weight:900;font-size:20px\">${fmt(remaining)}<\/div>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div class=\"divider\"><\/div>\r\n        <strong>${esc(t(\"add_expense\"))}<\/strong>\r\n        <div class=\"row\" style=\"margin-top:8px\">\r\n          <div class=\"col\"><input id=\"fb-label\" placeholder=\"${esc(t(\"placeholder_store\"))}\" \/><\/div>\r\n          <div style=\"width:160px\"><input id=\"fb-amount\" type=\"number\" min=\"0\" step=\"0.01\" placeholder=\"${esc(t(\"placeholder_amount\"))}\" \/><\/div>\r\n          <div style=\"align-self:end\"><button class=\"btn ok\" data-fb-addexpense>${esc(t(\"add\"))}<\/button><\/div>\r\n        <\/div>\r\n\r\n        <div class=\"list\">\r\n          ${state.fallback.expenses.slice(0,6).map(x=>`\r\n            <div class=\"item\">\r\n              <div class=\"itemTop\">\r\n                <div>\r\n                  <div class=\"itemTitle\">${esc(x.label||t(\"expense_default\"))}<\/div>\r\n                  <div class=\"mini\">${esc(x.at||\"\")}<\/div>\r\n                <\/div>\r\n                <div style=\"text-align:right\">\r\n                  <div style=\"font-weight:900\">${fmt(x.amount)}<\/div>\r\n                  <button class=\"btn danger\" data-fb-delexpense=\"${esc(x.id)}\">${esc(t(\"delete\"))}<\/button>\r\n                <\/div>\r\n              <\/div>\r\n            <\/div>\r\n          `).join(\"\") || `<div class=\"mini\">${esc(t(\"noExpenses\"))}<\/div>`}\r\n        <\/div>\r\n      <\/div>\r\n    `;\r\n  }\r\n\r\n  function viewCourses(){\r\n    const isStore = state._submode===\"store\";\r\n    const items = isStore ? state.groceries.filter(x=>x.status!==\"done\") : state.groceries;\r\n\r\n    return `\r\n      <div class=\"card\">\r\n        <div class=\"row\" style=\"align-items:center;justify-content:space-between\">\r\n          <div>\r\n            <strong>${esc(t(\"groceries_title\"))} ${isStore ? esc(t(\"groceries_store_suffix\")) : \"\"}<\/strong>\r\n            <div class=\"mini\">${esc(isStore ? t(\"groceries_store_desc\") : t(\"groceries_full_desc\"))}<\/div>\r\n          <\/div>\r\n          <div style=\"display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end\">\r\n            <button class=\"btn secondary\" data-store=\"0\">${esc(t(\"full_view\"))}<\/button>\r\n            <button class=\"btn\" data-store=\"1\">${esc(t(\"store_view\"))}<\/button>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        ${isStore ? \"\" : `\r\n          <div class=\"divider\"><\/div>\r\n          <div class=\"row\">\r\n            <div class=\"col\"><input id=\"g-name\" placeholder=\"${esc(t(\"placeholder_item\"))}\" \/><\/div>\r\n            <div style=\"width:130px\"><input id=\"g-qty\" type=\"number\" min=\"1\" value=\"1\" \/><\/div>\r\n            <div style=\"align-self:end\"><button class=\"btn\" data-g-add>${esc(t(\"add\"))}<\/button><\/div>\r\n          <\/div>\r\n        `}\r\n\r\n        <div class=\"divider\"><\/div>\r\n        <div class=\"list\">\r\n          ${items.map(itemRow).join(\"\") || `<div class=\"mini\">${esc(isStore ? t(\"all_done\") : t(\"no_items\"))}<\/div>`}\r\n        <\/div>\r\n      <\/div>\r\n    `;\r\n  }\r\n\r\n  function itemRow(it){\r\n    return `\r\n      <div class=\"item\" data-item=\"${esc(it.id)}\">\r\n        <div class=\"itemTop\">\r\n          <div>\r\n            <div class=\"itemTitle\">${esc(it.name)} <span class=\"mini\">\u00d7${esc(it.qty)}<\/span><\/div>\r\n            <div class=\"mini\">${esc(t(\"status_label\"))} : ${esc(labelStatus(it.status))}<\/div>\r\n          <\/div>\r\n          <div style=\"display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end\">\r\n            <button class=\"btn secondary\" data-g-act=\"request\" data-id=\"${esc(it.id)}\">${esc(t(\"act_request\"))}<\/button>\r\n            <button class=\"btn\" data-g-act=\"take\" data-id=\"${esc(it.id)}\">${esc(t(\"act_take\"))}<\/button>\r\n            <button class=\"btn ok\" data-g-act=\"done\" data-id=\"${esc(it.id)}\">${esc(t(\"act_done\"))}<\/button>\r\n            <button class=\"btn danger\" data-g-act=\"del\" data-id=\"${esc(it.id)}\">${esc(t(\"delete\"))}<\/button>\r\n          <\/div>\r\n        <\/div>\r\n      <\/div>\r\n    `;\r\n  }\r\n\r\n  function labelStatus(s){\r\n    if(s===\"done\") return t(\"status_done\");\r\n    if(s===\"taken\") return t(\"status_taken\");\r\n    if(s===\"requested\") return t(\"status_requested\");\r\n    return t(\"status_open\");\r\n  }\r\n\r\n  function viewMeals(){\r\n    const days = (I18N[state._lang] && Array.isArray(I18N[state._lang].days)) ? I18N[state._lang].days : I18N.fr.days;\r\n\r\n    return `\r\n      <div class=\"card\">\r\n        <strong>${esc(t(\"meals_title\"))}<\/strong>\r\n        <div class=\"mini\" style=\"margin-top:6px\">${esc(t(\"meals_desc\"))}<\/div>\r\n        <div class=\"divider\"><\/div>\r\n\r\n        <div class=\"row\">\r\n          <div class=\"col\">\r\n            <div class=\"mini\">${esc(t(\"day_label\"))}<\/div>\r\n            <select id=\"m-day\">${days.map(d=>`<option>${esc(d)}<\/option>`).join(\"\")}<\/select>\r\n          <\/div>\r\n          <div class=\"col\">\r\n            <div class=\"mini\">${esc(t(\"meal_label\"))}<\/div>\r\n            <select id=\"m-meal\"><option>${esc(t(\"lunch\"))}<\/option><option>${esc(t(\"dinner\"))}<\/option><\/select>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div class=\"divider\"><\/div>\r\n        <div class=\"row\">\r\n          <div class=\"col\"><input id=\"m-title\" placeholder=\"${esc(t(\"placeholder_dish\"))}\" \/><\/div>\r\n          <div style=\"width:220px\">\r\n            <select id=\"m-resp\">${state.members.map(m=>`<option value=\"${esc(m.id)}\">${esc(m.name)}<\/option>`).join(\"\")}<\/select>\r\n          <\/div>\r\n          <div style=\"align-self:end\"><button class=\"btn\" data-m-add>${esc(t(\"add\"))}<\/button><\/div>\r\n        <\/div>\r\n\r\n        <div class=\"divider\"><\/div>\r\n        <div class=\"list\">\r\n          ${state.meals.slice(0,14).map(x=>`\r\n            <div class=\"item\">\r\n              <div class=\"itemTop\">\r\n                <div>\r\n                  <div class=\"itemTitle\">${esc(x.day)} \u2022 ${esc(x.meal)} \u2014 ${esc(x.title)}<\/div>\r\n                  <div class=\"mini\">${esc(t(\"responsible_label\"))} : ${esc(memberName(x.responsibleId))}<\/div>\r\n                <\/div>\r\n                <button class=\"btn danger\" data-m-del=\"${esc(x.id)}\">${esc(t(\"delete\"))}<\/button>\r\n              <\/div>\r\n            <\/div>\r\n          `).join(\"\") || `<div class=\"mini\">${esc(t(\"noMeals\"))}<\/div>`}\r\n        <\/div>\r\n      <\/div>\r\n    `;\r\n  }\r\n\r\n  function memberName(id){\r\n    const m=state.members.find(x=>x.id===id);\r\n    return m?m.name:\"\u2014\";\r\n  }\r\n\r\n  function viewFoyer(){\r\n    const shareLink = state.foyerId && state.foyerKey\r\n      ? `${location.origin}${location.pathname}?foyer=${encodeURIComponent(state.foyerId)}&k=${encodeURIComponent(state.foyerKey)}`\r\n      : \"\";\r\n\r\n    const perm = (\"Notification\" in window) ? Notification.permission : \"unsupported\";\r\n    const permLabel =\r\n      perm === \"granted\" ? t(\"perm_granted\") :\r\n      perm === \"denied\"  ? t(\"perm_denied\") :\r\n      perm === \"default\" ? t(\"perm_default\") :\r\n      \"\u2014\";\r\n\r\n    const canPush = (\"serviceWorker\" in navigator) && (\"PushManager\" in window) && (\"Notification\" in window);\r\n\r\n    return `\r\n      <div class=\"row\">\r\n        <div class=\"col\">\r\n          <div class=\"card\">\r\n            <strong>${esc(t(\"create_share_household\"))}<\/strong>\r\n            <div class=\"mini\" style=\"margin-top:6px\">\r\n              ${esc(t(\"foyer_desc\"))}\r\n            <\/div>\r\n            <div class=\"divider\"><\/div>\r\n\r\n            ${state.foyerId ? `\r\n              <div class=\"pill\"><span class=\"dot ok\"><\/span><strong>${esc(t(\"household_id_prefix\"))}<\/strong> ${esc(state.foyerId)}<\/div>\r\n\r\n              <div class=\"divider\"><\/div>\r\n              <div class=\"mini\"><strong>${esc(t(\"share_link_full\"))}<\/strong><\/div>\r\n              <input value=\"${esc(shareLink)}\" readonly \/>\r\n              <div class=\"divider\"><\/div>\r\n\r\n              <div style=\"display:flex;gap:10px;flex-wrap:wrap\">\r\n                <button class=\"btn secondary\" data-copy=\"${esc(shareLink)}\">${esc(t(\"copy_link\"))}<\/button>\r\n                <button class=\"btn\" data-open-shortcut>${esc(t(\"open_shortcut\"))}<\/button>\r\n              <\/div>\r\n\r\n              <div class=\"mini\" style=\"margin-top:8px\">\r\n                ${esc(t(\"ios_shortcut_tip\"))}\r\n              <\/div>\r\n\r\n              <div class=\"divider\"><\/div>\r\n              <strong>${esc(t(\"notifications_title\"))}<\/strong>\r\n              <div class=\"mini\" style=\"margin-top:6px\">\r\n                ${esc(t(\"status_prefix\"))} <span class=\"pill\">${esc(permLabel)}<\/span>\r\n              <\/div>\r\n\r\n              ${canPush ? `\r\n                <div class=\"divider\"><\/div>\r\n                <div style=\"display:flex;gap:10px;flex-wrap:wrap\">\r\n                  <button class=\"btn ok\" data-push-enable>${esc(t(\"push_enable\"))}<\/button>\r\n                <\/div>\r\n                <div class=\"mini\" id=\"jx-push-msg\" style=\"margin-top:8px\"><\/div>\r\n              ` : `\r\n                <div class=\"mini\" style=\"margin-top:8px\">\r\n                  ${esc(t(\"browser_no_push\"))}\r\n                <\/div>\r\n              `}\r\n            ` : `\r\n              <div class=\"mini\">${esc(t(\"no_household_configured\"))}<\/div>\r\n              <div class=\"divider\"><\/div>\r\n              <button class=\"btn\" data-create-foyer>${esc(t(\"create_my_household\"))}<\/button>\r\n            `}\r\n          <\/div>\r\n\r\n          <div class=\"card\" style=\"margin-top:12px\">\r\n            <strong>${esc(t(\"members_title\"))}<\/strong>\r\n            <div class=\"divider\"><\/div>\r\n            <div class=\"row\">\r\n              <div class=\"col\"><input id=\"f-name\" placeholder=\"${esc(t(\"placeholder_name\"))}\" \/><\/div>\r\n              <div class=\"col\"><input id=\"f-role\" placeholder=\"${esc(t(\"placeholder_role\"))}\" \/><\/div>\r\n              <div style=\"align-self:end\"><button class=\"btn\" data-f-add>${esc(t(\"add\"))}<\/button><\/div>\r\n            <\/div>\r\n            <div class=\"list\">\r\n              ${state.members.map(m=>`\r\n                <div class=\"item\">\r\n                  <div class=\"itemTop\">\r\n                    <div>\r\n                      <div class=\"itemTitle\">${esc(m.name)}<\/div>\r\n                      <div class=\"mini\">${esc(m.role||\"\")}<\/div>\r\n                    <\/div>\r\n                    <button class=\"btn danger\" data-f-del=\"${esc(m.id)}\">${esc(t(\"delete\"))}<\/button>\r\n                  <\/div>\r\n                <\/div>\r\n              `).join(\"\")}\r\n            <\/div>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div class=\"col\">\r\n          <div class=\"card\">\r\n            <strong>${esc(t(\"fast_access\"))}<\/strong>\r\n            <div class=\"divider\"><\/div>\r\n            ${installHelpCard()}\r\n          <\/div>\r\n        <\/div>\r\n      <\/div>\r\n    `;\r\n  }\r\n\r\n  function installHelpCard(){\r\n    detectPlatform();\r\n    if(state.ui.install.isStandalone) return `<div class=\"mini\">${esc(t(\"already_added\"))}<\/div>`;\r\n    if(state.ui.install.isAndroid){\r\n      return `\r\n        <div class=\"mini\">${esc(t(\"android_install_possible\"))}<\/div>\r\n        <div class=\"divider\"><\/div>\r\n        <button class=\"btn ${state.ui.install.canPromptInstall?\"\":\"secondary\"}\" data-install-android ${state.ui.install.canPromptInstall?\"\":\"disabled\"}>${esc(t(\"install_home\"))}<\/button>\r\n      `;\r\n    }\r\n    if(state.ui.install.isIOS){\r\n      return `\r\n        <div class=\"mini\">\r\n          ${esc(t(\"ios_install_hint\"))}<br>\r\n          ${esc(t(\"ios_install_hint_2\"))}\r\n        <\/div>\r\n      `;\r\n    }\r\n    return `<div class=\"mini\">${esc(t(\"open_on_mobile\"))}<\/div>`;\r\n  }\r\n\r\n  function viewHelp(){\r\n    return `\r\n      <div class=\"card\">\r\n        <strong>${esc(t(\"help_title\"))}<\/strong>\r\n        <div class=\"divider\"><\/div>\r\n        <div class=\"mini\">\r\n          ${esc(t(\"help_line_1\"))}<br>\r\n          ${esc(t(\"help_line_2\"))}<br>\r\n          ${esc(t(\"help_line_3\"))}<br>\r\n        <\/div>\r\n      <\/div>\r\n    `;\r\n  }\r\n\r\n  \/* ============================================================\r\n     16 \u2014 RENDER\r\n     ============================================================ *\/\r\n  function render(){\r\n    refreshLang(false);\r\n    renderNav();\r\n    renderInstallBanner();\r\n\r\n    const view = $(\"#jx-view\");\r\n    if(!state.foyerId) state.route = (state.route===\"help\") ? \"help\" : \"foyer\";\r\n\r\n    if(state.route===\"dashboard\") view.innerHTML = viewDashboard();\r\n    else if(state.route===\"courses\") view.innerHTML = viewCourses();\r\n    else if(state.route===\"meals\") view.innerHTML = viewMeals();\r\n    else if(state.route===\"foyer\") view.innerHTML = viewFoyer();\r\n    else view.innerHTML = viewHelp();\r\n  }\r\n\r\n  \/* ============================================================\r\n     17 \u2014 CLICK HANDLER\r\n     ============================================================ *\/\r\n  function onClick(e){\r\n    const tt = e.target;\r\n    const hit = (selector) => (tt.closest ? tt.closest(selector) : null);\r\n\r\n    const routeBtn = hit(\"[data-route]\");\r\n    if(routeBtn){\r\n      const r = routeBtn.getAttribute(\"data-route\");\r\n      if(!r) return;\r\n      state.route = r;\r\n      state._submode = (routeBtn.getAttribute(\"data-sub\")===\"store\") ? \"store\" : null;\r\n      renderIfChanged(true);\r\n      return;\r\n    }\r\n\r\n    if(hit(\"[data-install-dismiss]\")){ dismissInstallBanner(); return; }\r\n    if(hit(\"[data-install-android]\")){ triggerAndroidInstall(); return; }\r\n\r\n    if(hit(\"[data-create-foyer]\")){ createFoyerFlow(); return; }\r\n\r\n    if(hit(\"[data-open-shortcut]\")){\r\n      const u = state.foyerId && state.foyerKey\r\n        ? `${location.origin}${location.pathname}?foyer=${encodeURIComponent(state.foyerId)}&k=${encodeURIComponent(state.foyerKey)}`\r\n        : null;\r\n      if(u) location.href = u;\r\n      return;\r\n    }\r\n\r\n    const copyBtn = hit(\"[data-copy]\");\r\n    if(copyBtn){\r\n      const txt = copyBtn.getAttribute(\"data-copy\") || \"\";\r\n      if(navigator.clipboard && navigator.clipboard.writeText){\r\n        navigator.clipboard.writeText(txt);\r\n      }else{\r\n        try{\r\n          const tmp = document.createElement(\"textarea\");\r\n          tmp.value = txt;\r\n          document.body.appendChild(tmp);\r\n          tmp.select();\r\n          document.execCommand(\"copy\");\r\n          tmp.remove();\r\n        }catch(err){}\r\n      }\r\n      return;\r\n    }\r\n\r\n    if(hit(\"[data-f-add]\")){\r\n      const name = ($(\"#f-name\")?.value || \"\").trim();\r\n      const role = ($(\"#f-role\")?.value || \"\").trim();\r\n      if(!name) return;\r\n      state.members.push({ id: uid(\"m\"), name, role });\r\n      saveSoon();\r\n      renderIfChanged(true);\r\n      return;\r\n    }\r\n\r\n    const delMemberBtn = hit(\"[data-f-del]\");\r\n    if(delMemberBtn){\r\n      const id = delMemberBtn.getAttribute(\"data-f-del\");\r\n      state.members = state.members.filter(x => x.id !== id);\r\n      saveSoon();\r\n      renderIfChanged(true);\r\n      return;\r\n    }\r\n\r\n    if(hit(\"[data-push-enable]\")){\r\n      const msg = document.querySelector(\"#jx-push-msg\");\r\n      const setMsg = (html)=>{ if(msg) msg.innerHTML = html; };\r\n      setMsg(`<span class=\"mini\">${esc(t(\"push_activating\"))}<\/span>`);\r\n\r\n      enablePush()\r\n        .then(()=>{ setMsg(`<span class=\"mini\">${esc(t(\"push_enabled\"))}<\/span>`); })\r\n        .catch((err)=>{\r\n          const m = (err && (err.message||err)) ? String(err.message||err) : \"Error\";\r\n          setMsg(`<span class=\"mini\">\u26d4 ${esc(m)}<\/span>`);\r\n        });\r\n\r\n      return;\r\n    }\r\n\r\n    const storeBtn = hit(\"[data-store]\");\r\n    if(storeBtn){\r\n      state._submode = storeBtn.getAttribute(\"data-store\")===\"1\" ? \"store\" : null;\r\n      renderIfChanged(true);\r\n      return;\r\n    }\r\n\r\n    if(hit(\"[data-g-add]\")){\r\n      const name = ($(\"#g-name\")?.value || \"\").trim();\r\n      const qty  = clamp(Number($(\"#g-qty\")?.value || 1), 1, 999);\r\n      if(!name) return;\r\n      state.groceries.unshift({ id: uid(\"g\"), name, qty, status: \"open\" });\r\n      if($(\"#g-name\")) $(\"#g-name\").value = \"\";\r\n      if($(\"#g-qty\"))  $(\"#g-qty\").value  = \"1\";\r\n      saveSoon();\r\n      renderIfChanged(true);\r\n      return;\r\n    }\r\n\r\n    const gActBtn = hit(\"[data-g-act]\");\r\n    if(gActBtn){\r\n      const act = gActBtn.getAttribute(\"data-g-act\");\r\n      const id  = gActBtn.getAttribute(\"data-id\");\r\n\r\n      if(act === \"del\"){\r\n        state.groceries = state.groceries.filter(x => x.id !== id);\r\n      }else{\r\n        const it = state.groceries.find(x => x.id === id);\r\n        if(it){\r\n          if(act === \"request\") it.status = \"requested\";\r\n          if(act === \"take\")    it.status = \"taken\";\r\n          if(act === \"done\")    it.status = \"done\";\r\n        }\r\n      }\r\n      saveSoon();\r\n      renderIfChanged(true);\r\n      return;\r\n    }\r\n\r\n    if(hit(\"[data-m-add]\")){\r\n      const day  = $(\"#m-day\")?.value;\r\n      const meal = $(\"#m-meal\")?.value;\r\n      const title = ($(\"#m-title\")?.value || \"\").trim();\r\n      const responsibleId = $(\"#m-resp\")?.value;\r\n      if(!title) return;\r\n\r\n      state.meals.unshift({ id: uid(\"meal\"), day, meal, title, responsibleId });\r\n      if($(\"#m-title\")) $(\"#m-title\").value = \"\";\r\n      saveSoon();\r\n      renderIfChanged(true);\r\n      return;\r\n    }\r\n\r\n    const delMealBtn = hit(\"[data-m-del]\");\r\n    if(delMealBtn){\r\n      const id = delMealBtn.getAttribute(\"data-m-del\");\r\n      state.meals = state.meals.filter(x => x.id !== id);\r\n      saveSoon();\r\n      renderIfChanged(true);\r\n      return;\r\n    }\r\n\r\n    if(hit(\"[data-fb-savebudget]\")){\r\n      const v = Number($(\"#fb-budget\")?.value || 0);\r\n      state.fallback.budget_courses = Number.isFinite(v) ? v : null;\r\n      saveSoon();\r\n      renderIfChanged(true);\r\n      return;\r\n    }\r\n\r\n    if(hit(\"[data-fb-addexpense]\")){\r\n      const label  = ($(\"#fb-label\")?.value || \"\").trim() || t(\"expense_default\");\r\n      const amount = Number($(\"#fb-amount\")?.value || 0);\r\n      if(!Number.isFinite(amount) || amount <= 0) return;\r\n\r\n      state.fallback.expenses.unshift({\r\n        id: uid(\"exp\"),\r\n        label,\r\n        amount,\r\n        at: new Date().toLocaleString(state._lang===\"en\"?\"en-US\":\"fr-FR\")\r\n      });\r\n\r\n      if($(\"#fb-label\"))  $(\"#fb-label\").value = \"\";\r\n      if($(\"#fb-amount\")) $(\"#fb-amount\").value = \"\";\r\n\r\n      saveSoon();\r\n      renderIfChanged(true);\r\n      return;\r\n    }\r\n\r\n    const delExpBtn = hit(\"[data-fb-delexpense]\");\r\n    if(delExpBtn){\r\n      const id = delExpBtn.getAttribute(\"data-fb-delexpense\");\r\n      state.fallback.expenses = state.fallback.expenses.filter(x => x.id !== id);\r\n      saveSoon();\r\n      renderIfChanged(true);\r\n      return;\r\n    }\r\n  }\r\n\r\n  \/* ============================================================\r\n     18 \u2014 SYNC\r\n     ============================================================ *\/\r\n  const saveSoon = debounce(()=>syncToServer(), CFG.saveDebounceMs);\r\n\r\n  function payloadState(){\r\n    return {\r\n      version: CFG.version,\r\n      members: state.members,\r\n      groceries: state.groceries,\r\n      meals: state.meals,\r\n      fallback: state.fallback,\r\n      updatedAt: nowISO()\r\n    };\r\n  }\r\n\r\n  function applyServerState(s){\r\n    if(!s || typeof s!==\"object\") return;\r\n    state.members = Array.isArray(s.members) ? s.members : state.members;\r\n    state.groceries = Array.isArray(s.groceries) ? s.groceries : state.groceries;\r\n    state.meals = Array.isArray(s.meals) ? s.meals : state.meals;\r\n    state.fallback = (s.fallback && typeof s.fallback===\"object\") ? s.fallback : state.fallback;\r\n    state._serverUpdatedAt = s.updatedAt || s.updated_at || state._serverUpdatedAt || \"\";\r\n  }\r\n\r\n  async function syncFromServer(force=false){\r\n    if(!state.foyerId || !state.foyerKey) return;\r\n    if(state.ui.syncing && !force) return;\r\n    if(!force && shouldBlockRender()) return;\r\n\r\n    state.ui.syncing = true;\r\n    try{\r\n      const res = await apiGetFoyer(state.foyerId, state.foyerKey);\r\n      if(!force && shouldBlockRender()) return;\r\n      applyServerState(res.state);\r\n      state.ui.lastSync = new Date().toLocaleTimeString(state._lang===\"en\"?\"en-US\":\"fr-FR\");\r\n      state.ui.error = null;\r\n    }catch(err){\r\n      state.ui.error = String(err.message||err);\r\n    }finally{\r\n      state.ui.syncing = false;\r\n      renderIfChanged(false);\r\n    }\r\n  }\r\n\r\n  async function syncToServer(){\r\n    if(!state.foyerId || !state.foyerKey) return;\r\n    try{\r\n      await apiSaveFoyer(state.foyerId, state.foyerKey, payloadState());\r\n      state.ui.lastSync = new Date().toLocaleTimeString(state._lang===\"en\"?\"en-US\":\"fr-FR\");\r\n      state.ui.error = null;\r\n    }catch(err){\r\n      state.ui.error = String(err.message||err);\r\n    }\r\n  }\r\n\r\n  async function createFoyerFlow(){\r\n    try{\r\n      const localMembers = Array.isArray(state.members)\r\n        ? JSON.parse(JSON.stringify(state.members))\r\n        : [];\r\n\r\n      const created = await apiCreateFoyer();\r\n      state.foyerId = created.foyer_id;\r\n      state.foyerKey = created.foyer_key;\r\n\r\n      applyServerState(created.state);\r\n\r\n      if((!Array.isArray(state.members) || state.members.length === 0) && localMembers.length > 0){\r\n        state.members = localMembers;\r\n        await apiSaveFoyer(state.foyerId, state.foyerKey, payloadState());\r\n      }\r\n\r\n      setKeyForFoyer(state.foyerId, state.foyerKey);\r\n      rememberLastFoyer(state.foyerId);\r\n\r\n      const u=new URL(location.href);\r\n      u.searchParams.set(\"foyer\", state.foyerId);\r\n      u.searchParams.delete(\"k\");\r\n      history.replaceState({}, \"\", u.toString());\r\n\r\n      renderIfChanged(true);\r\n    }catch(err){\r\n      alert(t(\"err_create_household\") + (err.message||err));\r\n    }\r\n  }\r\n\r\n  function vaultDetectFast(){\r\n    state.vault.available = false;\r\n    state.vault.envelope = null;\r\n  }\r\n\r\n  \/* ============================================================\r\n     20 \u2014 INIT\r\n     ============================================================ *\/\r\n  async function init(){\r\n    \/\/ \u2705 lock scroll UNIQUEMENT ici, et UNIQUEMENT sur Home (guard JS)\r\n    document.documentElement.classList.add(\"jxh-lock\");\r\n    document.body.classList.add(\"jxh-lock\", \"jxh-home-page\");\r\n\r\n    detectPlatform();\r\n    mount();\r\n    vaultDetectFast();\r\n\r\n    const p = urlParams();\r\n\r\n    if(p.foyer){\r\n      state.foyerId = p.foyer;\r\n\r\n      if(p.k){\r\n        state.foyerKey = p.k;\r\n        setKeyForFoyer(state.foyerId, state.foyerKey);\r\n        rememberLastFoyer(state.foyerId);\r\n        cleanUrlKeepFoyer();\r\n      }else{\r\n        state.foyerKey = getKeyForFoyer(state.foyerId);\r\n        rememberLastFoyer(state.foyerId);\r\n      }\r\n    }else{\r\n      const last = getLastFoyer();\r\n      if(last){\r\n        state.foyerId = last;\r\n        state.foyerKey = getKeyForFoyer(last);\r\n      }\r\n    }\r\n\r\n    renderIfChanged(true);\r\n    postToParent({ type:\"jxhome:ready\", version: CFG.version });\r\n\r\n    if(state.foyerId && state.foyerKey){\r\n      await syncFromServer(true);\r\n      setInterval(()=>syncFromServer(false), CFG.pollMs);\r\n    }\r\n  }\r\n\r\n  init();\r\n\r\n})();\r\n<\/script>\r\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_header_footer","meta":{"site-sidebar-layout":"no-sidebar","site-content-layout":"page-builder","ast-site-content-layout":"full-width-container","site-content-style":"unboxed","site-sidebar-style":"unboxed","ast-global-header-display":"disabled","ast-banner-title-visibility":"disabled","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"disabled","ast-breadcrumbs-content":"","ast-featured-img":"disabled","footer-sml-layout":"disabled","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"set","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"class_list":["post-3856","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/jourx.fr\/en\/wp-json\/wp\/v2\/pages\/3856","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/jourx.fr\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/jourx.fr\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/jourx.fr\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/jourx.fr\/en\/wp-json\/wp\/v2\/comments?post=3856"}],"version-history":[{"count":89,"href":"https:\/\/jourx.fr\/en\/wp-json\/wp\/v2\/pages\/3856\/revisions"}],"predecessor-version":[{"id":4008,"href":"https:\/\/jourx.fr\/en\/wp-json\/wp\/v2\/pages\/3856\/revisions\/4008"}],"wp:attachment":[{"href":"https:\/\/jourx.fr\/en\/wp-json\/wp\/v2\/media?parent=3856"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}