diff --git a/index.html b/index.html
index cb9aa19..fb5ffb4 100644
--- a/index.html
+++ b/index.html
@@ -385,7 +385,26 @@
}
.ing-remove:hover{color:var(--bad);background:rgba(232,80,63,.12);}
.ing-controls{display:flex;align-items:center;gap:14px;}
-.ing-controls input[type="range"]{flex:1;accent-color:var(--accent);height:6px;}
+.ing-controls input[type="range"]{
+ -webkit-appearance:none;appearance:none;
+ flex:1;accent-color:var(--accent);height:28px;cursor:pointer;
+ background:transparent;
+}
+.ing-controls input[type="range"]::-webkit-slider-runnable-track{
+ height:8px;border-radius:999px;background:rgba(255,255,255,.12);
+}
+.ing-controls input[type="range"]::-webkit-slider-thumb{
+ -webkit-appearance:none;appearance:none;width:20px;height:20px;
+ margin-top:-6px;border-radius:50%;background:var(--accent);
+ border:2px solid #23140c;box-shadow:0 0 0 2px rgba(240,90,40,.22);
+}
+.ing-controls input[type="range"]::-moz-range-track{
+ height:8px;border-radius:999px;background:rgba(255,255,255,.12);
+}
+.ing-controls input[type="range"]::-moz-range-thumb{
+ width:18px;height:18px;border-radius:50%;background:var(--accent);
+ border:2px solid #23140c;box-shadow:0 0 0 2px rgba(240,90,40,.22);
+}
.ing-num{
width:78px;background:var(--card);border:1px solid var(--border);color:var(--text);
padding:7px 8px;border-radius:var(--radius-sm);font-size:.9rem;text-align:right;font-weight:600;
@@ -1138,6 +1157,10 @@
========================================================= */
function clamp(v,min,max){ return Math.max(min, Math.min(max, v)); }
function round1(v){ return Math.round(v*10)/10; }
+function roundWhole(v){ return Math.round(Number(v) || 0); }
+function clampWholePct(value){
+ return clamp(roundWhole(value), 0, 100);
+}
function roundToQuarter(v){ return Math.round(v*4)/4; }
function formatQuarterValue(v){ return Number.isInteger(v) ? String(v) : String(v).replace(/0$/,""); }
function el(tag, cls, html){ const e=document.createElement(tag); if(cls) e.className=cls; if(html!==undefined) e.innerHTML=html; return e; }
@@ -1179,6 +1202,24 @@
function activeKeys(){ return Object.keys(blend).filter(k => blend[k] !== undefined); }
+function normalizeBlendPercentages(source, balanceTo100){
+ const normalized = {};
+ Object.keys(source || {}).forEach(key=>{
+ if(!INGREDIENTS[key]) return;
+ normalized[key] = clampWholePct(source[key]);
+ });
+ const keys = Object.keys(normalized);
+ if(balanceTo100 && keys.length){
+ const total = keys.reduce((sum,key)=> sum + normalized[key], 0);
+ const drift = 100 - total;
+ if(drift !== 0){
+ const adjustKey = keys.reduce((best,key)=> normalized[key] > normalized[best] ? key : best, keys[0]);
+ normalized[adjustKey] = clamp(normalized[adjustKey] + drift, 0, 100);
+ }
+ }
+ return normalized;
+}
+
function totalPercent(){
return activeKeys().reduce((sum,k)=> sum + (Number(blend[k])||0), 0);
}
@@ -1516,8 +1557,8 @@
''+
''+
''+
- ''+
- ''+
+ ''+
+ ''+
'%'+
'
';
container.appendChild(row);
@@ -1774,10 +1815,8 @@
renderNutritionFacts();
}
-function renderAll(){
+function renderBlendOutputs(){
renderTotal();
- renderActiveList();
- renderPicker();
const profile = computeFlavorProfile();
renderRadar(profile);
renderBars(profile);
@@ -1789,6 +1828,12 @@
persistCurrent();
}
+function renderAll(){
+ renderActiveList();
+ renderPicker();
+ renderBlendOutputs();
+}
+
/* =========================================================
PERSISTENCE
========================================================= */
@@ -1809,7 +1854,7 @@
if(!raw) return false;
const data = JSON.parse(raw);
if(data && data.blend && Object.keys(data.blend).length){
- blend = data.blend;
+ blend = normalizeBlendPercentages(data.blend, false);
currentBatchIndex = data.batchIndex || 0;
customGrams = data.customGrams || 200;
recipeCardUnit = data.recipeCardUnit === "volume" ? "volume" : "grams";
@@ -1840,8 +1885,12 @@
let v = Number(value);
if(isNaN(v) || v < 0) v = 0;
if(v > 100) v = 100;
- blend[key] = Math.round(v*10)/10;
- renderAll();
+ blend[key] = clampWholePct(v);
+ const slider = document.getElementById("range_"+key);
+ const num = document.getElementById("num_"+key);
+ if(slider) slider.value = blend[key];
+ if(num) num.value = blend[key];
+ renderBlendOutputs();
}
function autoBalance(){
@@ -1849,17 +1898,19 @@
if(keys.length===0){ showToast("Add ingredients before auto-balancing.","error"); return; }
const total = totalPercent();
if(total <= 0){
- const even = round1(100/keys.length);
- keys.forEach((k,i)=> blend[k] = i===keys.length-1 ? round1(100 - even*(keys.length-1)) : even);
+ const base = Math.floor(100/keys.length);
+ const remainder = 100 - base*keys.length;
+ keys.forEach((k,i)=> blend[k] = base + (i < remainder ? 1 : 0));
} else {
keys.forEach(k=>{
- blend[k] = round1((Number(blend[k])||0) / total * 100);
+ blend[k] = clampWholePct((Number(blend[k])||0) / total * 100);
});
// correct rounding drift on the last item
const newTotal = keys.reduce((s,k)=> s+blend[k],0);
- const drift = round1(100-newTotal);
- if(Math.abs(drift) >= 0.05){
- blend[keys[keys.length-1]] = round1(blend[keys[keys.length-1]] + drift);
+ const drift = 100-newTotal;
+ if(drift !== 0){
+ const adjustKey = keys.reduce((best,key)=> blend[key] > blend[best] ? key : best, keys[0]);
+ blend[adjustKey] = clamp(blend[adjustKey] + drift, 0, 100);
}
}
renderAll();
@@ -1877,7 +1928,7 @@
function loadRecipeById(id){
const recipe = allRecipes.find(r => r.id === id);
if(!recipe) return;
- blend = JSON.parse(JSON.stringify(recipe.items));
+ blend = normalizeBlendPercentages(recipe.items, true);
document.getElementById("blendNameInput").value = recipe.name;
document.getElementById("rcNotes").value = recipe.notes || "";
renderAll();
@@ -1988,11 +2039,11 @@
const cleaned = {};
Object.keys(data.items).forEach(k=>{
if(INGREDIENTS[k] !== undefined){
- cleaned[k] = Number(data.items[k]) || 0;
+ cleaned[k] = data.items[k];
}
});
if(Object.keys(cleaned).length===0) throw new Error("no valid ingredients");
- blend = cleaned;
+ blend = normalizeBlendPercentages(cleaned, true);
recipeCardUnit = data.recipeCardUnit === "volume" ? "volume" : "grams";
document.getElementById("blendNameInput").value = data.name || "Imported Blend";
document.getElementById("rcNotes").value = data.notes || "";
diff --git a/ingredients.json b/ingredients.json
index 7f247bf..29e4e9a 100644
--- a/ingredients.json
+++ b/ingredients.json
@@ -83,10 +83,10 @@
"color": 2
}
},
- "blackPeppercorns": {
+ "blackPeppercorns": {
"name": "Whole Black Peppercorns",
- "category": "Pepper / Whole Spice"
- "desc": "Whole peppercorns for grinding fresh; sharp, woody bite with mild background heat."
+ "category": "Pepper / Whole Spice",
+ "desc": "Whole peppercorns for grinding fresh; sharp, woody bite with mild background heat.",
"density": 6,
"affiliateLink": "https://amzn.to/3ThzhDl",
"affiliateAddToCartLink": null,
diff --git a/ingredients/index.html b/ingredients/index.html
index 975eab1..75154cf 100644
--- a/ingredients/index.html
+++ b/ingredients/index.html
@@ -99,7 +99,7 @@ Seasoning Ingredient Flavor Profiles
ThymeHerb - Earthy, minty-woody herb note.
TurmericColor / Earthy - Earthy bitterness and a bold golden color.
White SugarSweetener - Clean, direct sweetness with fast caramelization.
-Whole Black PeppercornsPepper - Sharp, woody bite with mild background heat.
+Whole Black PeppercornsPepper / Whole Spice - Whole peppercorns for grinding fresh; sharp, woody bite with mild background heat.