" + '
' + '
' + '' + esc(window.I18N[window.LANG].content.heroEyebrow) + "" + "

" + heroTitle + "

" + '

' + esc(window.I18N[window.LANG].content.heroLede) + "

" + '" + '
' + heroStats + "
" + "
" + '
' + '
' + features + "
" + "
" + '
' + '

' + esc(window.I18N[window.LANG].content.calcTitle) + "

" + "

" + esc(window.I18N[window.LANG].content.calcSub) + "

" + '
' + "
" + '
' + '

' + esc(window.I18N[window.LANG].content.guideTitle) + "

" + "

" + esc(window.I18N[window.LANG].content.guideSub) + "

" + '
' + guide + "
" + "
" + '
' + '

' + esc(window.I18N[window.LANG].content.oilsStrip.title) + "

" + "

" + esc(window.I18N[window.LANG].content.oilsStrip.desc) + "

" + '' + esc(window.I18N[window.LANG].content.oilsStrip.cta) + "" + '
' + pills + "
" + "
" + '
' + '

' + esc(window.I18N[window.LANG].content.safety.title) + "

" + '
    ' + safetyBullets + "
" + "
" + "
" + '"; // language switcher dropdown toggle (bind document listener once) var langBtn = document.getElementById("langBtn"); var langMenuEl = document.getElementById("langMenu"); langBtn.addEventListener("click", function (e) { e.stopPropagation(); langMenuEl.classList.toggle("open"); }); if (!window._langMenuBound) { window._langMenuBound = true; document.addEventListener("click", function (e) { var langEl = e.target && e.target.closest ? e.target.closest("[data-lang]") : null; if (langEl) { e.preventDefault(); window.switchLanguage(langEl.getAttribute("data-lang")); return; } var btnEl = document.getElementById("langBtn"); var menuEl = document.getElementById("langMenu"); if (menuEl && !menuEl.contains(e.target) && btnEl && !btnEl.contains(e.target)) { menuEl.classList.remove("open"); } }); } } // In-place language switch used by the Blogger single-file version window.switchLanguage = function (lang) { if (!window.I18N[lang]) return; window.LANG = lang; document.documentElement.setAttribute("lang", lang); var i18n = window.I18N[lang]; document.documentElement.setAttribute("dir", i18n.dir); if (i18n.dir === "rtl") document.documentElement.classList.add("rtl"); else document.documentElement.classList.remove("rtl"); if (window.BLOGGER_MODE) { buildStaticPage(); appEl = document.getElementById("app"); renderApp(); renderStep(); } }; // Replace elements with data-i18n attribute using translations function applyTranslatedStatics() { var els = document.querySelectorAll("[data-i18n]"); for (var i = 0; i < els.length; i++) { var el = els[i]; var key = el.getAttribute("data-i18n"); el.textContent = T(key); } } // ---------- Layout ---------- function renderApp() { appEl.innerHTML = '
' + '
' + '
' + '
' + '' + '' + '
' + '
'; document.getElementById("btnBack").addEventListener("click", function () { if (currentStep > 1) { currentStep--; renderStep(); } }); document.getElementById("btnNext").addEventListener("click", function () { if (currentStep < 5) { currentStep++; } else { document.getElementById("btnPrint") && scrollToRecipe(); } renderStep(); }); } function renderStepsNav() { var el = document.getElementById("wizardSteps"); if (!el) return; var html = ""; for (var i = 1; i <= 5; i++) { var done = i < currentStep ? " done" : ""; var active = i === currentStep ? " active" : ""; html += '
' + '' + (i < currentStep ? "✓" : i) + "" + '' + esc(T("steps." + (i - 1))) + "" + "
"; } el.innerHTML = html; } // ---------- Step rendering ---------- function renderStep() { renderStepsNav(); var body = document.getElementById("wizardBody"); var nav = document.querySelector(".wizard-nav"); if (!body) return; switch (currentStep) { case 1: body.innerHTML = stepSoapType(); break; case 2: body.innerHTML = stepOils(); bindOils(); break; case 3: body.innerHTML = stepLye(); break; case 4: body.innerHTML = stepWater(); break; case 5: body.innerHTML = stepRecipe(); break; } document.getElementById("btnBack").textContent = currentStep === 1 ? "" : T("back"); document.getElementById("btnBack").style.visibility = currentStep === 1 ? "hidden" : "visible"; document.getElementById("btnNext").textContent = currentStep === 5 ? T("printRecipe") : T("next"); nav.style.display = currentStep === 5 ? "none" : "flex"; } function stepSoapType() { var types = ["solid", "liquid", "cream"]; var html = '

' + esc(T("step1Title")) + "

"; html += '
'; types.forEach(function (type) { var sel = state.soapType === type ? " selected" : ""; html += '"; }); html += "
"; return html; } function soapIcon(type) { return type === "solid" ? "🌾" : type === "liquid" ? "💧" : "🍧"; } function bindSoapType() { var cards = document.querySelectorAll("[data-soaptype]"); for (var i = 0; i < cards.length; i++) { cards[i].addEventListener("click", function () { state.soapType = this.getAttribute("data-soaptype"); applySoapDefaults(); renderStep(); }); } } function applySoapDefaults() { if (state.soapType === "liquid") { state.lyeType = "koh"; state.superfat = 0; } else if (state.soapType === "cream") { state.lyeType = "dual"; state.superfat = 5; state.kohPercent = 40; } else { state.lyeType = "naoh"; state.superfat = 5; } } // ---------- Step 2: Oils ---------- function stepOils() { var html = '

' + esc(T("oilsTitle")) + "

"; html += '
'; // Left: search + list html += '
'; html += '"; html += '
'; html += "
"; // Right: added oils html += '
'; html += '
' + '

' + esc(T("addedTitle")) + "

" + '" + "
"; html += '
'; html += "
"; html += "
"; if (state.usePercentages) { html += '
' + '" + '" + '" + '' + '' + unitLabel() + "" + "
"; } return html; } function unitOptions() { return '" + '" + '"; } function unitLabel() { return state.weightUnit === "g" ? T("units.gram").split(" ")[0] : state.weightUnit === "oz" ? T("units.ounce").split(" ")[0] : T("units.pound").split(" ")[0]; } function bindOils() { var input = document.getElementById("oilSearch"); input.addEventListener("input", function () { searchQuery = input.value.toLowerCase(); renderOilsList(); }); renderOilsList(); renderAdded(); var sel = document.getElementById("weightUnitSel"); if (sel) sel.addEventListener("change", function () { state.weightUnit = sel.value; renderStep(); }); var tb = document.getElementById("totalBatch"); if (tb) tb.addEventListener("input", function () { state.totalBatchWeight = parseFloat(tb.value) || 0; renderAdded(); }); var up = document.getElementById("usePct"); if (up) up.addEventListener("change", function () { state.usePercentages = up.checked; if (state.usePercentages) { state.oils.forEach(function (o) { o.amount = 10; }); } renderStep(); }); } function renderOilsList() { var el = document.getElementById("oilsList"); if (!el) return; var q = searchQuery; var list = window.OILS.filter(function (o) { if (!q) return true; return o.name.toLowerCase().indexOf(q) !== -1 || (o.sci || "").toLowerCase().indexOf(q) !== -1; }); if (list.length === 0) { el.innerHTML = '
' + esc(T("noOilsFound")) + "
"; return; } var html = ""; list.forEach(function (o) { var added = state.oils.some(function (e) { return e.slug === o.slug; }); html += '
' + '
' + '
' + esc(window.oilName(o)) + (o.vegan ? ' ' + esc(T("vegan")) + "" : "") + "
" + '
' + "INS " + esc(C.fmt(o.ins, 0)) + "" + "SAP " + esc(o.naoh.toFixed(3)) + "" + 'H ' + esc(C.fmt(o.hard, 0)) + "" + 'C ' + esc(C.fmt(o.cond, 0)) + "" + "
" + "
" + '" + "
"; }); el.innerHTML = html; var btns = el.querySelectorAll("[data-add]"); for (var i = 0; i < btns.length; i++) { btns[i].addEventListener("click", function () { var slug = this.getAttribute("data-add"); addOil(slug); }); } } function addOil(slug) { var exists = state.oils.some(function (e) { return e.slug === slug; }); if (exists) return; state.oils.push({ slug: slug, amount: state.usePercentages ? 10 : 100, unit: "g" }); renderOilsList(); renderAdded(); } function renderAdded() { var el = document.getElementById("addedBody"); if (!el) return; if (state.oils.length === 0) { el.innerHTML = '
' + esc(T("noOilsYet")) + "
"; return; } var html = ""; state.oils.forEach(function (e, idx) { var oil = window.OILS_BY_SLUG[e.slug]; if (!oil) return; html += '
' + '
' + '' + esc(window.oilName(oil)) + "" + '' + esc(oil.sci || "") + "" + "
" + '' + '' + (state.usePercentages ? "%" : unitLabel()) + "" + '' + "
"; }); // total var totalG = C.totals(state).weight; html += '
' + "" + esc(T("totalOilsPercent")) + "" + "" + esc(C.fmt(state.usePercentages ? totalPct() : C.fromGrams(totalG, state.weightUnit))) + " " + (state.usePercentages ? "%" : unitLabel()) + "" + "
"; el.innerHTML = html; var amts = el.querySelectorAll(".added-amt"); for (var i = 0; i < amts.length; i++) { amts[i].addEventListener("input", function () { var idx = parseInt(this.getAttribute("data-idx"), 10); state.oils[idx].amount = parseFloat(this.value) || 0; renderAddedTotal(); }); } var rm = el.querySelectorAll(".btn-remove"); for (var j = 0; j < rm.length; j++) { rm[j].addEventListener("click", function () { var idx = parseInt(this.getAttribute("data-idx"), 10); state.oils.splice(idx, 1); renderAdded(); renderOilsList(); }); } } function totalPct() { return state.oils.reduce(function (s, e) { return s + (parseFloat(e.amount) || 0); }, 0); } function renderAddedTotal() { var el = document.querySelector(".added-total strong"); if (!el) return; var totalG = C.totals(state).weight; el.textContent = C.fmt(state.usePercentages ? totalPct() : C.fromGrams(totalG, state.weightUnit)) + " " + (state.usePercentages ? "%" : unitLabel()); } // ---------- Step 3: Lye & Weight ---------- function stepLye() { var html = '

' + esc(T("lyeTitle")) + "

"; html += '
'; // weight unit html += '
" + '
' + '" + '" + '" + "
"; // lye type html += '
" + '
' + '' + '' + '" + "
"; if (state.lyeType === "dual") { html += '
' + '' + '
' + state.kohPercent + "%
"; } // purity html += '
" + '%
'; if (state.lyeType === "koh" || state.lyeType === "dual") { html += '
" + '%
'; } // superfat html += '
" + '%' + '
' + esc(T("superfatHint")) + "
"; // total oils summary html += '
' + esc(T("totalOilsPercent")) + "" + "" + esc(C.fmt(C.fromGrams(C.totals(state).weight, state.weightUnit))) + " " + unitLabel() + "
"; html += "
"; return html; } function bindLye() { var segs = document.querySelectorAll("[data-lye]"); for (var i = 0; i < segs.length; i++) { segs[i].addEventListener("click", function () { state.lyeType = this.getAttribute("data-lye"); renderStep(); }); } var us = document.querySelectorAll("[data-unit]"); for (var j = 0; j < us.length; j++) { us[j].addEventListener("click", function () { state.weightUnit = this.getAttribute("data-unit"); renderStep(); }); } var kp = document.getElementById("kohPct"); if (kp) kp.addEventListener("input", function () { state.kohPercent = parseInt(kp.value, 10); document.getElementById("kohPctVal").textContent = state.kohPercent; }); var np = document.getElementById("naohPurity"); if (np) np.addEventListener("input", function () { state.naohPurity = parseFloat(np.value) || 100; }); var kpu = document.getElementById("kohPurity"); if (kpu) kpu.addEventListener("input", function () { state.kohPurity = parseFloat(kpu.value) || 90; }); var sf = document.getElementById("superfat"); if (sf) sf.addEventListener("input", function () { state.superfat = parseFloat(sf.value) || 0; }); } // ---------- Step 4: Water ---------- function stepWater() { var html = '

' + esc(T("waterTitle")) + "

"; html += '
'; html += '
" + '
' + '" + '" + '" + "
"; if (state.waterMethod === 1) { html += '
" + '%
'; } else if (state.waterMethod === 2) { html += '
" + ' : 1
'; } else { html += '
" + '%
'; } html += '
" + '%' + '
' + esc(T("waterDiscountHint")) + "
"; html += "
"; return html; } function bindWater() { var segs = document.querySelectorAll("[data-wm]"); for (var i = 0; i < segs.length; i++) { segs[i].addEventListener("click", function () { state.waterMethod = parseInt(this.getAttribute("data-wm"), 10); renderStep(); }); } var wp = document.getElementById("waterOilPct"); if (wp) wp.addEventListener("input", function () { state.waterOilPercent = parseFloat(wp.value) || 38; }); var wr = document.getElementById("waterRatio"); if (wr) wr.addEventListener("input", function () { state.waterRatio = parseFloat(wr.value) || 2.2; }); var lc = document.getElementById("lyeConc"); if (lc) lc.addEventListener("input", function () { state.lyeConcentration = parseFloat(lc.value) || 33; }); var wd = document.getElementById("waterDisc"); if (wd) wd.addEventListener("input", function () { state.waterDiscount = parseFloat(wd.value) || 0; }); } // ---------- Step 5: Recipe ---------- function stepRecipe() { var r = C.calculate(state); var html = '
' + "

" + esc(T("recipeTitle")) + "

" + "

" + soapTypeChip() + "

" + '
' + '" + '" + "
"; html += '
'; html += '
' + "

" + esc(T("neededIngredients")) + "

" + ingredientTable(r) + "
"; html += '
' + "

" + esc(T("propertiesTitle")) + "

" + propertyBars(r) + '
' + '
INS' + esc(C.fmt(r.props.ins, 0)) + "" + '
' + '' + esc(T("insIdeal")) + "
" + '
' + esc(T("iodineTitle")) + "" + esc(C.fmt(r.props.iodine, 0)) + "" + '
' + '' + esc(T("iodineHint")) + "
" + "
"; html += '
' + "

" + esc(T("fattyAcidsTitle")) + "

" + fattyAcidBars(r) + "
"; html += '
' + "

" + esc(T("additivesTitle")) + "

" + additiveRows(r) + "
"; html += "
"; html += '
' + '' + "" + esc(T("safeWarning")) + "
"; html += '
' + esc(T("calcNote")) + "
"; setTimeout(function () { var pr = document.getElementById("btnPrint"); if (pr) pr.addEventListener("click", function () { window.print(); }); var rs = document.getElementById("btnReset"); if (rs) rs.addEventListener("click", function () { state = C.defaultState(); currentStep = 1; renderStep(); }); }, 0); return html; } function soapTypeChip() { return '' + esc(T("soapType." + state.soapType)) + "" + '' + (state.lyeType === "dual" ? "NaOH + KOH" : state.lyeType.toUpperCase()) + "" + '' + esc(T("superfat")) + ": " + state.superfat + "%"; } function ingredientTable(r) { var unit = state.weightUnit; var f = function (g) { return C.fmt(C.fromGrams(g, unit)); }; var totalOils = r.totals.weight; var lyeTotal = r.lye.total; var water = r.water; var soapW = r.soapWeight; var rows = ""; if (r.lye.naoh > 0) { rows += ingredientRow("NaOH (100% " + esc(T("lyePurity")) + ")", r.lye.naoh, totalOils, soapW, f); } if (r.lye.koh > 0) { rows += ingredientRow("KOH (" + state.kohPurity + "% " + esc(T("lyePurity")) + ")", r.lye.koh, totalOils, soapW, f); } rows += ingredientRow(T("waterNeeded"), water, totalOils, soapW, f); rows += ingredientRow(T("totalOilsPercent"), totalOils, totalOils, soapW, f); rows += '' + esc(T("soapWeight")) + "" + f(soapW) + "" + C.fmt(C.fromGrams(soapW, "oz")) + "" + C.fmt(C.fromGrams(soapW, "lb")) + "100%"; var headT = T("ingredientsTable"); return '
' + "" + "" + rows + "
" + esc(headT.ingredient) + "" + esc(headT.grams) + "" + esc(headT.ounces) + "" + esc(headT.pounds) + "" + esc(headT.oilPercent) + "" + esc(headT.batchPercent) + "
"; } function ingredientRow(label, g, oilsG, batchG, f) { return "" + label + "" + f(g) + "" + C.fmt(C.fromGrams(g, "oz")) + "" + C.fmt(C.fromGrams(g, "lb")) + "" + C.fmt(oilsG ? g / oilsG * 100 : 0, 1) + "%" + C.fmt(batchG ? g / batchG * 100 : 0, 1) + "%"; } function propertyBars(r) { var props = [ { key: "hardness", val: r.props.hardness }, { key: "cleansing", val: r.props.cleansing }, { key: "conditioning", val: r.props.conditioning }, { key: "bubbly", val: r.props.bubbly }, { key: "creamy", val: r.props.creamy }, { key: "longevity", val: r.props.longevity } ]; var labels = { hardness: T("propHardness"), cleansing: T("propCleansing"), conditioning: T("propConditioning"), bubbly: T("propBubbly"), creamy: T("propCreamy"), longevity: T("propLongevity") }; var html = ""; props.forEach(function (p) { var rec = C.RECOMMENDED[p.key]; var cls = p.val >= rec[0] && p.val <= rec[1] ? " good" : " warn"; html += '
' + '
' + esc(labels[p.key]) + "" + esc(C.fmt(p.val, 1)) + "
" + '
' + '
' + "
"; }); return '
' + html + "
"; } function fattyAcidBars(r) { var fa = r.fa; var order = ["lauric", "myristic", "palmitic", "stearic", "oleic", "linoleic", "linolenic", "ricinoleic"]; var html = ""; order.forEach(function (k) { html += '
' + '
' + esc(T("fa." + k)) + "" + esc(C.fmt(fa[k], 1)) + "%
" + '
' + "
"; }); return '
' + html + "
"; } function additiveRows(r) { var a = r.additives; var f = function (g) { return C.fmt(C.fromGrams(g, state.weightUnit)); }; var u = unitLabel(); return '
' + '
' + esc(T("addSodiumLactate")) + "" + f(a.sodiumLactate) + " " + u + "
" + '

' + esc(T("addSodiumLactateDesc")) + "

" + '
' + '
' + esc(T("addFragrance")) + "" + f(a.fragranceMin) + " – " + f(a.fragranceMax) + " " + u + "
" + '

' + esc(T("addFragranceDesc")) + "

" + '
' + '
' + esc(T("addCitric")) + "" + f(a.citricAcid) + " " + u + "
" + '

' + esc(T("addCitricDesc")) + "

" + '
' + '
' + esc(T("addEdta")) + "" + f(a.edta) + " " + u + "
" + '

' + esc(T("addEdtaDesc")) + "

" + '
' + '
' + esc(T("addRoe")) + "" + f(a.roe) + " " + u + "
" + '

' + esc(T("addRoeDesc")) + "

"; } function scrollToRecipe() { var el = document.querySelector(".recipe-head"); if (el) el.scrollIntoView({ behavior: "smooth", block: "start" }); } // Global click delegation for soap type buttons rendered inside step document.addEventListener("click", function (e) { var t = e.target.closest ? e.target.closest("[data-soaptype]") : null; if (t) { state.soapType = t.getAttribute("data-soaptype"); applySoapDefaults(); renderStep(); } }); // Rerender binds after each step render var origRenderStep = renderStep; renderStep = function () { origRenderStep(); if (currentStep === 1) bindSoapType(); if (currentStep === 3) bindLye(); if (currentStep === 4) bindWater(); }; document.addEventListener("DOMContentLoaded", init); if (document.readyState !== "loading") init(); })();