{"product_id":"formulator-calculator","title":"Formulator calculator","description":"\u003cbody\u003e\n\n\n\u003cmeta charset=\"UTF-8\"\u003e\n\u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=5.0\"\u003e\n\u003ctitle\u003eSabi Library — Formulator's Calculator\u003c\/title\u003e\n\u003clink rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\"\u003e\n\u003clink href=\"https:\/\/fonts.googleapis.com\/css2?family=Poppins:wght@500;600;700;800\u0026amp;family=DM+Sans:wght@400;500;600;700\u0026amp;display=swap\" rel=\"stylesheet\"\u003e\n\u003cscript src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/jspdf\/2.5.1\/jspdf.umd.min.js\"\u003e\u003c\/script\u003e\n\u003cscript src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/xlsx\/0.18.5\/xlsx.full.min.js\"\u003e\u003c\/script\u003e\n\u003cstyle\u003e\n:root{\n  --forest:#003B2C; --sunshine:#FFBF00; --coral:#FF4040; --mango:#FF9F00;\n  --meadow:#168447; --pink:#F72567; --cream:#FFF3E3; --ink:#08251D;\n\n  --bg: var(--cream);\n  --surface:#ffffff;\n  --surface-2:#FBEFE0;\n  --text: var(--ink);\n  --text-muted: rgba(8,37,29,.62);\n  --text-faint: rgba(8,37,29,.4);\n  --border: rgba(8,37,29,.12);\n  --border-strong: rgba(8,37,29,.2);\n  --nav-bg: var(--forest);\n  --nav-text: var(--cream);\n  --nav-text-muted: rgba(255,243,227,.62);\n  --shadow-sm: 0 1px 2px rgba(8,37,29,.06), 0 1px 1px rgba(8,37,29,.04);\n  --shadow-md: 0 6px 20px rgba(8,37,29,.08);\n  --shadow-lg: 0 20px 48px rgba(8,37,29,.16);\n  --r-sm:8px; --r-md:12px; --r-lg:16px; --r-xl:22px;\n}\n[data-theme=\"dark\"]{\n  --bg:#051912;\n  --surface:#0C2A1F;\n  --surface-2:#0F3225;\n  --text: var(--cream);\n  --text-muted: rgba(255,243,227,.64);\n  --text-faint: rgba(255,243,227,.4);\n  --border: rgba(255,243,227,.12);\n  --border-strong: rgba(255,243,227,.22);\n  --nav-bg:#021009;\n  --nav-text: var(--cream);\n  --nav-text-muted: rgba(255,243,227,.55);\n  --shadow-sm: 0 1px 2px rgba(0,0,0,.3);\n  --shadow-md: 0 6px 20px rgba(0,0,0,.35);\n  --shadow-lg: 0 20px 48px rgba(0,0,0,.5);\n}\n*{box-sizing:border-box;}\nhtml,body{margin:0;padding:0;}\nbody{background:var(--bg); color:var(--text); font-family:'DM Sans',sans-serif; -webkit-font-smoothing:antialiased; transition:background .25s ease,color .25s ease;}\nh1,h2,h3,h4,.font-head{font-family:'Poppins',sans-serif;}\nbutton,input,select{font-family:inherit;color:inherit;}\ninput:focus,select:focus,button:focus-visible{outline:2px solid var(--meadow);outline-offset:1px;}\n::-webkit-scrollbar{width:10px;height:10px;}\n::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:99px;}\n\n.eyebrow{display:inline-block; font-size:11px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; margin-bottom:6px;}\n.badge{display:inline-flex; align-items:center; gap:6px; font-size:11px; font-weight:800; text-transform:uppercase; letter-spacing:.06em; padding:5px 11px; border-radius:99px;}\n\n\/* ---------- App shell ---------- *\/\n#app{min-height:100vh; display:flex; flex-direction:column;}\n.topbar{position:sticky; top:0; z-index:20; display:flex; align-items:center; gap:14px; padding:14px 20px; background:var(--nav-bg); box-shadow:var(--shadow-sm);}\n.icon-btn{display:inline-flex; align-items:center; justify-content:center; width:38px; height:38px; border-radius:10px; border:none; cursor:pointer; background:transparent; color:var(--nav-text); transition:background .15s ease; flex-shrink:0;}\n.icon-btn:hover{background:rgba(255,243,227,.12);}\n.icon-btn svg{width:20px;height:20px;}\n.topbar-title{font-family:'Poppins',sans-serif;font-weight:700;font-size:16px;color:var(--nav-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\n.topbar-sub{font-size:11px;color:var(--nav-text-muted);font-weight:600;text-transform:uppercase;letter-spacing:.08em;margin-top:1px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}\n.topbar-spacer{flex:1;}\n.topbar-titlewrap{min-width:0; overflow:hidden;}\n.topbar-actions{display:flex;align-items:center;gap:8px; min-width:0; flex-shrink:0;}\n.saved-pill{display:flex; align-items:center; gap:6px; font-size:11px; font-weight:700; color:var(--nav-text-muted); padding:6px 10px; border-radius:99px; background:rgba(255,243,227,.08); white-space:nowrap;}\n.saved-pill .dot{width:6px;height:6px;border-radius:50%;background:var(--meadow);flex-shrink:0;}\n.saved-pill.saving .dot{background:var(--sunshine);}\n.currency-select{background:rgba(255,255,255,.08); border:1px solid rgba(255,255,255,.25); color:var(--cream); font-weight:700; font-size:13px; border-radius:8px; padding:8px 10px; cursor:pointer;}\n.currency-select option{color:#000;}\n\n\/* ---------- Drawer nav ---------- *\/\n.drawer-overlay{position:fixed; inset:0; background:rgba(0,0,0,.4); z-index:29; opacity:0; pointer-events:none; transition:opacity .2s ease;}\n.drawer-overlay.open{opacity:1;pointer-events:auto;}\n.drawer{position:fixed; top:0; left:0; bottom:0; width:290px; max-width:84vw; background:var(--nav-bg); z-index:30; transform:translateX(-100%); transition:transform .25s cubic-bezier(.2,.8,.2,1); display:flex; flex-direction:column; box-shadow:var(--shadow-lg);}\n.drawer.open{transform:translateX(0);}\n.drawer-head{padding:22px 20px 18px; border-bottom:1px solid rgba(255,243,227,.1);}\n.drawer-wordmark{font-family:'Poppins',sans-serif;font-weight:800;font-size:19px;color:var(--cream);}\n.drawer-eyebrow{color:var(--sunshine);margin-top:6px;margin-bottom:0;}\n.drawer-nav{padding:14px 10px; display:flex; flex-direction:column; gap:2px; flex:1; overflow-y:auto;}\n.drawer-link{display:flex; align-items:center; gap:12px; padding:11px 12px; border-radius:10px; color:var(--cream); font-weight:600; font-size:14.5px; cursor:pointer; border:none; background:transparent; text-align:left; width:100%;}\n.drawer-link svg{width:18px;height:18px;flex-shrink:0;}\n.drawer-link:hover{background:rgba(255,191,0,.1); color:var(--sunshine);}\n.drawer-link.active{background:rgba(255,191,0,.16); color:var(--sunshine);}\n.drawer-foot{padding:16px 20px;border-top:1px solid rgba(255,243,227,.1);font-size:11px;color:var(--nav-text-muted);}\n\n\/* ---------- Main \/ layout ---------- *\/\nmain{flex:1; padding:24px 20px 64px;}\n.wrap{max-width:1080px;margin:0 auto;}\n.view{display:none;}\n.view.active{display:block;}\n.grid{display:grid;gap:18px;}\n.grid-2{grid-template-columns:1fr 1fr;}\n.grid-3{grid-template-columns:repeat(3,1fr);}\n.grid-4{grid-template-columns:repeat(4,1fr);}\n.grid-fields{grid-template-columns:repeat(2,1fr);}\n@media(max-width:760px){.grid-2,.grid-3,.grid-4{grid-template-columns:1fr;} .grid-fields{grid-template-columns:1fr;}}\n\n.card{position:relative; background:var(--surface); border:1px solid var(--border); border-radius:var(--r-lg); padding:24px; box-shadow:var(--shadow-sm);}\n.card.pad-top{padding-top:34px;}\n.chip{position:absolute; top:-16px; left:22px; width:38px; height:38px; border-radius:12px; display:flex; align-items:center; justify-content:center; box-shadow:var(--shadow-md);}\n.chip svg{width:17px;height:17px;color:#fff;}\n.card h3{margin:0 0 4px; font-size:17px; font-weight:700;}\n.card .hint{font-size:12.5px; color:var(--text-muted); line-height:1.5; margin:4px 0 0;}\n.section-head{margin-bottom:16px;}\n.section-head h2{margin:0 0 2px; font-size:19px; font-weight:700;}\n.section-head p{margin:0; font-size:13px; color:var(--text-muted);}\n\n\/* ---------- Fields ---------- *\/\n.field{display:flex; flex-direction:column; gap:6px;}\n.field label{font-size:10.5px; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--text-muted);}\n.input-wrap{display:flex; align-items:center; border:1.5px solid var(--border-strong); border-radius:var(--r-sm); background:var(--bg); padding:0 12px; transition:border-color .15s ease;}\n.input-wrap:focus-within{border-color:var(--meadow);}\n.input-wrap .prefix,.input-wrap .suffix{font-size:13px;font-weight:700;color:var(--text-faint);}\n.input-wrap .prefix{margin-right:6px;}\n.input-wrap .suffix{margin-left:6px;}\n.input-wrap input, .input-wrap select{border:none; background:transparent; width:100%; padding:10px 0; font-size:14px; font-weight:700;}\n.input-wrap select{cursor:pointer;}\n.input-wrap input[type=number]{-moz-appearance:textfield;}\n.input-wrap input::-webkit-outer-spin-button,.input-wrap input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0;}\n.field-plain{border:1.5px solid var(--border-strong); border-radius:var(--r-sm); background:var(--bg); padding:10px 12px; font-size:14px; font-weight:700; width:100%;}\n.subtotal-row{display:flex; align-items:center; justify-content:space-between; margin-top:16px; padding-top:16px; border-top:1px dashed var(--border-strong);}\n.subtotal-row .label{font-size:12.5px; font-weight:700; color:var(--text-muted); text-transform:uppercase; letter-spacing:.05em;}\n.subtotal-row .value{font-family:'Poppins',sans-serif; font-weight:800; font-size:19px;}\n\n\/* ---------- Segmented control \/ toggle ---------- *\/\n.segmented{display:inline-flex; border-radius:10px; padding:3px; background:var(--surface-2); border:1px solid var(--border);}\n.segmented button{border:none; background:transparent; padding:8px 16px; border-radius:8px; font-size:12.5px; font-weight:700; cursor:pointer; color:var(--text-muted);}\n.segmented button.active{background:var(--surface); color:var(--forest); box-shadow:var(--shadow-sm);}\n[data-theme=\"dark\"] .segmented button.active{color:var(--sunshine);}\n.toggle{display:inline-flex; align-items:center; gap:10px; cursor:pointer; user-select:none;}\n.toggle .track{width:42px; height:24px; border-radius:99px; background:var(--border-strong); position:relative; transition:background .18s ease; flex-shrink:0;}\n.toggle .thumb{position:absolute; top:3px; left:3px; width:18px; height:18px; border-radius:50%; background:#fff; box-shadow:var(--shadow-sm); transition:transform .18s ease;}\n.toggle.on .track{background:var(--meadow);}\n.toggle.on .thumb{transform:translateX(18px);}\n.toggle .label{font-size:13.5px; font-weight:700;}\n\n\/* ---------- Stat \/ KPI ---------- *\/\n.stat-grid{display:grid; grid-template-columns:repeat(4,1fr); gap:12px;}\n@media(max-width:760px){.stat-grid{grid-template-columns:repeat(2,1fr);}}\n.stat{border-radius:var(--r-md); padding:14px 16px; background:var(--surface-2);}\n.stat .k{font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--text-muted); margin-bottom:4px;}\n.stat .v{font-family:'Poppins',sans-serif; font-weight:800; font-size:19px;}\n.hero-price{border-radius:var(--r-xl); padding:28px; background:var(--forest); color:var(--cream); text-align:center;}\n.hero-price .k{color:var(--sunshine);}\n.hero-price .v{font-family:'Poppins',sans-serif; font-weight:800; font-size:36px; letter-spacing:-.01em;}\n.hero-price .s{font-size:12.5px; color:rgba(255,243,227,.65); margin-top:6px;}\n\n\/* ---------- Buttons ---------- *\/\n.btn{display:inline-flex; align-items:center; gap:8px; border-radius:10px; border:1.5px solid transparent; padding:10px 16px; font-size:13.5px; font-weight:700; cursor:pointer; white-space:nowrap;}\n.btn svg{width:15px;height:15px;}\n.btn-primary{background:var(--forest); color:var(--cream);}\n.btn-primary:hover{background:#02291e;}\n.btn-accent{background:var(--sunshine); color:var(--ink);}\n.btn-accent:hover{filter:brightness(.96);}\n.btn-ghost{background:transparent; border-color:var(--border-strong); color:var(--text);}\n.btn-ghost:hover{background:var(--surface-2);}\n.btn-danger{background:transparent; border-color:rgba(255,64,64,.4); color:var(--coral);}\n.btn-danger:hover{background:rgba(255,64,64,.08);}\n.btn-sm{padding:7px 11px; font-size:12px;}\n.btn-block{width:100%; justify-content:center;}\n.btn-row{display:flex; gap:8px; flex-wrap:wrap;}\n.icon-only{width:34px; height:34px; padding:0; justify-content:center; border-radius:9px;}\n\n\/* ---------- Dashboard \/ product cards ---------- *\/\n.stats-banner{display:grid; grid-template-columns:repeat(4,1fr); gap:12px; margin-bottom:24px;}\n@media(max-width:760px){.stats-banner{grid-template-columns:repeat(2,1fr);}}\n.mini-stat{background:var(--surface); border:1px solid var(--border); border-radius:var(--r-md); padding:16px; box-shadow:var(--shadow-sm);}\n.mini-stat .k{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted);margin-bottom:6px;}\n.mini-stat .v{font-family:'Poppins',sans-serif;font-weight:800;font-size:21px;}\n.toolbar{display:flex; align-items:center; gap:10px; margin-bottom:18px; flex-wrap:wrap;}\n.search-wrap{flex:1; min-width:200px; display:flex; align-items:center; gap:8px; border:1.5px solid var(--border-strong); border-radius:10px; padding:9px 12px; background:var(--surface);}\n.search-wrap svg{width:16px;height:16px;color:var(--text-faint); flex-shrink:0;}\n.search-wrap input{border:none;background:transparent;width:100%;font-size:13.5px;font-weight:600;}\n.select-sm{border:1.5px solid var(--border-strong); border-radius:10px; padding:9px 12px; font-size:13px; font-weight:700; background:var(--surface);}\n.product-grid{display:grid; grid-template-columns:repeat(auto-fill,minmax(240px,1fr)); gap:16px;}\n.product-card{background:var(--surface); border:1px solid var(--border); border-radius:var(--r-lg); padding:18px; box-shadow:var(--shadow-sm); cursor:pointer; transition:transform .15s ease, box-shadow .15s ease; display:flex; flex-direction:column; gap:12px;}\n.product-card:hover{transform:translateY(-2px); box-shadow:var(--shadow-md);}\n.product-card.archived{opacity:.6;}\n.product-card .pname{font-family:'Poppins',sans-serif; font-weight:700; font-size:15.5px; line-height:1.3;}\n.product-card .pcat{font-size:11px; color:var(--text-muted); font-weight:600; margin-top:2px;}\n.product-card .prow{display:flex; justify-content:space-between; align-items:baseline;}\n.product-card .prow .k{font-size:10.5px; text-transform:uppercase; letter-spacing:.05em; font-weight:700; color:var(--text-muted);}\n.product-card .prow .v{font-family:'Poppins',sans-serif; font-weight:800; font-size:15px;}\n.product-card .pdate{font-size:11px; color:var(--text-faint);}\n.product-card .pactions{display:flex; gap:6px; margin-top:2px;}\n.product-card .pactions button{flex:1; border:1px solid var(--border); background:var(--surface-2); border-radius:8px; height:30px; display:flex; align-items:center; justify-content:center; cursor:pointer; color:var(--text-muted);}\n.product-card .pactions button:hover{color:var(--forest); border-color:var(--border-strong);}\n.product-card .pactions button.danger:hover{color:var(--coral);}\n.product-card .pactions svg{width:14px;height:14px;}\n.empty-state{text-align:center; padding:64px 24px; border:1.5px dashed var(--border-strong); border-radius:var(--r-lg); color:var(--text-muted);}\n.empty-state svg{width:40px; height:40px; color:var(--text-faint); display:inline-block;}\n.empty-state h3{margin:14px 0 6px; color:var(--text);}\n.empty-state p{margin:0 0 18px; font-size:13.5px;}\n\n\/* ---------- Ingredient \/ packaging \/ cost rows ---------- *\/\n.row-list{display:flex; flex-direction:column; gap:12px;}\n.item-row{border:1.5px solid var(--border); border-radius:var(--r-md); padding:14px; background:var(--surface-2); position:relative;}\n.item-row-head{display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:10px;}\n.item-row-name{font-family:'Poppins',sans-serif; font-weight:700; font-size:14.5px;}\n.item-row-remove{width:28px;height:28px; border-radius:8px; border:1px solid var(--border-strong); background:var(--surface); display:flex; align-items:center; justify-content:center; cursor:pointer; color:var(--text-muted); flex-shrink:0;}\n.item-row-remove:hover{color:var(--coral); border-color:rgba(255,64,64,.4);}\n.item-row-remove svg{width:14px;height:14px;}\n.item-row-fields{display:grid; grid-template-columns:repeat(4,1fr); gap:10px;}\n.item-row-fields-compact{grid-template-columns:repeat(auto-fit, minmax(88px, 1fr));}\n@media(max-width:640px){.item-row-fields{grid-template-columns:repeat(2,1fr);}}\n.item-row-cost{margin-top:10px; padding-top:10px; border-top:1px dashed var(--border-strong); display:flex; justify-content:space-between; align-items:center; font-size:12.5px; font-weight:700; color:var(--text-muted);}\n.item-row-cost .v{font-family:'Poppins',sans-serif; font-weight:800; font-size:15px; color:var(--text);}\n.mini-field{display:flex; flex-direction:column; gap:4px;}\n.mini-field label{font-size:9.5px; font-weight:700; text-transform:uppercase; letter-spacing:.05em; color:var(--text-faint);}\n.mini-field .input-wrap{padding:0 8px;}\n.mini-field input,.mini-field select{padding:7px 0; font-size:13px;}\n\n.formula-balance{display:flex; align-items:center; justify-content:space-between; gap:14px; padding:14px 16px; border-radius:var(--r-md); background:var(--surface-2); margin-bottom:14px;}\n.formula-balance .fb-label{font-size:12.5px; font-weight:700; color:var(--text-muted); text-transform:uppercase; letter-spacing:.05em;}\n.formula-balance .fb-value{font-family:'Poppins',sans-serif; font-weight:800; font-size:22px;}\n.formula-balance .fb-status{font-size:12px; font-weight:800; padding:5px 12px; border-radius:99px; display:inline-flex; align-items:center; gap:5px; white-space:nowrap;}\n.formula-balance .fb-status svg{width:14px; height:14px; flex-shrink:0;}\n\n.ingredient-search{position:relative;}\n.ingredient-suggest{position:absolute; top:calc(100% + 4px); left:0; right:0; background:var(--surface); border:1px solid var(--border); border-radius:var(--r-md); box-shadow:var(--shadow-md); z-index:15; max-height:220px; overflow-y:auto; display:none;}\n.ingredient-suggest.open{display:block;}\n.ingredient-suggest button{display:flex; align-items:center; gap:8px; width:100%; text-align:left; padding:10px 14px; border:none; background:transparent; cursor:pointer; font-size:13.5px; font-weight:600; color:var(--text);}\n.ingredient-suggest button svg{width:14px; height:14px; flex-shrink:0;}\n.ingredient-suggest button:hover{background:var(--surface-2);}\n.ingredient-suggest .create-new{color:var(--meadow); font-weight:800; border-top:1px solid var(--border);}\n\n.lib-row{display:flex; align-items:center; justify-content:space-between; gap:12px; padding:14px 16px; border:1px solid var(--border); border-radius:var(--r-md); background:var(--surface); margin-bottom:10px;}\n.lib-row-name{font-family:'Poppins',sans-serif; font-weight:700; font-size:14.5px;}\n.lib-row-unit{font-size:11.5px; color:var(--text-muted); font-weight:600; margin-top:2px;}\n\n\/* ---------- Modal \/ Toast ---------- *\/\n.toast{position:fixed; bottom:22px; left:50%; transform:translateX(-50%) translateY(20px); z-index:60; background:var(--forest); color:var(--cream); padding:12px 20px; border-radius:12px; font-size:13.5px; font-weight:600; box-shadow:var(--shadow-lg); opacity:0; pointer-events:none; transition:all .25s ease;}\n.toast.show{opacity:1; transform:translateX(-50%) translateY(0);}\n.modal-overlay{position:fixed; inset:0; background:rgba(0,0,0,.45); z-index:50; display:flex; align-items:center; justify-content:center; padding:20px;}\n.modal{background:var(--surface); border-radius:var(--r-lg); padding:24px; max-width:420px; width:100%; box-shadow:var(--shadow-lg); max-height:86vh; overflow-y:auto;}\n.modal h3{margin:0 0 8px; font-size:16.5px;}\n.modal p{margin:0 0 16px; font-size:13.5px; color:var(--text-muted); line-height:1.55;}\n.modal .btn-row{justify-content:flex-end;}\n\n\/* Print *\/\n#printArea{display:none;}\n@media print{\n  body *{visibility:hidden;}\n  #printArea, #printArea *{visibility:visible;}\n  #printArea{display:block; position:absolute; top:0; left:0; width:100%; padding:20px; color:#08251D;}\n  .p-head{background:#003B2C; color:#FFF3E3; padding:20px; border-radius:10px; margin-bottom:20px; display:flex; justify-content:space-between; align-items:flex-start; gap:16px;}\n  .p-head .wm{font-weight:800; font-size:18px;}\n  .p-head .eb{color:rgba(255,243,227,.7); font-size:11px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; margin-top:4px;}\n  .p-head .brand-tag{color:#FFBF00; font-size:11px; font-weight:800; letter-spacing:.05em; white-space:nowrap; flex-shrink:0;}\n  .p-row{display:flex; justify-content:space-between; padding:6px 0; font-size:13px; border-bottom:1px solid #eee;}\n  .p-row.total{font-weight:800; font-size:15px; border-bottom:none; border-top:2px solid #08251D; margin-top:6px; padding-top:10px;}\n  .p-sec{font-weight:800; font-size:12px; text-transform:uppercase; letter-spacing:.06em; margin:18px 0 6px; color:#168447;}\n}\n@media(max-width:640px){\n  main{padding:18px 14px 48px;}\n  .topbar{padding:12px 14px; gap:8px;}\n  .topbar-actions{gap:5px;}\n  .topbar-actions [data-action=\"print-quote\"],\n  .topbar-actions [data-action=\"export-pdf\"]{display:none;}\n  .saved-pill span:last-child{display:none;}\n  .saved-pill{padding:6px 8px;}\n  .currency-select{padding:6px 7px; font-size:12px;}\n  .card{padding:18px;}\n  .hero-price .v{font-size:28px;}\n}\n\n\/* ---------- Access gate ---------- *\/\n.access-gate{position:fixed; inset:0; z-index:80; display:flex; align-items:center; justify-content:center; padding:24px; background:var(--forest);}\n.access-gate-card{max-width:360px; width:100%; background:rgba(255,243,227,.06); border:1px solid rgba(255,243,227,.14); border-radius:var(--r-xl); padding:32px 28px; text-align:center;}\n.access-gate-wordmark{font-family:'Poppins',sans-serif; font-weight:800; font-size:19px; color:var(--cream);}\n.access-gate-eyebrow{color:var(--sunshine); margin-top:6px;}\n.access-gate-card h2{font-family:'Poppins',sans-serif; color:var(--cream); font-size:17px; margin:16px 0 6px;}\n.access-gate-card p{color:rgba(255,243,227,.65); font-size:13px; line-height:1.55; margin:0 0 22px;}\n.access-gate-card .field{margin-bottom:14px; text-align:left;}\n.access-gate-card .field label{color:rgba(255,243,227,.6);}\n.access-gate-card .input-wrap{background:rgba(255,243,227,.08); border-color:rgba(255,243,227,.2);}\n.access-gate-card .input-wrap input{color:var(--cream);}\n.access-gate-error{color:var(--coral); font-size:12.5px; font-weight:700; margin:0 0 14px; min-height:16px;}\n.access-gate-disclaimer{color:rgba(255,243,227,.45); font-size:10.5px; line-height:1.5; margin:26px 0 0;}\n\u003c\/style\u003e\n\n\n\u003cdiv id=\"app\"\u003e\n\n  \u003cdiv class=\"access-gate\" id=\"accessGate\"\u003e\n    \u003cdiv class=\"access-gate-card\"\u003e\n      \u003cdiv class=\"access-gate-wordmark\"\u003eSABI LIBRARY\u003c\/div\u003e\n      \u003cdiv class=\"eyebrow access-gate-eyebrow\"\u003eFormulator's Calculator\u003c\/div\u003e\n      \u003ch2\u003eUnlock your calculator\u003c\/h2\u003e\n      \u003cp class=\"access-gate-error\" id=\"accessGateError\"\u003e\u003c\/p\u003e\n      \u003cdiv class=\"field\"\u003e\n        \u003clabel\u003eYour Name\u003c\/label\u003e\n        \u003cdiv class=\"input-wrap\"\u003e\u003cinput type=\"text\" id=\"accessGateName\" placeholder=\"e.g. Monica\" autocomplete=\"name\"\u003e\u003c\/div\u003e\n      \u003c\/div\u003e\n      \u003cdiv class=\"field\"\u003e\n        \u003clabel\u003eEmail\u003c\/label\u003e\n        \u003cdiv class=\"input-wrap\"\u003e\u003cinput type=\"email\" id=\"accessGateEmail\" placeholder=\"e.g. monica@email.com\" autocomplete=\"email\"\u003e\u003c\/div\u003e\n      \u003c\/div\u003e\n      \u003cdiv class=\"field\"\u003e\n        \u003clabel\u003eAccess Code\u003c\/label\u003e\n        \u003cdiv class=\"input-wrap\"\u003e\u003cinput type=\"text\" id=\"accessGateCode\" placeholder=\"6-digit code\" inputmode=\"numeric\" maxlength=\"6\" autocomplete=\"off\"\u003e\u003c\/div\u003e\n      \u003c\/div\u003e\n      \u003cdiv class=\"field\"\u003e\n        \u003clabel\u003eName Your Dashboard\u003c\/label\u003e\n        \u003cdiv class=\"input-wrap\"\u003e\u003cinput type=\"text\" id=\"accessGateDashboardName\" placeholder=\"e.g. Monica's Formulator Dashboard\" autocomplete=\"off\"\u003e\u003c\/div\u003e\n      \u003c\/div\u003e\n      \u003cbutton class=\"btn btn-accent btn-block\" id=\"accessGateSubmit\"\u003eUnlock\u003c\/button\u003e\n      \u003cp class=\"access-gate-disclaimer\"\u003eThis access is licensed to you personally. We are not responsible for any leak of confidential data.\u003c\/p\u003e\n    \u003c\/div\u003e\n  \u003c\/div\u003e\n\n  \u003cdiv class=\"drawer-overlay\" id=\"drawerOverlay\" data-action=\"close-drawer\"\u003e\u003c\/div\u003e\n  \u003cnav class=\"drawer\" id=\"drawer\"\u003e\n    \u003cdiv class=\"drawer-head\"\u003e\n      \u003cdiv class=\"drawer-wordmark\"\u003eSABI LIBRARY\u003c\/div\u003e\n      \u003cspan class=\"eyebrow drawer-eyebrow\"\u003eFormulator's Calculator\u003c\/span\u003e\n    \u003c\/div\u003e\n    \u003cdiv class=\"drawer-nav\"\u003e\n      \u003cbutton class=\"drawer-link\" data-action=\"nav-dashboard\" data-icon=\"grid\"\u003eDashboard\u003c\/button\u003e\n      \u003cbutton class=\"drawer-link\" data-action=\"nav-new\" data-icon=\"plus\"\u003eNew Product\u003c\/button\u003e\n      \u003cbutton class=\"drawer-link\" data-action=\"nav-saved\" data-icon=\"layers\"\u003eSaved Products\u003c\/button\u003e\n      \u003cbutton class=\"drawer-link\" data-action=\"nav-library\" data-icon=\"flask\"\u003eIngredient Library\u003c\/button\u003e\n      \u003cbutton class=\"drawer-link\" data-action=\"nav-settings\" data-icon=\"settings\"\u003eSettings\u003c\/button\u003e\n      \u003cbutton class=\"drawer-link\" data-action=\"nav-help\" data-icon=\"help\"\u003eHelp\u003c\/button\u003e\n    \u003c\/div\u003e\n    \u003cdiv class=\"drawer-foot\" id=\"drawerFoot\"\u003eEverything saves automatically to this browser.\u003c\/div\u003e\n  \u003c\/nav\u003e\n\n  \u003cheader class=\"topbar\"\u003e\n    \u003cbutton class=\"icon-btn\" id=\"hamburgerBtn\" data-action=\"toggle-drawer\" aria-label=\"Open menu\"\u003e\u003c\/button\u003e\n    \u003cdiv class=\"topbar-titlewrap\"\u003e\n      \u003cdiv class=\"topbar-title\" id=\"topbarTitle\"\u003eDashboard\u003c\/div\u003e\n      \u003cdiv class=\"topbar-sub\" id=\"topbarSub\"\u003e\u003c\/div\u003e\n    \u003c\/div\u003e\n    \u003cdiv class=\"topbar-spacer\"\u003e\u003c\/div\u003e\n    \u003cdiv class=\"topbar-actions\" id=\"topbarActions\"\u003e\u003c\/div\u003e\n  \u003c\/header\u003e\n\n  \u003cmain\u003e\n    \u003cdiv class=\"wrap\"\u003e\n      \u003csection id=\"view-dashboard\" class=\"view\"\u003e\u003c\/section\u003e\n      \u003csection id=\"view-saved\" class=\"view\"\u003e\u003c\/section\u003e\n      \u003csection id=\"view-product\" class=\"view\"\u003e\u003c\/section\u003e\n      \u003csection id=\"view-library\" class=\"view\"\u003e\u003c\/section\u003e\n      \u003csection id=\"view-settings\" class=\"view\"\u003e\u003c\/section\u003e\n      \u003csection id=\"view-help\" class=\"view\"\u003e\u003c\/section\u003e\n    \u003c\/div\u003e\n  \u003c\/main\u003e\n\n  \u003cdiv id=\"printArea\"\u003e\u003c\/div\u003e\n  \u003cdiv class=\"toast\" id=\"toast\"\u003e\u003c\/div\u003e\n  \u003cdiv id=\"modalRoot\"\u003e\u003c\/div\u003e\n\u003c\/div\u003e\n\n\u003cscript\u003e\n(function(){\n\"use strict\";\n\n\/* ============ ICONS ============ *\/\nconst ICONS = {\n  menu:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"\u003e\u003cpath d=\"M3 6h18M3 12h18M3 18h18\"\/\u003e\u003c\/svg\u003e',\n  grid:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003crect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1.5\"\/\u003e\u003crect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1.5\"\/\u003e\u003crect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1.5\"\/\u003e\u003crect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1.5\"\/\u003e\u003c\/svg\u003e',\n  plus:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"\u003e\u003cpath d=\"M12 5v14M5 12h14\"\/\u003e\u003c\/svg\u003e',\n  layers:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"m12 2 9 5-9 5-9-5 9-5Z\"\/\u003e\u003cpath d=\"m3 12 9 5 9-5\"\/\u003e\u003cpath d=\"m3 17 9 5 9-5\"\/\u003e\u003c\/svg\u003e',\n  flask:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M9 2v6L4 18a2 2 0 0 0 2 3h12a2 2 0 0 0 2-3l-5-10V2\"\/\u003e\u003cpath d=\"M8.5 2h7M6 14h12\"\/\u003e\u003c\/svg\u003e',\n  settings:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"3\"\/\u003e\u003cpath d=\"M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1 1.55V21a2 2 0 0 1-4 0v-.09A1.7 1.7 0 0 0 9 19.4a1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.55-1H3a2 2 0 0 1 0-4h.09A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-1.55V3a2 2 0 0 1 4 0v.09a1.7 1.7 0 0 0 1 1.55 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9a1.7 1.7 0 0 0 1.55 1H21a2 2 0 0 1 0 4h-.09a1.7 1.7 0 0 0-1.55 1Z\"\/\u003e\u003c\/svg\u003e',\n  help:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"\/\u003e\u003cpath d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 2-3 4\"\/\u003e\u003cpath d=\"M12 17h.01\"\/\u003e\u003c\/svg\u003e',\n  edit:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M12 20h9\"\/\u003e\u003cpath d=\"M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z\"\/\u003e\u003c\/svg\u003e',\n  copy:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003crect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\"\/\u003e\u003cpath d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"\/\u003e\u003c\/svg\u003e',\n  archive:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003crect x=\"2\" y=\"4\" width=\"20\" height=\"5\" rx=\"1\"\/\u003e\u003cpath d=\"M4 9v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9M10 13h4\"\/\u003e\u003c\/svg\u003e',\n  unarchive:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003crect x=\"2\" y=\"4\" width=\"20\" height=\"5\" rx=\"1\"\/\u003e\u003cpath d=\"M4 9v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9M14 16l-2-2-2 2M12 14v5\"\/\u003e\u003c\/svg\u003e',\n  trash:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0-1 14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2L4 6\"\/\u003e\u003c\/svg\u003e',\n  download:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"\/\u003e\u003cpath d=\"M7 10l5 5 5-5M12 15V3\"\/\u003e\u003c\/svg\u003e',\n  printer:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M6 9V2h12v7\"\/\u003e\u003cpath d=\"M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2\"\/\u003e\u003crect x=\"6\" y=\"14\" width=\"12\" height=\"8\"\/\u003e\u003c\/svg\u003e',\n  sun:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"4\"\/\u003e\u003cpath d=\"M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4\"\/\u003e\u003c\/svg\u003e',\n  moon:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z\"\/\u003e\u003c\/svg\u003e',\n  search:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003ccircle cx=\"11\" cy=\"11\" r=\"8\"\/\u003e\u003cpath d=\"m21 21-4.3-4.3\"\/\u003e\u003c\/svg\u003e',\n  box:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M21 8v13H3V8\"\/\u003e\u003cpath d=\"M1 3h22v5H1zM10 12h4\"\/\u003e\u003c\/svg\u003e',\n  tag:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M20.6 12.6 12.4 20.8a2 2 0 0 1-2.8 0l-8-8a2 2 0 0 1 0-2.8L9.8 1.8A2 2 0 0 1 11.2 1.2H18a2.8 2.8 0 0 1 2.8 2.8v6.8a2 2 0 0 1-.2 1.8Z\"\/\u003e\u003cpath d=\"M15.5 7.5h.01\"\/\u003e\u003c\/svg\u003e',\n  wallet:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M21 12V7H5a2 2 0 0 1 0-4h14v4\"\/\u003e\u003cpath d=\"M3 5v14a2 2 0 0 0 2 2h16v-5\"\/\u003e\u003cpath d=\"M18 12a2 2 0 0 0 0 4h4v-4Z\"\/\u003e\u003c\/svg\u003e',\n  scale:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M12 3v18M3 7h18M5 7l-3 6a3 3 0 0 0 6 0Zm14 0-3 6a3 3 0 0 0 6 0Z\"\/\u003e\u003c\/svg\u003e',\n  info:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"\/\u003e\u003cpath d=\"M12 16v-4M12 8h.01\"\/\u003e\u003c\/svg\u003e',\n  target:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"\/\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"6\"\/\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"2\"\/\u003e\u003c\/svg\u003e',\n  cog:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"3\"\/\u003e\u003cpath d=\"M12 2v3M12 19v3M4.2 4.2l2.1 2.1M17.7 17.7l2.1 2.1M2 12h3M19 12h3M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1\"\/\u003e\u003c\/svg\u003e',\n  package:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z\"\/\u003e\u003cpath d=\"m3.3 7 8.7 5 8.7-5M12 22V12\"\/\u003e\u003c\/svg\u003e',\n  check:'\u003csvg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M20 6 9 17l-5-5\"\/\u003e\u003c\/svg\u003e',\n};\nfunction icon(name){return ICONS[name]||'';}\n\n\/* ============ CONSTANTS ============ *\/\nconst CURRENCIES = { NGN:{symbol:'\\u20a6'}, USD:{symbol:'$'}, EUR:{symbol:'\\u20ac'}, GHS:{symbol:'GH\\u20b5'} };\nconst STORAGE_KEY = 'sabi_formulator_v1';\nconst WEIGHT_UNITS = { mg:0.001, g:1, kg:1000 };\nconst VOLUME_UNITS = { ml:1, L:1000 };\nconst ALL_UNITS = { ...WEIGHT_UNITS, ...VOLUME_UNITS };\nconst MANU_PRESETS = ['Labour','Electricity','Water','Factory Expenses','Gas','Transportation','Quality Control','Equipment Cost','Miscellaneous'];\n\nfunction unitFamily(unit){ return WEIGHT_UNITS[unit] ? 'weight' : (VOLUME_UNITS[unit] ? 'volume' : 'other'); }\nfunction toBase(value, unit){ const f = ALL_UNITS[unit]; return (Number(value)||0) * (f||1); }\nfunction baseLabel(unit){ return unitFamily(unit)==='volume' ? 'ml' : 'g'; }\n\n\/* ============ STATE ============ *\/\nlet state = loadState();\nlet currentView = 'dashboard';\nlet activeProductId = null;\nlet savedTimer = null;\n\nfunction uid(prefix){ return (prefix||'id')+'_'+Date.now().toString(36)+Math.random().toString(36).slice(2,7); }\n\nfunction defaultProduct(){\n  const now = Date.now();\n  return {\n    id: uid('prod'), name:'', archived:false, createdAt:now, updatedAt:now,\n    batchSize:{ value:1, unit:'kg' },\n    unitSize:{ value:100, unit:'g' },\n    unitsProduced: 10,\n    formulaMode:'weight',\n    ingredients: [],\n    packaging: [],\n    manufacturingCosts: MANU_PRESETS.map(label=\u003e({ id:uid('mc'), label, amount:0, preset:true })),\n    pricing: { mode:'margin', marginPercent:40, sellingPrice:0 },\n  };\n}\nfunction defaultIngredientRow(){\n  return { id:uid('ing'), ingredientId:null, name:'', unit:'g',\n    quantityPurchased:0, purchaseUnit:'g', purchaseCost:0,\n    quantityUsed:0, usedUnit:'g', percentage:0 };\n}\nfunction defaultPackagingRow(){\n  return { id:uid('pkg'), name:'', quantityPurchased:0, purchaseCost:0, quantityUsed:0 };\n}\n\nfunction loadState(){\n  try{\n    const raw = localStorage.getItem(STORAGE_KEY);\n    if(raw){ const parsed = JSON.parse(raw); if(parsed \u0026\u0026 parsed.products) return parsed; }\n  }catch(e){}\n  return { theme:'light', currency:'NGN', activeProductId:null, products:{}, ingredientLibrary:{} };\n}\nfunction persist(){ try{ localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); }catch(e){} }\nfunction scheduleSave(){\n  showSavedIndicator('saving');\n  clearTimeout(savedTimer);\n  savedTimer = setTimeout(()=\u003e{ persist(); showSavedIndicator('saved'); }, 450);\n}\nwindow.addEventListener('beforeunload', persist);\ndocument.addEventListener('visibilitychange', ()=\u003e{ if(document.hidden) persist(); });\n\nfunction fmt(amount){\n  const c = CURRENCIES[state.currency] || CURRENCIES.NGN;\n  const n = isFinite(amount) ? amount : 0;\n  return c.symbol + n.toLocaleString(undefined,{minimumFractionDigits:2, maximumFractionDigits:2});\n}\nfunction fmtNum(n, dp){ return (isFinite(n)?n:0).toLocaleString(undefined,{maximumFractionDigits:dp==null?2:dp}); }\nfunction fmtPct(n){ return (isFinite(n)?n:0).toFixed(1)+'%'; }\nfunction escapeHtml(s){ return String(s==null?'':s).replace(\/[\u0026\u003c\u003e\"']\/g, c=\u003e({'\u0026':'\u0026amp;','\u003c':'\u0026lt;','\u003e':'\u0026gt;','\"':'\u0026quot;',\"'\":'\u0026#39;'}[c])); }\nfunction escapeAttr(s){ return escapeHtml(s).replace(\/`\/g,'\u0026#96;'); }\n\n\/* ============ CALCULATIONS ============ *\/\nfunction computeIngredientCost(row, batchBaseValue, mode){\n  const costPerBase = row.quantityPurchased \u003e 0 ? row.purchaseCost \/ toBase(row.quantityPurchased, row.purchaseUnit) : 0;\n  let usedBase;\n  if(mode === 'percentage'){\n    usedBase = (Number(row.percentage)||0)\/100 * batchBaseValue;\n  } else {\n    usedBase = toBase(row.quantityUsed, row.usedUnit);\n  }\n  return { cost: costPerBase * usedBase, usedBase, costPerBase };\n}\nfunction computePackagingCost(row){\n  const per = row.quantityPurchased\u003e0 ? row.purchaseCost \/ row.quantityPurchased : 0;\n  return per * (Number(row.quantityUsed)||0);\n}\nfunction computeProduct(p){\n  const batchBase = toBase(p.batchSize.value, p.batchSize.unit);\n  let rawMaterialCost = 0, formulaPercentTotal = 0;\n  const ingredientCalcs = p.ingredients.map(row=\u003e{\n    const r = computeIngredientCost(row, batchBase, p.formulaMode);\n    rawMaterialCost += r.cost;\n    formulaPercentTotal += Number(row.percentage)||0;\n    return { row, ...r };\n  });\n  let packagingCost = 0;\n  p.packaging.forEach(row=\u003e{ packagingCost += computePackagingCost(row); });\n  let manufacturingCost = 0;\n  p.manufacturingCosts.forEach(row=\u003e{ manufacturingCost += Number(row.amount)||0; });\n\n  const totalBatchCost = rawMaterialCost + packagingCost + manufacturingCost;\n  const unitsProduced = Number(p.unitsProduced)||0;\n  const costPerUnit = unitsProduced\u003e0 ? totalBatchCost\/unitsProduced : 0;\n  const costPerBaseUnit = batchBase\u003e0 ? totalBatchCost\/batchBase : 0;\n\n  let sellingPrice;\n  if(p.pricing.mode === 'margin'){\n    const m = Math.min(Math.max(Number(p.pricing.marginPercent)||0,0),95);\n    sellingPrice = m\u003c100 ? costPerUnit\/(1-m\/100) : costPerUnit;\n  } else {\n    sellingPrice = Number(p.pricing.sellingPrice)||0;\n  }\n  const profit = sellingPrice - costPerUnit;\n  const marginPct = sellingPrice\u003e0 ? (profit\/sellingPrice)*100 : 0;\n  const markupPct = costPerUnit\u003e0 ? (profit\/costPerUnit)*100 : 0;\n  const expectedRevenue = sellingPrice * unitsProduced;\n\n  return {\n    batchBase, ingredientCalcs, rawMaterialCost, packagingCost, manufacturingCost,\n    totalBatchCost, costPerUnit, costPerBaseUnit, formulaPercentTotal,\n    sellingPrice, profit, marginPct, markupPct, expectedRevenue,\n  };\n}\n\n\/* ============ TOAST \/ MODAL ============ *\/\nfunction toast(msg){\n  const el = document.getElementById('toast');\n  el.textContent = msg; el.classList.add('show');\n  clearTimeout(el._t); el._t = setTimeout(()=\u003eel.classList.remove('show'), 2200);\n}\nfunction showSavedIndicator(kind){\n  const el = document.getElementById('savedPill'); if(!el) return;\n  if(kind==='saving'){ el.classList.add('saving'); el.querySelector('span').textContent='Saving\\u2026'; }\n  else { el.classList.remove('saving'); el.querySelector('span').textContent='Saved'; }\n}\nfunction openModal({title, body, actions}){\n  document.getElementById('modalRoot').innerHTML = `\n    \u003cdiv class=\"modal-overlay\" id=\"modalOverlay\"\u003e\n      \u003cdiv class=\"modal\"\u003e\u003ch3\u003e${title}\u003c\/h3\u003e\u003cdiv\u003e${body}\u003c\/div\u003e\u003cdiv class=\"btn-row\" style=\"margin-top:18px;\"\u003e${actions}\u003c\/div\u003e\u003c\/div\u003e\n    \u003c\/div\u003e`;\n  document.getElementById('modalOverlay').addEventListener('click', e=\u003e{ if(e.target.id==='modalOverlay') closeModal(); });\n}\nfunction closeModal(){ document.getElementById('modalRoot').innerHTML=''; }\n\n\/* ============ NAV ============ *\/\nconst VIEW_META = {\n  dashboard:{title:'Dashboard'}, saved:{title:'Saved Products'}, product:{title:'Product'},\n  library:{title:'Ingredient Library'}, settings:{title:'Settings'}, help:{title:'Help'},\n};\nfunction showView(name){\n  currentView = name;\n  document.querySelectorAll('.view').forEach(v=\u003ev.classList.remove('active'));\n  document.getElementById('view-'+name).classList.add('active');\n  document.querySelectorAll('.drawer-link').forEach(l=\u003el.classList.remove('active'));\n  const map = {dashboard:'nav-dashboard',saved:'nav-saved',product:'nav-new',library:'nav-library',settings:'nav-settings',help:'nav-help'};\n  const activeLink = document.querySelector(`[data-action=\"${map[name]}\"]`);\n  if(activeLink \u0026\u0026 name!=='product') activeLink.classList.add('active');\n\n  const meta = VIEW_META[name];\n  document.getElementById('topbarTitle').textContent = name==='product' \u0026\u0026 state.products[activeProductId]\n    ? (state.products[activeProductId].name || 'SABI LIBRARY')\n    : (name==='dashboard' \u0026\u0026 personalDashboardName ? personalDashboardName : meta.title);\n  document.getElementById('topbarSub').textContent = name==='product' ? \"Formulator's Calculator\" : '';\n  renderTopbarActions();\n  closeDrawer();\n\n  if(name==='dashboard') renderDashboard();\n  if(name==='saved') renderSaved();\n  if(name==='product') renderProduct();\n  if(name==='library') renderLibrary();\n  if(name==='settings') renderSettings();\n  if(name==='help') renderHelp();\n}\nfunction renderTopbarActions(){\n  const el = document.getElementById('topbarActions');\n  const currencyOpts = Object.keys(CURRENCIES).map(c=\u003e`\u003coption value=\"${c}\" ${state.currency===c?'selected':''}\u003e${c}\u003c\/option\u003e`).join('');\n  let extra = '';\n  if(currentView==='product' \u0026\u0026 state.products[activeProductId]){\n    extra = `\n      \u003cbutton class=\"icon-btn icon-only\" data-action=\"print-quote\" title=\"Print\" style=\"border:1px solid rgba(255,243,227,.18);\"\u003e${icon('printer')}\u003c\/button\u003e\n      \u003cbutton class=\"icon-btn icon-only\" data-action=\"export-pdf\" title=\"Export PDF\" style=\"border:1px solid rgba(255,243,227,.18);\"\u003e${icon('download')}\u003c\/button\u003e\n    `;\n  }\n  el.innerHTML = `\n    ${extra}\n    \u003cselect class=\"currency-select\" id=\"currencySelect\"\u003e${currencyOpts}\u003c\/select\u003e\n    \u003cbutton class=\"icon-btn\" data-action=\"toggle-theme\" title=\"Toggle theme\"\u003e${state.theme==='dark'?icon('sun'):icon('moon')}\u003c\/button\u003e\n    \u003cdiv class=\"saved-pill\" id=\"savedPill\"\u003e\u003cspan class=\"dot\"\u003e\u003c\/span\u003e\u003cspan\u003eSaved\u003c\/span\u003e\u003c\/div\u003e\n  `;\n  document.getElementById('currencySelect').addEventListener('change', e=\u003e{\n    state.currency = e.target.value; scheduleSave(); showView(currentView);\n  });\n}\nfunction openDrawer(){ document.getElementById('drawer').classList.add('open'); document.getElementById('drawerOverlay').classList.add('open'); }\nfunction closeDrawer(){ document.getElementById('drawer').classList.remove('open'); document.getElementById('drawerOverlay').classList.remove('open'); }\n\n\/* ============ PRODUCT ACTIONS ============ *\/\nfunction createNewProduct(){\n  const p = defaultProduct();\n  state.products[p.id] = p; activeProductId = p.id; state.activeProductId = p.id;\n  scheduleSave(); showView('product');\n}\nfunction openProduct(id){ activeProductId = id; state.activeProductId = id; scheduleSave(); showView('product'); }\nfunction duplicateProduct(id){\n  const src = state.products[id]; if(!src) return;\n  const clone = JSON.parse(JSON.stringify(src));\n  clone.id = uid('prod'); clone.name = (src.name||'Untitled formulation') + ' (Copy)';\n  clone.createdAt = Date.now(); clone.updatedAt = Date.now(); clone.archived=false;\n  state.products[clone.id] = clone; scheduleSave(); toast('Product duplicated'); refreshList();\n}\nfunction renameProduct(id){\n  const p = state.products[id]; if(!p) return;\n  openModal({ title:'Rename product',\n    body:`\u003cinput id=\"renameInput\" class=\"field-plain\" value=\"${escapeAttr(p.name)}\" placeholder=\"Product name\" \/\u003e`,\n    actions:`\u003cbutton class=\"btn btn-ghost\" data-action=\"cancel-modal\"\u003eCancel\u003c\/button\u003e\u003cbutton class=\"btn btn-primary\" data-action=\"confirm-rename\" data-id=\"${id}\"\u003eSave\u003c\/button\u003e` });\n  setTimeout(()=\u003e{ const i=document.getElementById('renameInput'); if(i){i.focus();i.select();} },20);\n}\nfunction archiveProduct(id,val){\n  const p = state.products[id]; if(!p) return;\n  p.archived=val; p.updatedAt=Date.now(); scheduleSave();\n  toast(val?'Product archived':'Product restored'); refreshList();\n}\nfunction deleteProduct(id){\n  const p = state.products[id]; if(!p) return;\n  openModal({ title:'Delete product?',\n    body:`\u003cp\u003eThis permanently deletes \u003cstrong\u003e${escapeHtml(p.name||'Untitled formulation')}\u003c\/strong\u003e and its full formulation. This can\\u2019t be undone.\u003c\/p\u003e`,\n    actions:`\u003cbutton class=\"btn btn-ghost\" data-action=\"cancel-modal\"\u003eCancel\u003c\/button\u003e\u003cbutton class=\"btn btn-danger\" data-action=\"confirm-delete\" data-id=\"${id}\"\u003eDelete\u003c\/button\u003e` });\n}\nfunction refreshList(){\n  if(currentView==='dashboard') renderDashboard();\n  if(currentView==='saved') renderSaved();\n  if(currentView==='product') renderProduct();\n}\n\n\/* ============ PRODUCT CARD ============ *\/\nfunction productCardHTML(p){\n  const c = computeProduct(p);\n  const date = new Date(p.updatedAt).toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'});\n  return `\n  \u003cdiv class=\"product-card ${p.archived?'archived':''}\" data-action=\"open-product\" data-id=\"${p.id}\"\u003e\n    \u003cdiv\u003e\n      \u003cdiv class=\"pname\"\u003e${escapeHtml(p.name)||'Untitled formulation'}\u003c\/div\u003e\n      \u003cdiv class=\"pcat\"\u003e${fmtNum(p.batchSize.value)} ${p.batchSize.unit} batch \\u00b7 ${p.unitsProduced} units\u003c\/div\u003e\n    \u003c\/div\u003e\n    \u003cdiv class=\"prow\"\u003e\u003cspan class=\"k\"\u003eCost \/ unit\u003c\/span\u003e\u003cspan class=\"v\"\u003e${fmt(c.costPerUnit)}\u003c\/span\u003e\u003c\/div\u003e\n    \u003cdiv class=\"prow\"\u003e\u003cspan class=\"k\"\u003eTotal batch cost\u003c\/span\u003e\u003cspan class=\"v\"\u003e${fmt(c.totalBatchCost)}\u003c\/span\u003e\u003c\/div\u003e\n    \u003cdiv class=\"pdate\"\u003eLast edited ${date}\u003c\/div\u003e\n    \u003cdiv class=\"pactions\"\u003e\n      \u003cbutton data-action=\"rename-product\" data-id=\"${p.id}\" title=\"Rename\"\u003e${icon('edit')}\u003c\/button\u003e\n      \u003cbutton data-action=\"duplicate-product\" data-id=\"${p.id}\" title=\"Duplicate\"\u003e${icon('copy')}\u003c\/button\u003e\n      \u003cbutton data-action=\"${p.archived?'unarchive-product':'archive-product'}\" data-id=\"${p.id}\" title=\"${p.archived?'Restore':'Archive'}\"\u003e${icon(p.archived?'unarchive':'archive')}\u003c\/button\u003e\n      \u003cbutton data-action=\"delete-product\" data-id=\"${p.id}\" title=\"Delete\" class=\"danger\"\u003e${icon('trash')}\u003c\/button\u003e\n    \u003c\/div\u003e\n  \u003c\/div\u003e`;\n}\n\n\/* ============ DASHBOARD ============ *\/\nfunction renderDashboard(){\n  const all = Object.values(state.products).filter(p=\u003e!p.archived).sort((a,b)=\u003eb.updatedAt-a.updatedAt);\n  const avgCostPerUnit = all.length ? all.reduce((s,p)=\u003es+computeProduct(p).costPerUnit,0)\/all.length : 0;\n  const totalBatchValue = all.reduce((s,p)=\u003es+computeProduct(p).totalBatchCost,0);\n\n  document.getElementById('view-dashboard').innerHTML = `\n    \u003cdiv class=\"section-head\"\u003e\n      \u003cspan class=\"eyebrow\" style=\"color:var(--meadow);\"\u003eOverview\u003c\/span\u003e\n      \u003ch2\u003e${personalDashboardName ? escapeHtml(personalDashboardName) : 'My Products'}\u003c\/h2\u003e\n      \u003cp\u003eEvery formulation you\\u2019ve costed, at a glance.\u003c\/p\u003e\n    \u003c\/div\u003e\n    \u003cdiv class=\"stats-banner\"\u003e\n      \u003cdiv class=\"mini-stat\"\u003e\u003cdiv class=\"k\"\u003eProducts\u003c\/div\u003e\u003cdiv class=\"v\"\u003e${all.length}\u003c\/div\u003e\u003c\/div\u003e\n      \u003cdiv class=\"mini-stat\"\u003e\u003cdiv class=\"k\"\u003eAvg. cost \/ unit\u003c\/div\u003e\u003cdiv class=\"v\"\u003e${fmt(avgCostPerUnit)}\u003c\/div\u003e\u003c\/div\u003e\n      \u003cdiv class=\"mini-stat\"\u003e\u003cdiv class=\"k\"\u003eCombined batch value\u003c\/div\u003e\u003cdiv class=\"v\"\u003e${fmt(totalBatchValue)}\u003c\/div\u003e\u003c\/div\u003e\n      \u003cdiv class=\"mini-stat\"\u003e\u003cdiv class=\"k\"\u003eIngredient library\u003c\/div\u003e\u003cdiv class=\"v\"\u003e${Object.keys(state.ingredientLibrary).length}\u003c\/div\u003e\u003c\/div\u003e\n    \u003c\/div\u003e\n    ${all.length ? `\u003cdiv class=\"product-grid\"\u003e${all.map(productCardHTML).join('')}\u003c\/div\u003e` : emptyStateHTML()}\n  `;\n}\nfunction emptyStateHTML(){\n  return `\u003cdiv class=\"empty-state\"\u003e${icon('box')}\u003ch3\u003eNo products yet\u003c\/h3\u003e\u003cp\u003eCreate your first formulation to start costing it.\u003c\/p\u003e\n    \u003cbutton class=\"btn btn-primary\" data-action=\"nav-new\"\u003e${icon('plus')} New Product\u003c\/button\u003e\u003c\/div\u003e`;\n}\n\n\/* ============ SAVED PRODUCTS ============ *\/\nlet savedFilter = { q:'', sort:'updated', showArchived:false };\nfunction renderSaved(){\n  document.getElementById('view-saved').innerHTML = `\n    \u003cdiv class=\"section-head\"\u003e\n      \u003cspan class=\"eyebrow\" style=\"color:var(--pink);\"\u003eLibrary\u003c\/span\u003e\n      \u003ch2\u003eSaved products\u003c\/h2\u003e\n      \u003cp\u003eSearch, manage, and reopen any formulation.\u003c\/p\u003e\n    \u003c\/div\u003e\n    \u003cdiv class=\"toolbar\"\u003e\n      \u003cdiv class=\"search-wrap\"\u003e${icon('search')}\u003cinput id=\"savedSearch\" placeholder=\"Search products...\" value=\"${escapeAttr(savedFilter.q)}\" \/\u003e\u003c\/div\u003e\n      \u003cselect class=\"select-sm\" id=\"savedSort\"\u003e\n        \u003coption value=\"updated\" ${savedFilter.sort==='updated'?'selected':''}\u003eLast edited\u003c\/option\u003e\n        \u003coption value=\"name\" ${savedFilter.sort==='name'?'selected':''}\u003eName (A\\u2013Z)\u003c\/option\u003e\n        \u003coption value=\"cost\" ${savedFilter.sort==='cost'?'selected':''}\u003eCost per unit\u003c\/option\u003e\n      \u003c\/select\u003e\n      \u003clabel class=\"toggle ${savedFilter.showArchived?'on':''}\" id=\"archiveToggle\"\u003e\u003cspan class=\"track\"\u003e\u003cspan class=\"thumb\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003cspan class=\"label\"\u003eShow archived\u003c\/span\u003e\u003c\/label\u003e\n      \u003cbutton class=\"btn btn-accent\" data-action=\"nav-new\"\u003e${icon('plus')} New\u003c\/button\u003e\n    \u003c\/div\u003e\n    \u003cdiv id=\"savedGrid\"\u003e\u003c\/div\u003e\n  `;\n  document.getElementById('savedSearch').addEventListener('input', e=\u003e{ savedFilter.q=e.target.value; renderSavedGrid(); });\n  document.getElementById('savedSort').addEventListener('change', e=\u003e{ savedFilter.sort=e.target.value; renderSavedGrid(); });\n  document.getElementById('archiveToggle').addEventListener('click', ()=\u003e{ savedFilter.showArchived=!savedFilter.showArchived; renderSaved(); });\n  renderSavedGrid();\n}\nfunction renderSavedGrid(){\n  const grid = document.getElementById('savedGrid');\n  let list = Object.values(state.products).filter(p=\u003esavedFilter.showArchived?true:!p.archived);\n  if(savedFilter.q.trim()){ const q=savedFilter.q.toLowerCase(); list=list.filter(p=\u003e(p.name||'').toLowerCase().includes(q)); }\n  if(savedFilter.sort==='updated') list.sort((a,b)=\u003eb.updatedAt-a.updatedAt);\n  if(savedFilter.sort==='name') list.sort((a,b)=\u003e(a.name||'').localeCompare(b.name||''));\n  if(savedFilter.sort==='cost') list.sort((a,b)=\u003ecomputeProduct(b).costPerUnit-computeProduct(a).costPerUnit);\n  grid.innerHTML = list.length ? `\u003cdiv class=\"product-grid\"\u003e${list.map(productCardHTML).join('')}\u003c\/div\u003e` : `\u003cdiv class=\"empty-state\"\u003e${icon('search')}\u003ch3\u003eNo matches\u003c\/h3\u003e\u003cp\u003eTry a different search term.\u003c\/p\u003e\u003c\/div\u003e`;\n}\n\n\/* ============ INGREDIENT LIBRARY ============ *\/\nfunction renderLibrary(){\n  const items = Object.values(state.ingredientLibrary).sort((a,b)=\u003ea.name.localeCompare(b.name));\n  document.getElementById('view-library').innerHTML = `\n    \u003cdiv class=\"section-head\"\u003e\n      \u003cspan class=\"eyebrow\" style=\"color:var(--coral);\"\u003eReference\u003c\/span\u003e\n      \u003ch2\u003eIngredient Library\u003c\/h2\u003e\n      \u003cp\u003eNames and default units only \\u2014 purchase prices are never stored here, since raw material costs change often.\u003c\/p\u003e\n    \u003c\/div\u003e\n    \u003cdiv class=\"btn-row\" style=\"margin-bottom:18px;\"\u003e\u003cbutton class=\"btn btn-accent\" data-action=\"add-library-ingredient\"\u003e${icon('plus')} Add Ingredient\u003c\/button\u003e\u003c\/div\u003e\n    \u003cdiv id=\"libraryList\"\u003e\n      ${items.length ? items.map(i=\u003e`\n        \u003cdiv class=\"lib-row\"\u003e\n          \u003cdiv\u003e\u003cdiv class=\"lib-row-name\"\u003e${escapeHtml(i.name)}\u003c\/div\u003e\u003cdiv class=\"lib-row-unit\"\u003eDefault unit: ${i.defaultUnit}\u003c\/div\u003e\u003c\/div\u003e\n          \u003cdiv class=\"btn-row\"\u003e\n            \u003cbutton class=\"btn btn-ghost btn-sm\" data-action=\"edit-library-ingredient\" data-id=\"${i.id}\"\u003e${icon('edit')} Edit\u003c\/button\u003e\n            \u003cbutton class=\"btn btn-danger btn-sm\" data-action=\"delete-library-ingredient\" data-id=\"${i.id}\"\u003e${icon('trash')} Delete\u003c\/button\u003e\n          \u003c\/div\u003e\n        \u003c\/div\u003e`).join('') : `\u003cdiv class=\"empty-state\"\u003e${icon('flask')}\u003ch3\u003eNo ingredients yet\u003c\/h3\u003e\u003cp\u003eAdd ingredients here, or add them directly while building a formulation.\u003c\/p\u003e\u003c\/div\u003e`}\n    \u003c\/div\u003e\n  `;\n}\nfunction libraryIngredientModal(existing){\n  const ing = existing || { id:null, name:'', defaultUnit:'g' };\n  openModal({\n    title: existing ? 'Edit ingredient' : 'Add ingredient',\n    body: `\n      \u003cdiv class=\"field\" style=\"margin-bottom:12px;\"\u003e\u003clabel\u003eIngredient Name\u003c\/label\u003e\u003cinput id=\"libName\" class=\"field-plain\" value=\"${escapeAttr(ing.name)}\" placeholder=\"e.g. Niacinamide\" \/\u003e\u003c\/div\u003e\n      \u003cdiv class=\"field\"\u003e\u003clabel\u003eDefault Unit\u003c\/label\u003e\n        \u003cselect id=\"libUnit\" class=\"field-plain\"\u003e\n          ${Object.keys(ALL_UNITS).map(u=\u003e`\u003coption value=\"${u}\" ${ing.defaultUnit===u?'selected':''}\u003e${u}\u003c\/option\u003e`).join('')}\n        \u003c\/select\u003e\n      \u003c\/div\u003e`,\n    actions: `\u003cbutton class=\"btn btn-ghost\" data-action=\"cancel-modal\"\u003eCancel\u003c\/button\u003e\u003cbutton class=\"btn btn-primary\" data-action=\"save-library-ingredient\" data-id=\"${ing.id||''}\"\u003eSave\u003c\/button\u003e`\n  });\n}\nfunction saveLibraryIngredient(existingId){\n  const name = document.getElementById('libName').value.trim();\n  const unit = document.getElementById('libUnit').value;\n  if(!name){ toast('Enter an ingredient name'); return; }\n  const id = existingId || uid('lib');\n  state.ingredientLibrary[id] = { id, name, defaultUnit:unit };\n  scheduleSave(); closeModal(); toast('Ingredient saved'); refreshList(); if(currentView==='library') renderLibrary();\n}\n\n\/* ============ PRODUCT WORKSPACE ============ *\/\nfunction renderProduct(){\n  const p = state.products[activeProductId];\n  const el = document.getElementById('view-product');\n  if(!p){\n    el.innerHTML = `\u003cdiv class=\"empty-state\"\u003e${icon('flask')}\u003ch3\u003eNo product open\u003c\/h3\u003e\u003cp\u003eCreate a new product or open one from your saved products.\u003c\/p\u003e\n      \u003cdiv class=\"btn-row\" style=\"justify-content:center;\"\u003e\u003cbutton class=\"btn btn-primary\" data-action=\"nav-new\"\u003e${icon('plus')} New Product\u003c\/button\u003e\u003cbutton class=\"btn btn-ghost\" data-action=\"nav-saved\"\u003eBrowse saved\u003c\/button\u003e\u003c\/div\u003e\u003c\/div\u003e`;\n    return;\n  }\n  const sym = CURRENCIES[state.currency].symbol;\n  const weightUnitOpts = Object.keys(WEIGHT_UNITS).concat(Object.keys(VOLUME_UNITS));\n\n  el.innerHTML = `\n    \u003cdiv class=\"section-head\"\u003e\n      \u003cspan class=\"eyebrow\" style=\"color:var(--meadow);\"\u003eFormulator's Calculator\u003c\/span\u003e\n      \u003ch2 id=\"prodTitle\"\u003e${escapeHtml(p.name)||'SABI LIBRARY'}\u003c\/h2\u003e\n      \u003cp\u003eBuild the formulation, then see the true cost to make it.\u003c\/p\u003e\n    \u003c\/div\u003e\n    \u003cdiv class=\"btn-row\" style=\"margin-bottom:20px;\"\u003e\n      \u003cbutton class=\"btn btn-ghost btn-sm\" data-action=\"rename-product\" data-id=\"${p.id}\"\u003e${icon('edit')} Rename\u003c\/button\u003e\n      \u003cbutton class=\"btn btn-ghost btn-sm\" data-action=\"duplicate-product\" data-id=\"${p.id}\"\u003e${icon('copy')} Duplicate\u003c\/button\u003e\n      \u003cbutton class=\"btn btn-ghost btn-sm\" data-action=\"${p.archived?'unarchive-product':'archive-product'}\" data-id=\"${p.id}\"\u003e${icon(p.archived?'unarchive':'archive')} ${p.archived?'Restore':'Archive'}\u003c\/button\u003e\n      \u003cbutton class=\"btn btn-danger btn-sm\" data-action=\"delete-product\" data-id=\"${p.id}\"\u003e${icon('trash')} Delete\u003c\/button\u003e\n      \u003cspan style=\"flex:1;\"\u003e\u003c\/span\u003e\n      \u003cbutton class=\"btn btn-ghost btn-sm\" data-action=\"export-excel\"\u003e${icon('download')} Excel\u003c\/button\u003e\n      \u003cbutton class=\"btn btn-ghost btn-sm\" data-action=\"export-pdf\"\u003e${icon('download')} PDF\u003c\/button\u003e\n      \u003cbutton class=\"btn btn-ghost btn-sm\" data-action=\"print-quote\"\u003e${icon('printer')} Print\u003c\/button\u003e\n    \u003c\/div\u003e\n\n    \u003cdiv id=\"prodForm\"\u003e\n\n      \u003cdiv class=\"card pad-top\" style=\"margin-bottom:18px;\"\u003e\n        \u003cdiv class=\"chip\" style=\"background:var(--meadow);\"\u003e${icon('tag')}\u003c\/div\u003e\n        \u003cspan class=\"eyebrow\" style=\"color:var(--meadow);\"\u003e1. Product Information\u003c\/span\u003e\n        \u003ch3\u003eThe basics\u003c\/h3\u003e\n        \u003cdiv class=\"grid grid-fields\" style=\"margin-top:14px;\"\u003e\n          \u003cdiv class=\"field\"\u003e\u003clabel\u003eProduct Name\u003c\/label\u003e\u003cinput class=\"field-plain\" data-path=\"name\" value=\"${escapeAttr(p.name)}\" placeholder=\"e.g. Brightening Face Cream\" \/\u003e\u003c\/div\u003e\n          \u003cdiv class=\"field\"\u003e\u003clabel\u003eUnits Produced\u003c\/label\u003e\u003cinput type=\"number\" min=\"0\" class=\"field-plain\" data-path=\"unitsProduced\" value=\"${p.unitsProduced}\" placeholder=\"e.g. 40\" \/\u003e\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"grid grid-fields\" style=\"margin-top:12px;\"\u003e\n          \u003cdiv class=\"field\"\u003e\n            \u003clabel\u003eBatch Size\u003c\/label\u003e\n            \u003cdiv style=\"display:flex; gap:8px;\"\u003e\n              \u003cdiv class=\"input-wrap\" style=\"flex:1;\"\u003e\u003cinput type=\"number\" min=\"0\" step=\"0.01\" data-path=\"batchSize.value\" value=\"${p.batchSize.value}\" \/\u003e\u003c\/div\u003e\n              \u003cselect class=\"field-plain\" style=\"width:90px;\" data-path=\"batchSize.unit\"\u003e${weightUnitOpts.map(u=\u003e`\u003coption value=\"${u}\" ${p.batchSize.unit===u?'selected':''}\u003e${u}\u003c\/option\u003e`).join('')}\u003c\/select\u003e\n            \u003c\/div\u003e\n          \u003c\/div\u003e\n          \u003cdiv class=\"field\"\u003e\n            \u003clabel\u003eUnit Size (per finished item)\u003c\/label\u003e\n            \u003cdiv style=\"display:flex; gap:8px;\"\u003e\n              \u003cdiv class=\"input-wrap\" style=\"flex:1;\"\u003e\u003cinput type=\"number\" min=\"0\" step=\"0.01\" data-path=\"unitSize.value\" value=\"${p.unitSize.value}\" \/\u003e\u003c\/div\u003e\n              \u003cselect class=\"field-plain\" style=\"width:90px;\" data-path=\"unitSize.unit\"\u003e${weightUnitOpts.map(u=\u003e`\u003coption value=\"${u}\" ${p.unitSize.unit===u?'selected':''}\u003e${u}\u003c\/option\u003e`).join('')}\u003c\/select\u003e\n            \u003c\/div\u003e\n          \u003c\/div\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"card pad-top\" style=\"margin-bottom:18px;\"\u003e\n        \u003cdiv class=\"chip\" style=\"background:var(--coral);\"\u003e${icon('flask')}\u003c\/div\u003e\n        \u003cspan class=\"eyebrow\" style=\"color:var(--coral);\"\u003e2. Ingredients\u003c\/span\u003e\n        \u003ch3\u003eWhat goes into the formula\u003c\/h3\u003e\n        \u003cdiv style=\"margin:14px 0;\"\u003e${segmented('formulaMode', p.formulaMode, [['weight','Weight Mode'],['percentage','Percentage Mode']])}\u003c\/div\u003e\n        ${p.formulaMode==='percentage' ? formulaBalanceHTML(p) : ''}\n        \u003cdiv class=\"row-list\" id=\"ingredientList\"\u003e${p.ingredients.map(row=\u003eingredientRowHTML(p,row)).join('') || `\u003cp class=\"hint\"\u003eNo ingredients yet.\u003c\/p\u003e`}\u003c\/div\u003e\n        \u003cdiv class=\"btn-row\" style=\"margin-top:14px;\"\u003e\u003cbutton class=\"btn btn-accent btn-sm\" data-action=\"add-ingredient\"\u003e${icon('plus')} Add Ingredient\u003c\/button\u003e\u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"card pad-top\" style=\"margin-bottom:18px;\"\u003e\n        \u003cdiv class=\"chip\" style=\"background:var(--mango);\"\u003e${icon('package')}\u003c\/div\u003e\n        \u003cspan class=\"eyebrow\" style=\"color:var(--mango);\"\u003e3. Packaging\u003c\/span\u003e\n        \u003ch3\u003eBottles, jars, labels, boxes\u003c\/h3\u003e\n        \u003cdiv class=\"row-list\" id=\"packagingList\" style=\"margin-top:14px;\"\u003e${p.packaging.map(row=\u003epackagingRowHTML(row)).join('') || `\u003cp class=\"hint\"\u003eNo packaging items yet.\u003c\/p\u003e`}\u003c\/div\u003e\n        \u003cdiv class=\"btn-row\" style=\"margin-top:14px;\"\u003e\u003cbutton class=\"btn btn-accent btn-sm\" data-action=\"add-packaging\"\u003e${icon('plus')} Add Packaging\u003c\/button\u003e\u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"card pad-top\" style=\"margin-bottom:18px;\"\u003e\n        \u003cdiv class=\"chip\" style=\"background:var(--forest);\"\u003e${icon('cog')}\u003c\/div\u003e\n        \u003cspan class=\"eyebrow\" style=\"color:var(--forest);\"\u003e4. Manufacturing Costs\u003c\/span\u003e\n        \u003ch3\u003eEverything else it costs to produce this batch\u003c\/h3\u003e\n        \u003cdiv class=\"grid grid-fields\" style=\"margin-top:14px;\" id=\"manuList\"\u003e${p.manufacturingCosts.map(row=\u003emanuRowHTML(row)).join('')}\u003c\/div\u003e\n        \u003cdiv class=\"btn-row\" style=\"margin-top:14px;\"\u003e\u003cbutton class=\"btn btn-ghost btn-sm\" data-action=\"add-manu-cost\"\u003e${icon('plus')} Add Custom Cost\u003c\/button\u003e\u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"card pad-top\" style=\"margin-bottom:18px;\"\u003e\n        \u003cdiv class=\"chip\" style=\"background:var(--sunshine);\"\u003e${icon('scale')}\u003c\/div\u003e\n        \u003cspan class=\"eyebrow\" style=\"color:#B38300;\"\u003e5. Results Dashboard\u003c\/span\u003e\n        \u003ch3\u003eThe true cost of this batch\u003c\/h3\u003e\n        \u003cdiv class=\"stat-grid\" style=\"margin-top:14px;\"\u003e\n          \u003cdiv class=\"stat\"\u003e\u003cdiv class=\"k\"\u003eRaw Material Cost\u003c\/div\u003e\u003cdiv class=\"v\" id=\"res-raw\"\u003e${fmt(0)}\u003c\/div\u003e\u003c\/div\u003e\n          \u003cdiv class=\"stat\"\u003e\u003cdiv class=\"k\"\u003ePackaging Cost\u003c\/div\u003e\u003cdiv class=\"v\" id=\"res-pkg\"\u003e${fmt(0)}\u003c\/div\u003e\u003c\/div\u003e\n          \u003cdiv class=\"stat\"\u003e\u003cdiv class=\"k\"\u003eManufacturing Cost\u003c\/div\u003e\u003cdiv class=\"v\" id=\"res-manu\"\u003e${fmt(0)}\u003c\/div\u003e\u003c\/div\u003e\n          \u003cdiv class=\"stat\"\u003e\u003cdiv class=\"k\"\u003eTotal Batch Cost\u003c\/div\u003e\u003cdiv class=\"v\" id=\"res-total\"\u003e${fmt(0)}\u003c\/div\u003e\u003c\/div\u003e\n          \u003cdiv class=\"stat\"\u003e\u003cdiv class=\"k\"\u003eCost Per ${baseLabel(p.batchSize.unit)}\u003c\/div\u003e\u003cdiv class=\"v\" id=\"res-perbase\"\u003e${fmt(0)}\u003c\/div\u003e\u003c\/div\u003e\n          \u003cdiv class=\"stat\"\u003e\u003cdiv class=\"k\"\u003eCost Per Unit\u003c\/div\u003e\u003cdiv class=\"v\" id=\"res-perunit\"\u003e${fmt(0)}\u003c\/div\u003e\u003c\/div\u003e\n          \u003cdiv class=\"stat\"\u003e\u003cdiv class=\"k\"\u003eUnits Produced\u003c\/div\u003e\u003cdiv class=\"v\" id=\"res-units\"\u003e${p.unitsProduced}\u003c\/div\u003e\u003c\/div\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"card pad-top\"\u003e\n        \u003cdiv class=\"chip\" style=\"background:var(--pink);\"\u003e${icon('target')}\u003c\/div\u003e\n        \u003cspan class=\"eyebrow\" style=\"color:var(--pink);\"\u003e6. Pricing Simulation\u003c\/span\u003e\n        \u003ch3\u003eWhat if I sold each unit at\\u2026\u003c\/h3\u003e\n        \u003cp class=\"hint\"\u003eOptional \\u2014 this tool is primarily for formulation costing. Use this to sanity-check a price.\u003c\/p\u003e\n        \u003cdiv style=\"margin:14px 0;\"\u003e${segmented('pricingMode', p.pricing.mode, [['margin','Set Margin %'],['price','Set Selling Price']])}\u003c\/div\u003e\n        \u003cdiv class=\"grid grid-fields\"\u003e\n          \u003cdiv class=\"field\" id=\"pricingMarginField\" style=\"${p.pricing.mode==='margin'?'':'display:none;'}\"\u003e\n            \u003clabel\u003eDesired Profit Margin\u003c\/label\u003e\n            \u003cdiv class=\"input-wrap\"\u003e\u003cinput type=\"number\" min=\"0\" max=\"95\" step=\"0.5\" data-path=\"pricing.marginPercent\" value=\"${p.pricing.marginPercent}\" \/\u003e\u003cspan class=\"suffix\"\u003e%\u003c\/span\u003e\u003c\/div\u003e\n          \u003c\/div\u003e\n          \u003cdiv class=\"field\" id=\"pricingPriceField\" style=\"${p.pricing.mode==='price'?'':'display:none;'}\"\u003e\n            \u003clabel\u003eSelling Price \/ Unit\u003c\/label\u003e\n            \u003cdiv class=\"input-wrap\"\u003e\u003cspan class=\"prefix\"\u003e${sym}\u003c\/span\u003e\u003cinput type=\"number\" min=\"0\" step=\"1\" data-path=\"pricing.sellingPrice\" value=\"${p.pricing.sellingPrice}\" \/\u003e\u003c\/div\u003e\n          \u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"hero-price\" style=\"margin-top:16px;\"\u003e\n          \u003cspan class=\"eyebrow k\"\u003eRecommended Selling Price\u003c\/span\u003e\n          \u003cdiv class=\"v\" id=\"sim-sellingPrice\"\u003e${fmt(0)}\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"stat-grid\" style=\"margin-top:16px;\"\u003e\n          \u003cdiv class=\"stat\"\u003e\u003cdiv class=\"k\"\u003eProfit \/ Unit\u003c\/div\u003e\u003cdiv class=\"v\" id=\"sim-profit\"\u003e${fmt(0)}\u003c\/div\u003e\u003c\/div\u003e\n          \u003cdiv class=\"stat\"\u003e\u003cdiv class=\"k\"\u003eProfit Margin\u003c\/div\u003e\u003cdiv class=\"v\" id=\"sim-margin\"\u003e0%\u003c\/div\u003e\u003c\/div\u003e\n          \u003cdiv class=\"stat\"\u003e\u003cdiv class=\"k\"\u003eMarkup\u003c\/div\u003e\u003cdiv class=\"v\" id=\"sim-markup\"\u003e0%\u003c\/div\u003e\u003c\/div\u003e\n          \u003cdiv class=\"stat\"\u003e\u003cdiv class=\"k\"\u003eExpected Revenue\u003c\/div\u003e\u003cdiv class=\"v\" id=\"sim-revenue\"\u003e${fmt(0)}\u003c\/div\u003e\u003c\/div\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n    \u003c\/div\u003e\n  `;\n  attachProductListeners(p);\n  updateComputedDisplays(p);\n}\n\nfunction segmented(path, current, options){\n  return `\u003cdiv class=\"segmented\" data-path=\"${path}\"\u003e\n    ${options.map(([val,label])=\u003e`\u003cbutton type=\"button\" data-action=\"set-segment\" data-path=\"${path}\" data-value=\"${val}\" class=\"${current===val?'active':''}\"\u003e${label}\u003c\/button\u003e`).join('')}\n  \u003c\/div\u003e`;\n}\nfunction formulaBalanceHTML(p){\n  const total = p.ingredients.reduce((s,r)=\u003es+(Number(r.percentage)||0),0);\n  const balanced = Math.abs(total-100) \u003c 0.05;\n  return `\u003cdiv class=\"formula-balance\" id=\"formulaBalance\"\u003e\n    \u003cdiv\u003e\u003cdiv class=\"fb-label\"\u003eFormula Percentage\u003c\/div\u003e\u003cdiv class=\"fb-value\" id=\"fb-value\"\u003e${total.toFixed(1)}%\u003c\/div\u003e\u003c\/div\u003e\n    \u003cdiv class=\"fb-status\" id=\"fb-status\" style=\"background:${balanced?'rgba(22,132,71,.14)':'rgba(255,191,0,.18)'}; color:${balanced?'var(--meadow)':'#B38300'};\"\u003e${balanced?icon('check')+' Formula Balanced':'\\u26a0 Must equal 100%'}\u003c\/div\u003e\n  \u003c\/div\u003e`;\n}\n\nfunction ingredientRowHTML(p, row){\n  const sym = CURRENCIES[state.currency].symbol;\n  const unitOpts = Object.keys(ALL_UNITS);\n  return `\n  \u003cdiv class=\"item-row\" data-ing-id=\"${row.id}\"\u003e\n    \u003cdiv class=\"item-row-head\"\u003e\n      \u003cdiv class=\"ingredient-search\" style=\"flex:1;\"\u003e\n        \u003cinput class=\"field-plain ing-name-input\" data-ing-id=\"${row.id}\" value=\"${escapeAttr(row.name)}\" placeholder=\"Search or type ingredient name\\u2026\" autocomplete=\"off\" \/\u003e\n        \u003cdiv class=\"ingredient-suggest\" id=\"suggest-${row.id}\"\u003e\u003c\/div\u003e\n      \u003c\/div\u003e\n      \u003cbutton class=\"item-row-remove\" data-action=\"remove-ingredient\" data-id=\"${row.id}\"\u003e${icon('trash')}\u003c\/button\u003e\n    \u003c\/div\u003e\n    \u003cdiv class=\"item-row-fields item-row-fields-compact\"\u003e\n      \u003cdiv class=\"mini-field\"\u003e\u003clabel\u003eQty Purchased\u003c\/label\u003e\u003cdiv class=\"input-wrap\"\u003e\u003cinput type=\"number\" min=\"0\" step=\"0.01\" data-ing=\"quantityPurchased\" data-id=\"${row.id}\" value=\"${row.quantityPurchased}\" \/\u003e\u003c\/div\u003e\u003c\/div\u003e\n      \u003cdiv class=\"mini-field\"\u003e\u003clabel\u003eUnit\u003c\/label\u003e\u003cdiv class=\"input-wrap\"\u003e\u003cselect data-ing=\"purchaseUnit\" data-id=\"${row.id}\"\u003e${unitOpts.map(u=\u003e`\u003coption value=\"${u}\" ${row.purchaseUnit===u?'selected':''}\u003e${u}\u003c\/option\u003e`).join('')}\u003c\/select\u003e\u003c\/div\u003e\u003c\/div\u003e\n      \u003cdiv class=\"mini-field\"\u003e\u003clabel\u003ePurchase Cost\u003c\/label\u003e\u003cdiv class=\"input-wrap\"\u003e\u003cspan class=\"prefix\"\u003e${sym}\u003c\/span\u003e\u003cinput type=\"number\" min=\"0\" step=\"1\" data-ing=\"purchaseCost\" data-id=\"${row.id}\" value=\"${row.purchaseCost}\" \/\u003e\u003c\/div\u003e\u003c\/div\u003e\n      ${p.formulaMode==='weight' ? `\n      \u003cdiv class=\"mini-field\"\u003e\u003clabel\u003eQty Used\u003c\/label\u003e\u003cdiv class=\"input-wrap\"\u003e\u003cinput type=\"number\" min=\"0\" step=\"0.01\" data-ing=\"quantityUsed\" data-id=\"${row.id}\" value=\"${row.quantityUsed}\" \/\u003e\u003c\/div\u003e\u003c\/div\u003e\n      \u003cdiv class=\"mini-field\"\u003e\u003clabel\u003eUsed Unit\u003c\/label\u003e\u003cdiv class=\"input-wrap\"\u003e\u003cselect data-ing=\"usedUnit\" data-id=\"${row.id}\"\u003e${unitOpts.map(u=\u003e`\u003coption value=\"${u}\" ${row.usedUnit===u?'selected':''}\u003e${u}\u003c\/option\u003e`).join('')}\u003c\/select\u003e\u003c\/div\u003e\u003c\/div\u003e\n      ` : `\n      \u003cdiv class=\"mini-field\"\u003e\u003clabel\u003e% of Ingredients\u003c\/label\u003e\u003cdiv class=\"input-wrap\"\u003e\u003cinput type=\"number\" min=\"0\" max=\"100\" step=\"0.1\" data-ing=\"percentage\" data-id=\"${row.id}\" value=\"${row.percentage}\" \/\u003e\u003cspan class=\"suffix\"\u003e%\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\n      `}\n    \u003c\/div\u003e\n    ${p.formulaMode==='percentage' ? `\u003cp class=\"hint\" style=\"margin-top:8px;\"\u003eQuantity of ingredient used: \u003cstrong id=\"ing-used-${row.id}\"\u003e0 ${row.purchaseUnit}\u003c\/strong\u003e\u003c\/p\u003e` : ''}\n    \u003cdiv class=\"item-row-cost\"\u003e\u003cspan\u003eIngredient Cost\u003c\/span\u003e\u003cspan class=\"v\" id=\"ing-cost-${row.id}\"\u003e${fmt(0)}\u003c\/span\u003e\u003c\/div\u003e\n  \u003c\/div\u003e`;\n}\nfunction packagingRowHTML(row){\n  const sym = CURRENCIES[state.currency].symbol;\n  return `\n  \u003cdiv class=\"item-row\" data-pkg-id=\"${row.id}\"\u003e\n    \u003cdiv class=\"item-row-head\"\u003e\n      \u003cinput class=\"field-plain\" style=\"flex:1;\" data-pkg=\"name\" data-id=\"${row.id}\" value=\"${escapeAttr(row.name)}\" placeholder=\"e.g. 50ml Bottle\" \/\u003e\n      \u003cbutton class=\"item-row-remove\" data-action=\"remove-packaging\" data-id=\"${row.id}\"\u003e${icon('trash')}\u003c\/button\u003e\n    \u003c\/div\u003e\n    \u003cdiv class=\"item-row-fields\" style=\"grid-template-columns:repeat(3,1fr);\"\u003e\n      \u003cdiv class=\"mini-field\"\u003e\u003clabel\u003eQty Purchased\u003c\/label\u003e\u003cdiv class=\"input-wrap\"\u003e\u003cinput type=\"number\" min=\"0\" step=\"1\" data-pkg=\"quantityPurchased\" data-id=\"${row.id}\" value=\"${row.quantityPurchased}\" \/\u003e\u003c\/div\u003e\u003c\/div\u003e\n      \u003cdiv class=\"mini-field\"\u003e\u003clabel\u003ePurchase Cost\u003c\/label\u003e\u003cdiv class=\"input-wrap\"\u003e\u003cspan class=\"prefix\"\u003e${sym}\u003c\/span\u003e\u003cinput type=\"number\" min=\"0\" step=\"1\" data-pkg=\"purchaseCost\" data-id=\"${row.id}\" value=\"${row.purchaseCost}\" \/\u003e\u003c\/div\u003e\u003c\/div\u003e\n      \u003cdiv class=\"mini-field\"\u003e\u003clabel\u003eQty Used\u003c\/label\u003e\u003cdiv class=\"input-wrap\"\u003e\u003cinput type=\"number\" min=\"0\" step=\"1\" data-pkg=\"quantityUsed\" data-id=\"${row.id}\" value=\"${row.quantityUsed}\" \/\u003e\u003c\/div\u003e\u003c\/div\u003e\n    \u003c\/div\u003e\n    \u003cdiv class=\"item-row-cost\"\u003e\u003cspan\u003ePackaging Cost\u003c\/span\u003e\u003cspan class=\"v\" id=\"pkg-cost-${row.id}\"\u003e${fmt(0)}\u003c\/span\u003e\u003c\/div\u003e\n  \u003c\/div\u003e`;\n}\nfunction manuRowHTML(row){\n  const sym = CURRENCIES[state.currency].symbol;\n  return `\n  \u003cdiv class=\"field\" data-manu-id=\"${row.id}\"\u003e\n    ${row.preset ? `\n    \u003clabel\u003e${escapeHtml(row.label)}\u003c\/label\u003e\n    \u003cdiv class=\"input-wrap\"\u003e\u003cspan class=\"prefix\"\u003e${sym}\u003c\/span\u003e\u003cinput type=\"number\" min=\"0\" step=\"1\" data-manu=\"amount\" data-id=\"${row.id}\" value=\"${row.amount}\" \/\u003e\u003c\/div\u003e\n    ` : `\n    \u003cdiv style=\"display:flex; gap:6px; align-items:flex-end;\"\u003e\n      \u003cdiv style=\"flex:1;\"\u003e\n        \u003cinput class=\"field-plain\" style=\"margin-bottom:6px;\" data-manu=\"label\" data-id=\"${row.id}\" value=\"${escapeAttr(row.label)}\" placeholder=\"Custom cost name\" \/\u003e\n        \u003cdiv class=\"input-wrap\"\u003e\u003cspan class=\"prefix\"\u003e${sym}\u003c\/span\u003e\u003cinput type=\"number\" min=\"0\" step=\"1\" data-manu=\"amount\" data-id=\"${row.id}\" value=\"${row.amount}\" \/\u003e\u003c\/div\u003e\n      \u003c\/div\u003e\n      \u003cbutton class=\"item-row-remove\" data-action=\"remove-manu-cost\" data-id=\"${row.id}\" style=\"margin-bottom:1px;\"\u003e${icon('trash')}\u003c\/button\u003e\n    \u003c\/div\u003e\n    `}\n  \u003c\/div\u003e`;\n}\n\nfunction attachProductListeners(p){\n  const form = document.getElementById('prodForm');\n\n  form.addEventListener('input', e=\u003e{\n    const path = e.target.dataset.path;\n    if(path){\n      const val = e.target.type==='number' ? (parseFloat(e.target.value)||0) : e.target.value;\n      setPath(p, path, val);\n      p.updatedAt = Date.now();\n      if(path==='name') document.getElementById('prodTitle').textContent = val || 'SABI LIBRARY';\n      updateComputedDisplays(p);\n      scheduleSave();\n      return;\n    }\n    const rowId = e.target.dataset.id;\n    if(e.target.dataset.ing){\n      const row = p.ingredients.find(r=\u003er.id===rowId);\n      if(row){\n        const field = e.target.dataset.ing;\n        row[field] = e.target.type==='number' ? (parseFloat(e.target.value)||0) : e.target.value;\n        p.updatedAt = Date.now();\n        updateComputedDisplays(p); scheduleSave();\n      }\n      return;\n    }\n    if(e.target.dataset.pkg){\n      const row = p.packaging.find(r=\u003er.id===rowId);\n      if(row){\n        row[e.target.dataset.pkg] = e.target.type==='number' ? (parseFloat(e.target.value)||0) : e.target.value;\n        p.updatedAt = Date.now(); updateComputedDisplays(p); scheduleSave();\n      }\n      return;\n    }\n    if(e.target.dataset.manu){\n      const row = p.manufacturingCosts.find(r=\u003er.id===rowId);\n      if(row){\n        row[e.target.dataset.manu] = e.target.type==='number' ? (parseFloat(e.target.value)||0) : e.target.value;\n        p.updatedAt = Date.now(); updateComputedDisplays(p); scheduleSave();\n      }\n      return;\n    }\n    if(e.target.classList.contains('ing-name-input')){\n      const row = p.ingredients.find(r=\u003er.id===e.target.dataset.ingId);\n      if(row){ row.name = e.target.value; row.ingredientId = null; p.updatedAt = Date.now(); scheduleSave(); }\n      renderIngredientSuggest(e.target.dataset.ingId, e.target.value);\n    }\n  });\n\n  form.addEventListener('focusin', e=\u003e{\n    if(e.target.classList.contains('ing-name-input')){\n      renderIngredientSuggest(e.target.dataset.ingId, e.target.value);\n    }\n  });\n  document.addEventListener('click', e=\u003e{\n    if(!e.target.closest('.ingredient-search')){\n      document.querySelectorAll('.ingredient-suggest').forEach(s=\u003es.classList.remove('open'));\n    }\n  });\n\n  form.querySelectorAll('[data-action=\"set-segment\"]').forEach(btn=\u003e{\n    btn.addEventListener('click', ()=\u003e{\n      const path = btn.dataset.path, value = btn.dataset.value;\n      setPath(p, path, value);\n      p.updatedAt = Date.now();\n      scheduleSave();\n      renderProduct();\n    });\n  });\n}\n\nfunction setPath(obj, path, value){\n  const keys = path.split('.'); let o = obj;\n  for(let i=0;i\u003ckeys.length-1;i++) o = o[keys[i]];\n  o[keys[keys.length-1]] = value;\n}\n\nfunction renderIngredientSuggest(rowId, query){\n  const box = document.getElementById('suggest-'+rowId);\n  if(!box) return;\n  const q = (query||'').toLowerCase().trim();\n  const matches = Object.values(state.ingredientLibrary).filter(i=\u003e!q || i.name.toLowerCase().includes(q)).slice(0,8);\n  let html = matches.map(i=\u003e`\u003cbutton type=\"button\" data-action=\"pick-ingredient\" data-row=\"${rowId}\" data-lib=\"${i.id}\"\u003e${escapeHtml(i.name)} \u003cspan style=\"color:var(--text-faint); font-weight:600;\"\u003e(${i.defaultUnit})\u003c\/span\u003e\u003c\/button\u003e`).join('');\n  if(q){ html += `\u003cbutton type=\"button\" class=\"create-new\" data-action=\"create-ingredient\" data-row=\"${rowId}\" data-name=\"${escapeAttr(query)}\"\u003e${icon('plus')} Create \"${escapeHtml(query)}\"\u003c\/button\u003e`; }\n  box.innerHTML = html || `\u003cdiv style=\"padding:12px 14px; font-size:12.5px; color:var(--text-muted);\"\u003eType to search or create an ingredient.\u003c\/div\u003e`;\n  box.classList.add('open');\n}\n\n\/* ============ ADD \/ REMOVE ROWS ============ *\/\nfunction addIngredient(){\n  const p = state.products[activeProductId]; if(!p) return;\n  p.ingredients.push(defaultIngredientRow()); p.updatedAt=Date.now(); scheduleSave(); renderProduct();\n}\nfunction removeIngredient(id){\n  const p = state.products[activeProductId]; if(!p) return;\n  p.ingredients = p.ingredients.filter(r=\u003er.id!==id); p.updatedAt=Date.now(); scheduleSave(); renderProduct();\n}\nfunction addPackaging(){\n  const p = state.products[activeProductId]; if(!p) return;\n  p.packaging.push(defaultPackagingRow()); p.updatedAt=Date.now(); scheduleSave(); renderProduct();\n}\nfunction removePackaging(id){\n  const p = state.products[activeProductId]; if(!p) return;\n  p.packaging = p.packaging.filter(r=\u003er.id!==id); p.updatedAt=Date.now(); scheduleSave(); renderProduct();\n}\nfunction addManuCost(){\n  const p = state.products[activeProductId]; if(!p) return;\n  p.manufacturingCosts.push({ id:uid('mc'), label:'', amount:0, preset:false });\n  p.updatedAt=Date.now(); scheduleSave(); renderProduct();\n}\nfunction removeManuCost(id){\n  const p = state.products[activeProductId]; if(!p) return;\n  p.manufacturingCosts = p.manufacturingCosts.filter(r=\u003er.id!==id); p.updatedAt=Date.now(); scheduleSave(); renderProduct();\n}\nfunction pickIngredient(rowId, libId){\n  const p = state.products[activeProductId]; if(!p) return;\n  const row = p.ingredients.find(r=\u003er.id===rowId);\n  const lib = state.ingredientLibrary[libId];\n  if(row \u0026\u0026 lib){\n    row.name = lib.name; row.ingredientId = lib.id;\n    row.purchaseUnit = lib.defaultUnit; row.usedUnit = lib.defaultUnit;\n  }\n  p.updatedAt = Date.now(); scheduleSave(); renderProduct();\n}\nfunction createIngredientFromRow(rowId, name){\n  if(!name || !name.trim()) return;\n  const id = uid('lib');\n  state.ingredientLibrary[id] = { id, name:name.trim(), defaultUnit:'g' };\n  pickIngredient(rowId, id);\n}\n\n\/* ============ COMPUTED DISPLAY UPDATE (live, no re-render) ============ *\/\nfunction updateComputedDisplays(p){\n  const c = computeProduct(p);\n  const set = (id,val)=\u003e{ const e=document.getElementById(id); if(e) e.textContent = val; };\n\n  c.ingredientCalcs.forEach(({row,cost,usedBase})=\u003e{\n    set('ing-cost-'+row.id, fmt(cost));\n    if(p.formulaMode==='percentage'){\n      const displayUnit = row.purchaseUnit;\n      const displayValue = usedBase \/ (ALL_UNITS[displayUnit] || 1);\n      set('ing-used-'+row.id, fmtNum(displayValue, displayValue \u003c 1 ? 3 : 2) + ' ' + displayUnit);\n    }\n  });\n  p.packaging.forEach(row=\u003e set('pkg-cost-'+row.id, fmt(computePackagingCost(row))) );\n\n  if(p.formulaMode==='percentage'){\n    const total = c.formulaPercentTotal;\n    const balanced = Math.abs(total-100) \u003c 0.05;\n    set('fb-value', total.toFixed(1)+'%');\n    const statusEl = document.getElementById('fb-status');\n    if(statusEl){\n      statusEl.style.background = balanced ? 'rgba(22,132,71,.14)' : 'rgba(255,191,0,.18)';\n      statusEl.style.color = balanced ? 'var(--meadow)' : '#B38300';\n      statusEl.innerHTML = balanced ? icon('check')+' Formula Balanced' : '\\u26a0 Must equal 100%';\n    }\n  }\n\n  set('res-raw', fmt(c.rawMaterialCost));\n  set('res-pkg', fmt(c.packagingCost));\n  set('res-manu', fmt(c.manufacturingCost));\n  set('res-total', fmt(c.totalBatchCost));\n  set('res-perbase', fmt(c.costPerBaseUnit));\n  set('res-perunit', fmt(c.costPerUnit));\n  set('res-units', p.unitsProduced);\n\n  set('sim-sellingPrice', fmt(c.sellingPrice));\n  set('sim-profit', fmt(c.profit));\n  set('sim-margin', fmtPct(c.marginPct));\n  set('sim-markup', fmtPct(c.markupPct));\n  set('sim-revenue', fmt(c.expectedRevenue));\n}\n\n\/* ============ SETTINGS ============ *\/\nfunction renderSettings(){\n  const currencyOpts = Object.keys(CURRENCIES).map(k=\u003e`\u003coption value=\"${k}\" ${state.currency===k?'selected':''}\u003e${CURRENCIES[k].symbol} (${k})\u003c\/option\u003e`).join('');\n  document.getElementById('view-settings').innerHTML = `\n    \u003cdiv class=\"section-head\"\u003e\u003cspan class=\"eyebrow\" style=\"color:var(--forest);\"\u003ePreferences\u003c\/span\u003e\u003ch2\u003eSettings\u003c\/h2\u003e\u003cp\u003eApplies across every saved product.\u003c\/p\u003e\u003c\/div\u003e\n    \u003cdiv class=\"grid grid-2\"\u003e\n      \u003cdiv class=\"card pad-top\"\u003e\n        \u003cdiv class=\"chip\" style=\"background:var(--forest);\"\u003e${state.theme==='dark'?icon('sun'):icon('moon')}\u003c\/div\u003e\n        \u003cspan class=\"eyebrow\" style=\"color:var(--forest);\"\u003eAppearance\u003c\/span\u003e\u003ch3\u003eDark mode\u003c\/h3\u003e\n        \u003clabel class=\"toggle ${state.theme==='dark'?'on':''}\" id=\"themeToggleSettings\" style=\"margin-top:12px;\"\u003e\u003cspan class=\"track\"\u003e\u003cspan class=\"thumb\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003cspan class=\"label\"\u003eUse dark theme\u003c\/span\u003e\u003c\/label\u003e\n      \u003c\/div\u003e\n      \u003cdiv class=\"card pad-top\"\u003e\n        \u003cdiv class=\"chip\" style=\"background:var(--sunshine);\"\u003e${icon('wallet')}\u003c\/div\u003e\n        \u003cspan class=\"eyebrow\" style=\"color:#B38300;\"\u003eCurrency\u003c\/span\u003e\u003ch3\u003eDisplay currency\u003c\/h3\u003e\n        \u003cselect class=\"select-sm\" id=\"settingsCurrency\" style=\"width:100%; margin-top:12px;\"\u003e${currencyOpts}\u003c\/select\u003e\n      \u003c\/div\u003e\n      \u003cdiv class=\"card pad-top\"\u003e\n        \u003cdiv class=\"chip\" style=\"background:var(--coral);\"\u003e${icon('archive')}\u003c\/div\u003e\n        \u003cspan class=\"eyebrow\" style=\"color:var(--coral);\"\u003eData\u003c\/span\u003e\u003ch3\u003eBackup \u0026amp; reset\u003c\/h3\u003e\n        \u003cdiv class=\"btn-row\" style=\"margin-top:12px;\"\u003e\n          \u003cbutton class=\"btn btn-ghost btn-sm\" id=\"exportAllBtn\"\u003e${icon('download')} Export all (JSON)\u003c\/button\u003e\n          \u003cbutton class=\"btn btn-ghost btn-sm\" id=\"importAllBtn\"\u003eImport JSON\u003c\/button\u003e\n          \u003cbutton class=\"btn btn-danger btn-sm\" id=\"clearAllBtn\"\u003e${icon('trash')} Clear all data\u003c\/button\u003e\n        \u003c\/div\u003e\n        \u003cinput type=\"file\" id=\"importFile\" accept=\"application\/json\" style=\"display:none;\" \/\u003e\n      \u003c\/div\u003e\n    \u003c\/div\u003e\n  `;\n  document.getElementById('themeToggleSettings').addEventListener('click', toggleTheme);\n  document.getElementById('settingsCurrency').addEventListener('change', e=\u003e{ state.currency=e.target.value; scheduleSave(); renderTopbarActions(); });\n  document.getElementById('exportAllBtn').addEventListener('click', ()=\u003e{\n    const blob = new Blob([JSON.stringify(state,null,2)], {type:'application\/json'});\n    const url = URL.createObjectURL(blob); const a=document.createElement('a');\n    a.href=url; a.download='sabi-formulator-backup.json'; a.click(); URL.revokeObjectURL(url);\n    toast('Backup downloaded');\n  });\n  document.getElementById('importAllBtn').addEventListener('click', ()=\u003edocument.getElementById('importFile').click());\n  document.getElementById('importFile').addEventListener('change', e=\u003e{\n    const file = e.target.files[0]; if(!file) return;\n    const reader = new FileReader();\n    reader.onload = evt=\u003e{\n      try{ const parsed = JSON.parse(evt.target.result);\n        if(parsed \u0026\u0026 parsed.products){ state=parsed; persist(); toast('Backup imported'); applyTheme(); showView('dashboard'); }\n        else toast('That file doesn\\u2019t look like a valid backup');\n      }catch(err){ toast('Couldn\\u2019t read that file'); }\n    };\n    reader.readAsText(file);\n  });\n  document.getElementById('clearAllBtn').addEventListener('click', ()=\u003e{\n    openModal({ title:'Clear all data?', body:'\u003cp\u003eThis deletes every saved product and ingredient. This can\\u2019t be undone \\u2014 export a backup first if you want to keep a copy.\u003c\/p\u003e',\n      actions:`\u003cbutton class=\"btn btn-ghost\" data-action=\"cancel-modal\"\u003eCancel\u003c\/button\u003e\u003cbutton class=\"btn btn-danger\" id=\"confirmClearAll\"\u003eClear everything\u003c\/button\u003e` });\n    document.getElementById('confirmClearAll').addEventListener('click', ()=\u003e{\n      state = { theme:state.theme, currency:state.currency, activeProductId:null, products:{}, ingredientLibrary:{} };\n      persist(); closeModal(); toast('All data cleared'); showView('dashboard');\n    });\n  });\n}\n\n\/* ============ HELP ============ *\/\nfunction renderHelp(){\n  const items = [\n    ['Product Information','Batch size is the total amount you\\u2019re producing (e.g. 5kg). Unit size is how big each finished item is (e.g. 100g jar). Units produced is how many finished items that batch makes.'],\n    ['Ingredients','Search your Ingredient Library or type a new name. Enter how much you bought and what it cost \\u2014 the calculator works out your true cost per gram or ml, then applies it to how much you actually used.'],\n    ['Weight vs Percentage Mode','Weight Mode: type exactly how much of each ingredient you used. Percentage Mode: type what % of your total ingredients each one makes up \\u2014 useful when formulating by ratio. Percentages must total 100% to be balanced.'],\n    ['Packaging','Bottles, jars, labels, boxes \\u2014 anything you buy in bulk and use some of per batch. Cost is calculated the same way: cost per item \\u00d7 quantity used.'],\n    ['Manufacturing Costs','Everything else it costs to actually produce this batch \\u2014 labour, electricity, water, and more. Add custom costs for anything not listed.'],\n    ['Results Dashboard','Your true cost \\u2014 raw materials + packaging + manufacturing, broken down per batch, per gram\/ml, and per finished unit.'],\n    ['Pricing Simulation','Optional. This tool is built for costing, not pricing strategy \\u2014 use this section only to sanity-check a price against your real cost.'],\n    ['Ingredient Library','Only stores ingredient names and default units \\u2014 never purchase prices, since raw material costs change constantly. Every formulation asks for the latest price.'],\n    ['Auto-save','Every change saves instantly to this browser. Nothing is sent anywhere else. Export a backup from Settings periodically.'],\n  ];\n  document.getElementById('view-help').innerHTML = `\n    \u003cdiv class=\"section-head\"\u003e\u003cspan class=\"eyebrow\" style=\"color:var(--pink);\"\u003eGuide\u003c\/span\u003e\u003ch2\u003eHelp\u003c\/h2\u003e\u003cp\u003eWhat each section means and how the numbers are calculated.\u003c\/p\u003e\u003c\/div\u003e\n    ${items.map(([t,d])=\u003e`\u003cdetails class=\"help-item\" style=\"border:1px solid var(--border); border-radius:var(--r-md); padding:14px 16px; background:var(--surface); margin-bottom:10px;\"\u003e\u003csummary style=\"cursor:pointer; font-weight:700; font-size:14px; list-style:none; display:flex; justify-content:space-between; align-items:center;\"\u003e${t} ${icon('info')}\u003c\/summary\u003e\u003cp style=\"margin:10px 0 0; font-size:13.5px; color:var(--text-muted); line-height:1.6;\"\u003e${d}\u003c\/p\u003e\u003c\/details\u003e`).join('')}\n  `;\n}\n\n\/* ============ THEME ============ *\/\nfunction toggleTheme(){\n  state.theme = state.theme==='dark' ? 'light' : 'dark';\n  applyTheme(); scheduleSave(); renderTopbarActions();\n  if(currentView==='settings') renderSettings();\n}\nfunction applyTheme(){ document.documentElement.setAttribute('data-theme', state.theme); }\n\nfunction reportBrandName(){\n  if(personalDashboardName){\n    const stripped = personalDashboardName.replace(\/'s\\s+Formulator\\s+Dashboard$\/i,'').trim();\n    return stripped || 'Sabi Library';\n  }\n  return 'Sabi Library';\n}\n\n\/* ============ EXPORTS ============ *\/\nfunction exportPDF(){\n  const p = state.products[activeProductId]; if(!p){ toast('Open a product first'); return; }\n  if(!window.jspdf){ toast('PDF library still loading, try again in a moment'); return; }\n  const c = computeProduct(p);\n  const { jsPDF } = window.jspdf;\n  const doc = new jsPDF();\n  const brandName = reportBrandName();\n  doc.setFillColor(0,59,44); doc.rect(0,0,210,32,'F');\n  doc.setTextColor(255,243,227); doc.setFontSize(18); doc.text(brandName, 14, 15);\n  doc.setFontSize(10); doc.setTextColor(255,243,227); doc.text(\"FORMULATOR'S COSTING REPORT\", 14, 23);\n  doc.setFontSize(9.5); doc.setTextColor(255,191,0); doc.text('SABI LIBRARY', 196, 13, {align:'right'});\n  let y = 44;\n  doc.setTextColor(8,37,29); doc.setFontSize(15); doc.text(p.name||'Untitled formulation', 14, y); y+=7;\n  doc.setFontSize(9.5); doc.setTextColor(110,110,110);\n  doc.text(`Batch: ${fmtNum(p.batchSize.value)} ${p.batchSize.unit}    Units: ${p.unitsProduced}    Date: ${new Date().toLocaleDateString()}`, 14, y); y+=10;\n  function sectionTitle(t){ doc.setFontSize(10.5); doc.setTextColor(22,132,71); doc.text(t.toUpperCase(),14,y); y+=5.5; doc.setDrawColor(230,230,230); doc.line(14,y-3,196,y-3); }\n  function row(label,value){ doc.setFontSize(10); doc.setTextColor(8,37,29); doc.text(String(label),14,y); doc.text(String(value),196,y,{align:'right'}); y+=6.2; if(y\u003e275){ doc.addPage(); y=20; } }\n  sectionTitle('Ingredients');\n  c.ingredientCalcs.forEach(({row:r,cost})=\u003e row(r.name||'Ingredient', fmt(cost)));\n  row('Subtotal \\u2014 Raw Material Cost', fmt(c.rawMaterialCost)); y+=3;\n  sectionTitle('Packaging');\n  p.packaging.forEach(r=\u003e row(r.name||'Packaging item', fmt(computePackagingCost(r))));\n  row('Subtotal \\u2014 Packaging Cost', fmt(c.packagingCost)); y+=3;\n  sectionTitle('Manufacturing Costs');\n  p.manufacturingCosts.forEach(r=\u003e row(r.label||'Cost', fmt(r.amount)));\n  row('Subtotal \\u2014 Manufacturing Cost', fmt(c.manufacturingCost)); y+=3;\n  sectionTitle('Results');\n  row('Total Batch Cost', fmt(c.totalBatchCost));\n  row(`Cost Per ${baseLabel(p.batchSize.unit)}`, fmt(c.costPerBaseUnit));\n  row('Cost Per Unit', fmt(c.costPerUnit));\n  row('Recommended Selling Price', fmt(c.sellingPrice));\n  row('Profit \/ Unit', fmt(c.profit));\n  row('Profit Margin', fmtPct(c.marginPct));\n  doc.save(`${(p.name||'formulation').replace(\/[^a-z0-9]+\/gi,'-')}-costing-report.pdf`);\n  toast('PDF exported');\n}\nfunction exportExcel(){\n  const p = state.products[activeProductId]; if(!p){ toast('Open a product first'); return; }\n  if(!window.XLSX){ toast('Excel library still loading, try again in a moment'); return; }\n  const c = computeProduct(p);\n  const rows = [\n    [`${reportBrandName()} \\u2014 Formulator's Costing Report`], ['Powered by Sabi Library'], [],\n    ['Product', p.name||''], ['Batch Size', `${p.batchSize.value} ${p.batchSize.unit}`], ['Units Produced', p.unitsProduced], ['Date', new Date().toLocaleDateString()], [],\n    ['INGREDIENTS'],\n    ...c.ingredientCalcs.map(({row:r,cost})=\u003e[r.name||'Ingredient', cost]),\n    ['Subtotal \\u2014 Raw Material Cost', c.rawMaterialCost], [],\n    ['PACKAGING'],\n    ...p.packaging.map(r=\u003e[r.name||'Packaging item', computePackagingCost(r)]),\n    ['Subtotal \\u2014 Packaging Cost', c.packagingCost], [],\n    ['MANUFACTURING COSTS'],\n    ...p.manufacturingCosts.map(r=\u003e[r.label||'Cost', r.amount]),\n    ['Subtotal \\u2014 Manufacturing Cost', c.manufacturingCost], [],\n    ['RESULTS'],\n    ['Total Batch Cost', c.totalBatchCost],\n    [`Cost Per ${baseLabel(p.batchSize.unit)}`, c.costPerBaseUnit],\n    ['Cost Per Unit', c.costPerUnit],\n    ['Recommended Selling Price', c.sellingPrice],\n    ['Profit \/ Unit', c.profit],\n    ['Profit Margin %', c.marginPct.toFixed(1)],\n  ];\n  const ws = XLSX.utils.aoa_to_sheet(rows); ws['!cols']=[{wch:34},{wch:18}];\n  const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Costing');\n  XLSX.writeFile(wb, `${(p.name||'formulation').replace(\/[^a-z0-9]+\/gi,'-')}-costing.xlsx`);\n  toast('Excel file exported');\n}\nfunction printQuote(){\n  const p = state.products[activeProductId]; if(!p){ toast('Open a product first'); return; }\n  const c = computeProduct(p);\n  let html = `\n    \u003cdiv class=\"p-head\"\u003e\u003cdiv\u003e\u003cdiv class=\"wm\"\u003e${escapeHtml(reportBrandName())}\u003c\/div\u003e\u003cdiv class=\"eb\"\u003eFormulator's Costing Report\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"brand-tag\"\u003eSABI LIBRARY\u003c\/div\u003e\u003c\/div\u003e\n    \u003ch2 style=\"margin:0 0 4px;\"\u003e${escapeHtml(p.name)||'Untitled formulation'}\u003c\/h2\u003e\n    \u003cp style=\"color:#666;font-size:12px;margin:0 0 16px;\"\u003eBatch: ${fmtNum(p.batchSize.value)} ${p.batchSize.unit} \\u00b7 Units: ${p.unitsProduced} \\u00b7 ${new Date().toLocaleDateString()}\u003c\/p\u003e\n    \u003cdiv class=\"p-sec\"\u003eIngredients\u003c\/div\u003e\n    ${c.ingredientCalcs.map(({row:r,cost})=\u003e`\u003cdiv class=\"p-row\"\u003e\u003cspan\u003e${escapeHtml(r.name)||'Ingredient'}\u003c\/span\u003e\u003cspan\u003e${fmt(cost)}\u003c\/span\u003e\u003c\/div\u003e`).join('')}\n    \u003cdiv class=\"p-row total\"\u003e\u003cspan\u003eRaw Material Cost\u003c\/span\u003e\u003cspan\u003e${fmt(c.rawMaterialCost)}\u003c\/span\u003e\u003c\/div\u003e\n    \u003cdiv class=\"p-sec\"\u003ePackaging\u003c\/div\u003e\n    ${p.packaging.map(r=\u003e`\u003cdiv class=\"p-row\"\u003e\u003cspan\u003e${escapeHtml(r.name)||'Packaging item'}\u003c\/span\u003e\u003cspan\u003e${fmt(computePackagingCost(r))}\u003c\/span\u003e\u003c\/div\u003e`).join('')}\n    \u003cdiv class=\"p-row total\"\u003e\u003cspan\u003ePackaging Cost\u003c\/span\u003e\u003cspan\u003e${fmt(c.packagingCost)}\u003c\/span\u003e\u003c\/div\u003e\n    \u003cdiv class=\"p-sec\"\u003eManufacturing Costs\u003c\/div\u003e\n    ${p.manufacturingCosts.map(r=\u003e`\u003cdiv class=\"p-row\"\u003e\u003cspan\u003e${escapeHtml(r.label)||'Cost'}\u003c\/span\u003e\u003cspan\u003e${fmt(r.amount)}\u003c\/span\u003e\u003c\/div\u003e`).join('')}\n    \u003cdiv class=\"p-row total\"\u003e\u003cspan\u003eManufacturing Cost\u003c\/span\u003e\u003cspan\u003e${fmt(c.manufacturingCost)}\u003c\/span\u003e\u003c\/div\u003e\n    \u003cdiv class=\"p-sec\"\u003eResults\u003c\/div\u003e\n    \u003cdiv class=\"p-row total\"\u003e\u003cspan\u003eTotal Batch Cost\u003c\/span\u003e\u003cspan\u003e${fmt(c.totalBatchCost)}\u003c\/span\u003e\u003c\/div\u003e\n    \u003cdiv class=\"p-row\"\u003e\u003cspan\u003eCost Per ${baseLabel(p.batchSize.unit)}\u003c\/span\u003e\u003cspan\u003e${fmt(c.costPerBaseUnit)}\u003c\/span\u003e\u003c\/div\u003e\n    \u003cdiv class=\"p-row total\"\u003e\u003cspan\u003eCost Per Unit\u003c\/span\u003e\u003cspan\u003e${fmt(c.costPerUnit)}\u003c\/span\u003e\u003c\/div\u003e\n  `;\n  document.getElementById('printArea').innerHTML = html;\n  window.print();\n}\n\n\/* ============ GLOBAL CLICK DELEGATION ============ *\/\ndocument.body.addEventListener('click', e=\u003e{\n  const t = e.target.closest('[data-action]');\n  if(!t) return;\n  const action = t.dataset.action;\n  const id = t.dataset.id;\n\n  switch(action){\n    case 'toggle-drawer': openDrawer(); break;\n    case 'close-drawer': closeDrawer(); break;\n    case 'nav-dashboard': showView('dashboard'); break;\n    case 'nav-new': createNewProduct(); break;\n    case 'nav-saved': showView('saved'); break;\n    case 'nav-library': showView('library'); break;\n    case 'nav-settings': showView('settings'); break;\n    case 'nav-help': showView('help'); break;\n    case 'open-product': if(!e.target.closest('.pactions')) openProduct(id); break;\n    case 'rename-product': renameProduct(id); break;\n    case 'duplicate-product': duplicateProduct(id); break;\n    case 'archive-product': archiveProduct(id, true); break;\n    case 'unarchive-product': archiveProduct(id, false); break;\n    case 'delete-product': deleteProduct(id); break;\n    case 'confirm-delete':\n      delete state.products[id];\n      if(activeProductId===id) activeProductId=null;\n      persist(); closeModal(); toast('Product deleted'); refreshList();\n      if(currentView==='product' \u0026\u0026 !state.products[activeProductId]) showView('dashboard');\n      break;\n    case 'confirm-rename': {\n      const input = document.getElementById('renameInput');\n      const val = input ? input.value.trim() : '';\n      const prod = state.products[id];\n      if(prod){ prod.name=val; prod.updatedAt=Date.now(); scheduleSave(); }\n      closeModal(); refreshList();\n      if(currentView==='product'){ const el2=document.getElementById('prodTitle'); if(el2) el2.textContent = val || 'SABI LIBRARY'; }\n      break;\n    }\n    case 'cancel-modal': closeModal(); break;\n    case 'toggle-theme': toggleTheme(); break;\n    case 'export-pdf': exportPDF(); break;\n    case 'export-excel': exportExcel(); break;\n    case 'print-quote': printQuote(); break;\n    case 'add-ingredient': addIngredient(); break;\n    case 'remove-ingredient': removeIngredient(id); break;\n    case 'add-packaging': addPackaging(); break;\n    case 'remove-packaging': removePackaging(id); break;\n    case 'add-manu-cost': addManuCost(); break;\n    case 'remove-manu-cost': removeManuCost(id); break;\n    case 'pick-ingredient': pickIngredient(t.dataset.row, t.dataset.lib); break;\n    case 'create-ingredient': createIngredientFromRow(t.dataset.row, t.dataset.name); break;\n    case 'add-library-ingredient': libraryIngredientModal(null); break;\n    case 'edit-library-ingredient': libraryIngredientModal(state.ingredientLibrary[id]); break;\n    case 'delete-library-ingredient':\n      delete state.ingredientLibrary[id]; scheduleSave(); toast('Ingredient removed'); renderLibrary(); break;\n    case 'save-library-ingredient': saveLibraryIngredient(id||null); break;\n  }\n});\n\n\/* ============ INIT ============ *\/\n\/* ============ ACCESS GATE ============ *\/\nconst ACCESS_CODE = '381204';\nconst ACCESS_KEY = 'sabi_formulator_access_v1';\nlet personalDashboardName = '';\n\nfunction hasAccess(){\n  try{\n    const raw = localStorage.getItem(ACCESS_KEY);\n    if(!raw) return false;\n    const parsed = JSON.parse(raw);\n    return !!(parsed \u0026\u0026 parsed.unlocked === true);\n  }catch(e){ return false; }\n}\nfunction grantAccess(data){\n  try{ localStorage.setItem(ACCESS_KEY, JSON.stringify({unlocked:true, ...data, unlockedAt:Date.now()})); }\n  catch(e){}\n}\nfunction isValidEmail(v){ return \/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$\/.test(v); }\nfunction attemptUnlock(){\n  const nameEl = document.getElementById('accessGateName');\n  const emailEl = document.getElementById('accessGateEmail');\n  const codeEl = document.getElementById('accessGateCode');\n  const dashEl = document.getElementById('accessGateDashboardName');\n  const errEl = document.getElementById('accessGateError');\n  const name = (nameEl.value||'').trim();\n  const email = (emailEl.value||'').trim();\n  const code = (codeEl.value||'').trim();\n  let dashboardName = (dashEl.value||'').trim();\n  if(!name){ errEl.textContent = 'Please enter your name.'; nameEl.focus(); return; }\n  if(!email || !isValidEmail(email)){ errEl.textContent = 'Please enter a valid email.'; emailEl.focus(); return; }\n  if(code !== ACCESS_CODE){ errEl.textContent = 'That code doesn\\u2019t look right. Check your purchase email.'; codeEl.focus(); return; }\n  if(!dashboardName){ dashboardName = name + '\\u2019s Formulator Dashboard'; }\n  errEl.textContent = '';\n  grantAccess({name, email, dashboardName});\n  document.getElementById('accessGate').style.display = 'none';\n  startApp();\n}\n\nfunction init(){\n  document.getElementById('accessGateSubmit').addEventListener('click', attemptUnlock);\n  ['accessGateName','accessGateEmail','accessGateCode','accessGateDashboardName'].forEach((id,i,arr)=\u003e{\n    document.getElementById(id).addEventListener('keydown', e=\u003e{\n      if(e.key==='Enter'){\n        if(i \u003c arr.length-1) document.getElementById(arr[i+1]).focus();\n        else attemptUnlock();\n      }\n    });\n  });\n\n  if(hasAccess()){\n    document.getElementById('accessGate').style.display = 'none';\n    startApp();\n  }\n  \/\/ else: gate stays visible, startApp() runs only after attemptUnlock() succeeds\n}\n\nfunction startApp(){\n  applyTheme();\n  document.getElementById('hamburgerBtn').innerHTML = icon('menu');\n  document.querySelectorAll('.drawer-link').forEach(l=\u003e{ l.innerHTML = icon(l.dataset.icon) + '\u003cspan\u003e'+l.textContent+'\u003c\/span\u003e'; });\n  try{\n    const raw = localStorage.getItem(ACCESS_KEY);\n    const parsed = raw ? JSON.parse(raw) : null;\n    if(parsed \u0026\u0026 parsed.name){\n      document.getElementById('drawerFoot').textContent = 'Hi ' + parsed.name + ' \\u2014 everything saves automatically to this browser.';\n    }\n    if(parsed \u0026\u0026 parsed.dashboardName){ personalDashboardName = parsed.dashboardName; }\n  }catch(e){}\n  activeProductId = state.activeProductId;\n  showView(activeProductId \u0026\u0026 state.products[activeProductId] ? 'product' : 'dashboard');\n}\ninit();\n\n})();\n\u003c\/script\u003e\n\n\u003c\/body\u003e","brand":"SABI LIBRARY","offers":[{"title":"Default Title","offer_id":47268100276380,"sku":null,"price":10000.0,"currency_code":"NGN","in_stock":true}],"thumbnail_url":"\/\/cdn.shopify.com\/s\/files\/1\/0757\/0079\/7596\/files\/FC6FE70D-A71D-4EA5-B02C-563D516546FE_864773db-bd4b-4c92-a404-3a3ce2b9b77a.png?v=1785027127","url":"https:\/\/sabilibrary.com\/products\/formulator-calculator","provider":"SABI LIBRARY","version":"1.0","type":"link"}