When to use wet vs dry, how equilibrium brining works, and the right salt-by-weight targets for juicy results—plus a calculator for exact grams.
Brining is easier when you do the math once. Use this Brining Calculator to set a target salt percentage, enter meat (and water, if wet), and get exact salt by weight—plus a realistic minimum brining time based on thickness and shape. Prefer grams for accuracy; volume equivalents (Diamond Crystal, Morton, or a custom density) are available for quick estimates, and can be shown as a range.
Tip: If your timing is uncertain, choose Equilibrium (self-limiting) so the meat won’t creep past your target salinity. Note: Most cooks land between 1.2–1.8%. Start at 1.5%.
Calculate Brine
Brine Calculator
Get exact salt by weight for wet, dry, or equilibrium brines, and a realistic minimum cure time.
Tip: weigh salt for accuracy. Volume equals are estimates.
Your Brining Plan
Ingredients
Salt (by weight) ()
Approximate volume equivalents (for salt by weight):
Minimum cure time
Safety: keep brining ≤ 40°F (4°C). Discard used brine. Learn more about wet vs dry vs equilibrium brines.
Additional Resources:
- USDA Food Safety Guidelines
- USDA Bacon & Curing Safety
- National Center for Home Food Preservation – Curing
- For commercial use, consult your local health department and follow commercial food safety regulations.
Disclaimer: This calculator is provided for informational and educational purposes only. Results are estimates based on general brining principles and should not be considered professional food safety advice. Always follow local health department regulations and USDA guidelines for food safety. Use accurate measurements, proper food handling practices, and maintain appropriate temperatures. The user assumes all risk associated with food preparation. No warranty or guarantee is provided regarding the accuracy of calculations or safety of results. When in doubt, consult a food safety professional.
// Density const DENSITY_PRESETS={dc:{label:'Diamond Crystal',range:[130,150]},morton:{label:'Morton Kosher',range:[235,250]}}; const DState={preset:'dc',showRange:true,customGPerCup:135};
// DOM const $=id=>document.getElementById(id); const app=$('brine-app'), themeToggle=$('theme-toggle'); const tabs={eq:$('tab-eq'),wet:$('tab-wet'),dry:$('tab-dry'),time:$('tab-time')}; const panels={salt:$('panel-salt'),time:$('panel-time')}; const unitBtns={m:$('u-metric'), i:$('u-imperial')}; const target=$('target'), tVal=$('target-val'), lblTarget=$('lbl-target'), tHelp=$('target-help'); const meatLb=$('meat-lb'), meatOz=$('meat-oz'), meatG=$('meat-g'), meatImp=$('meat-imp'), meatMet=$('meat-met'), lblMeat=$('lbl-meat'); const waterVol=$('water-vol'), waterUnit=$('water-unit'), waterGEl=$('water-g'), waterImp=$('water-imp'), waterMet=$('water-met'), lblWater=$('lbl-water'); const copyBtn=$('copy'), toast=$('toast'), readyHint=$('ready-hint'); const thick=$('thick'), lblThick=$('lbl-thick'), shape=$('shape'), copyTime=$('copy-time'); const tablist=$('tablist'); const saltResults=$('salt-results'); const timeBrineBtn=$('time-brine'), timeCureBtn=$('time-cure'), shapeHelper=$('shape-helper'); const timeResultTitle=$('time-result-title'), timeResultHelper=$('time-result-helper'), thicknessWarning=$('thickness-warning'); const densControls=$('density-controls'), densNote=$('dens-note'), densCustom=$('dens-custom'), densRange=$('dens-range'); const modeWarn=$('mode-warn'), volumes=$('volumes'), vdc=$('v-dc'), vmo=$('v-mo'), vcu=$('v-cu'), vcuWrap=$('v-cu-wrap'); const presetChipsContainer=$('preset-chips'); const useCure=$('use-cure'), sugarWrap=$('sugar-wrap'), targetSugar=$('target-sugar'), sugarVal=$('sugar-val'); const printBtn=$('print-btn'); const cureInfo=$('cure-info'), curePpmInfo=$('cure-ppm-info'), cureWarning=$('cure-warning'); const cureModeToggle=$('cure-mode-toggle'), cureHelpTrigger=$('cure-help-trigger'), cureModeExplanation=$('cure-mode-explanation'); const summary={ method:$('summary-method'), total:$('summary-total'), meat:$('summary-meat'), water:$('summary-water'), saltPct:$('summary-salt-pct'), salt:$('summary-salt'), sugarWrap:$('summary-sugar-wrap'), sugarPct:$('summary-sugar-pct'), sugar:$('summary-sugar'), cureWrap:$('summary-cure-wrap'), curePct:$('summary-cure-pct'), cure:$('summary-cure') }; const showMathSaltBtn=$('show-math-salt'), mathSaltContent=$('math-salt-content'), mathSaltFormula=$('math-salt-formula'); const showMathTimeBtn=$('show-math-time'), mathTimeContent=$('math-time-content'), mathTimeFormula=$('math-time-formula'); const timeExamplesList=$('time-examples-list');
// Update cure label dynamically function updateCureLabel(){ const label=document.querySelector('label[for="use-cure"]'); const header=$('cure-context-header'); const help=$('cure-context-help'); if(!label || !header || !help)return;
if(S.cureTargetMode==='ppm'){ // PPM mode: show calculated percentage with FSIS ingoing basis const calcPct=(S.targetPPM/PPM_PER_PERCENT).toFixed(3); let basis=''; if(S.mode==='dry' || S.productType==='bacon-dry'){ basis='of meat (PPM ingoing basis)'; }else{ const pickupClamped=Math.min(25,Math.max(5,Number(S.pickupPct)||10)); basis=`of meat + ${pickupClamped}% pickup (PPM ingoing basis)`; } label.textContent=`Add Curing Salt (~${calcPct}% ${basis} for ${S.targetPPM} ppm)`; }else{ // Legacy percent mode (convenience basis) const pct=(S.cureLevel==='bacon')?CURE_PCT_BACON:CURE_SALT_PERCENT; let basis=(S.mode==='dry')?'of meat':'of meat + water'; label.textContent=`Add Curing Salt (${pct}% ${basis})`; }
// Context-aware header and help text if(S.mode==='eq'){ header.textContent='FOR CURED MEATS'; header.style.color=''; help.textContent='For bacon, ham, pastrami, etc.'; help.style.color=''; }else{ header.innerHTML='⚠️ FOR LONG-TERM CURING ONLY'; header.style.color='#c2410c'; help.innerHTML='Not needed for flavor brining (turkey, chicken, pork chops, etc.)'; help.style.color='#d97706'; } }
// Manage inline help visibility for cure modes function syncCureHelpVisibility({forceOpen=false}={}){ if(!cureModeExplanation || !cureHelpTrigger)return;
if(!S.useCure){ cureModeExplanation.classList.add('hidden'); cureModeExplanation.setAttribute('aria-hidden','true'); cureHelpTrigger.setAttribute('aria-expanded','false'); cureHelpTrigger.disabled=true; cureHelpOverride=null; return; }
cureHelpTrigger.disabled=false;
let shouldOpen; if(forceOpen){ shouldOpen=true; cureHelpOverride=true; }else if(cureHelpOverride!==null){ shouldOpen=cureHelpOverride; }else{ shouldOpen=true; }
cureModeExplanation.classList.toggle('hidden',!shouldOpen); cureModeExplanation.setAttribute('aria-hidden',shouldOpen?'false':'true'); cureHelpTrigger.setAttribute('aria-expanded',shouldOpen?'true':'false'); }
// Update cure info dynamically function updateCureInfo(){ syncCureHelpVisibility();
if(!S.useCure){ cureInfo.classList.add('hidden'); cureWarning.classList.add('hidden'); $('accelerator-notice').classList.add('hidden'); return; }
cureInfo.classList.remove('hidden');
// Show PPM info with calculated percentage if(S.cureTargetMode==='ppm'){ const calcPct=(S.targetPPM/PPM_PER_PERCENT).toFixed(3); curePpmInfo.textContent=`Targeting ${S.targetPPM} ppm nitrite (${calcPct}% Cure #1 on ingoing basis).`; }else{ // Legacy percent mode if(S.cureLevel==='bacon'){ curePpmInfo.textContent='Using bacon-specific rate targeting 120 ppm nitrite (convenience basis).'; }else{ curePpmInfo.textContent='Using standard rate targeting 156 ppm nitrite (convenience basis).'; } }
// Validate and show warnings const validationMsg=validateCurePPM(); if(validationMsg){ cureWarning.textContent=validationMsg; cureWarning.classList.remove('hidden'); }else{ cureWarning.classList.add('hidden'); }
// Show accelerator notice for pumped bacon if(S.productType==='bacon-pumped' && S.useCure){ $('accelerator-notice').classList.remove('hidden'); }else{ $('accelerator-notice').classList.add('hidden'); } }
// Helpers function fmtImperial(g){ const totalOzRaw=g/G.GRAMS_PER_OZ; let totalOz=Math.round(totalOzRaw*10)/10; let lb=Math.floor(totalOz/16); let oz=Math.round((totalOz-lb*16)*10)/10; if(oz>=16-1e-9){lb+=1;oz=0.0;} return lb>0?`${lb} lb ${oz.toFixed(1)} oz`:`${oz.toFixed(1)} oz`; } function fmtMetric(g){return g>=1000?`${(g/1000).toFixed(2)} kg`:`${g.toFixed(0)} g`;} function hoursToDH(h){ const d=Math.floor(h/24); let r=Math.round(h - d*24); if(r===24){ r=0; } const days=d?`${d} Day${d>1?'s':''}`:''; const hours=r?`${r} Hour${r>1?'s':''}`:''; return days&&hours?`${days}, ${hours}`:(days||hours||'0 Hours'); } function showToast(msg){toast.textContent=msg;toast.classList.add('show');setTimeout(()=>toast.classList.remove('show'),1400);} async function copy(text){ try{ if(navigator.clipboard&&isSecureContext){ await navigator.clipboard.writeText(text); return true; } }catch(e){} const ta=document.createElement('textarea'); ta.value=text; ta.style.position='fixed'; ta.style.left='-9999px'; ta.style.top='0'; ta.setAttribute('aria-hidden','true'); document.body.appendChild(ta); ta.select(); let ok=false; try{ ok=document.execCommand('copy'); }catch(e){} document.body.removeChild(ta); return ok; } function track(evt,data={}){ try{ if(window.dataLayer)window.dataLayer.push({event:evt,...data}); }catch(e){} }
// Density helpers function volumesFromGrams(grams,gPerCup){const cups=grams/gPerCup;const tbsp=cups*16;const tsp=tbsp*3;return {cups,tbsp,tsp};} function fmtRange(a,b,unit,prec=2){const same=Math.abs(a-b)<1e-6;const A=a.toFixed(prec),B=b.toFixed(prec);return same?`${A} ${unit}`:`${A}–${B} ${unit}`;} function volString(grams,density,showRange){ if(showRange&&Array.isArray(density)){ const[lo,hi]=density;const vLo=volumesFromGrams(grams,hi);const vHi=volumesFromGrams(grams,lo); return `Cups: ${fmtRange(vLo.cups,vHi.cups,'cups',2)}; Tbsp: ${fmtRange(vLo.tbsp,vHi.tbsp,'Tbsp',1)}; tsp: ${fmtRange(vLo.tsp,vHi.tsp,'tsp',0)}`; }else{ const gpc=Array.isArray(density)?(density[0]+density[1])/2:density;const v=volumesFromGrams(grams,gpc); return `Cups: ${v.cups.toFixed(2)}; Tbsp: ${v.tbsp.toFixed(1)}; tsp: ${v.tsp.toFixed(0)}`; } } function updateDensNote(){ let base='Approximate volumes only. '; if(DState.preset==='dc') base+='Calculated using Diamond Crystal at ~130–150 g per cup. '; else if(DState.preset==='morton') base+='Calculated using Morton Kosher at ~235–250 g per cup. '; else base+=`Calculated using ${DState.customGPerCup} g per cup. `; densNote.innerHTML=base+'Densities vary with humidity and packing. For accuracy, weigh salt.'; } function renderVolumeBoxes(grams){ volumes.classList.remove('hidden');densControls.classList.remove('hidden'); vdc.innerHTML=volString(grams,DENSITY_PRESETS.dc.range,DState.showRange); vmo.innerHTML=volString(grams,DENSITY_PRESETS.morton.range,DState.showRange); if(DState.preset==='custom'){ vcuWrap.classList.remove('hidden'); const gpc=Math.max(80,Math.min(350,Number(DState.customGPerCup)||135)); vcu.innerHTML=volString(grams,gpc,false); }else{vcuWrap.classList.add('hidden');} updateDensNote(); } // Core math function calc(){ // Guard against invalid inputs if(S.meatG <= 0) { return { total:0, tableSalt:0, cureSalt:0, accelerator:0, sugar:0, warn:'Enter meat weight to calculate.', cureRate:0, ingoingBasisG:0 }; } let total=0, tableSalt=0, cureSalt=0, accelerator=0, sugar=0, warn=null; let ingoingBasisG=0; // FSIS "ingoing" basis for cure calculations let cureRate=0; // % of cure salt // Auto-set accelerator for pumped bacon (mandatory per 9 CFR 424.22(b)(1)) if(S.productType==='bacon-pumped' && S.useCure){ S.useAccelerator=true; }else{ S.useAccelerator=false; } // Calculate cure rate and ingoing basis if(S.cureTargetMode==='percent'){ // Legacy percent mode: convenience basis (meat+water or meat) cureRate=(S.cureLevel==='bacon')?CURE_PCT_BACON:CURE_SALT_PERCENT; }else{ // PPM mode: FSIS "ingoing" basis // Clamp pickup to valid range const pickupClamped=Math.min(25,Math.max(5,Number(S.pickupPct)||10)); if(S.productType==='bacon-dry' || S.mode==='dry'){ ingoingBasisG=S.meatG; // dry cure = meat only }else{ // pumped/massaged/immersion: meat + pickup const pickup=pickupClamped/100; ingoingBasisG=S.meatG*(1+pickup); } // Formula: 1% Cure #1 = 625 ppm nitrite (Cure #1 is 6.25% NaNO₂) // Keep full precision for calculations; round only for display cureRate=S.targetPPM/PPM_PER_PERCENT; } // Calculate weights by brining mode if(S.mode==='eq'){ total=S.meatG+S.waterG; tableSalt=total*S.target/100; sugar=total*S.targetSugar/100; if(S.cureTargetMode==='ppm'){ // PPM mode: use ingoing basis cureSalt=S.useCure? ingoingBasisG*(cureRate/100) : 0; accelerator=S.useAccelerator? ingoingBasisG*(ACCELERATOR_PPM/1000000) : 0; }else{ // Percent mode: use total (legacy convenience) cureSalt=S.useCure? total*(cureRate/100) : 0; accelerator=S.useAccelerator? total*(ACCELERATOR_PPM/1000000) : 0; } } if(S.mode==='wet'){ total=S.meatG+S.waterG; if(S.waterG<=0) warn='Wet brine needs water. Add water or switch to Dry/Equilibrium.'; tableSalt=S.waterG*S.target/100; sugar=S.waterG*S.targetSugar/100; if(S.cureTargetMode==='ppm'){ cureSalt=S.useCure? ingoingBasisG*(cureRate/100) : 0; accelerator=S.useAccelerator? ingoingBasisG*(ACCELERATOR_PPM/1000000) : 0; }else{ cureSalt=S.useCure? total*(cureRate/100) : 0; accelerator=S.useAccelerator? total*(ACCELERATOR_PPM/1000000) : 0; } } if(S.mode==='dry'){ total=S.meatG; tableSalt=S.meatG*S.target/100; sugar=S.meatG*S.targetSugar/100; // Dry always uses meat basis regardless of mode cureSalt=S.useCure? S.meatG*(cureRate/100) : 0; accelerator=S.useAccelerator? S.meatG*(ACCELERATOR_PPM/1000000) : 0; } return { total, tableSalt, cureSalt, accelerator, sugar, warn, cureRate, ingoingBasisG }; } function validateCurePPM(){ if(!S.useCure || S.cureTargetMode!=='ppm') return null; const limits=CURE_LIMITS[S.productType]; const ppm=S.targetPPM; // Hard limits (blocking errors) if(ppm>limits.max){ if(S.productType==='comminuted'){ return `ERROR: Exceeds ${limits.max} ppm limit for comminuted products per 9 CFR 424.21(c). Must reduce target.`; } if(S.productType==='bacon-pumped'){ return `ERROR: Pumped bacon is fixed at ${limits.max} ppm per 9 CFR 424.22(b)(1). Cannot exceed.`; } return `ERROR: Exceeds ${limits.max} ppm limit for ${S.productType} per 9 CFR 424.21(c). Must reduce target.`; }
// Warnings (advisory)
if(ppm>=limits.warn && ppm<=limits.max){
return `Warning: ${ppm} ppm approaches upper limit for ${S.productType}. Consider reducing slightly.`;
}
if(ppm
// URL / storage function stateToParams(){ const p=new URLSearchParams(); p.set('mode',S.mode); p.set('unit',S.unit); p.set('target',S.target.toFixed(2)); if(S.meatG>0)p.set('meatG',Math.round(S.meatG)); if(S.waterG>0)p.set('waterG',Math.round(S.waterG)); p.set('thickIn',S.thickIn.toFixed(2)); p.set('shape',S.shape); p.set('theme',S.theme); p.set('dens',DState.preset); p.set('densRange',DState.showRange?'1':'0'); p.set('densCustom',DState.customGPerCup); if(S.selectedPreset)p.set('preset',S.selectedPreset); p.set('useCure', S.useCure ? '1' : '0'); p.set('sugar', S.targetSugar.toFixed(2)); p.set('timeMode', S.timeMode); p.set('cureTargetMode', S.cureTargetMode); p.set('targetPPM', S.targetPPM); p.set('productType', S.productType); p.set('pickupPct', S.pickupPct); history.replaceState(null,'','?'+p.toString()); try{ localStorage.setItem('brine_mode',S.mode); localStorage.setItem('brine_unit',S.unit); localStorage.setItem('brine_theme',S.theme); }catch(e){} } function paramsToState(){ const sp=new URLSearchParams(location.search); let lsMode=null, lsUnit=null, lsTheme=null; try{ lsMode=localStorage.getItem('brine_mode'); lsUnit=localStorage.getItem('brine_unit'); lsTheme=localStorage.getItem('brine_theme'); }catch(e){}
const urlMode=sp.get('mode'); if(urlMode&&VALID_MODES.includes(urlMode)) S.mode=urlMode; else if(lsMode&&VALID_MODES.includes(lsMode)) S.mode=lsMode;
const urlUnit=sp.get('unit'); if(urlUnit&&VALID_UNITS.includes(urlUnit)) S.unit=urlUnit; else if(lsUnit&&VALID_UNITS.includes(lsUnit)) S.unit=lsUnit;
const urlTheme=sp.get('theme'); if(urlTheme&&VALID_THEMES.includes(urlTheme)) S.theme=urlTheme; else if(lsTheme&&VALID_THEMES.includes(lsTheme)) S.theme=lsTheme; else S.theme='auto';
if(sp.has('target'))S.target=parseFloat(sp.get('target'))||S.target; if(sp.has('meatG'))S.meatG=parseFloat(sp.get('meatG'))||S.meatG; if(sp.has('waterG'))S.waterG=parseFloat(sp.get('waterG'))||S.waterG; if(sp.has('thickIn'))S.thickIn=parseFloat(sp.get('thickIn'))||S.thickIn;
const urlShape=sp.get('shape'); if(urlShape&&VALID_SHAPES.includes(urlShape)) S.shape=urlShape;
const urlDens=sp.get('dens'); if(urlDens&&VALID_DENS.includes(urlDens)) DState.preset=urlDens;
if(sp.has('densRange'))DState.showRange=sp.get('densRange')==='1'; if(sp.has('densCustom'))DState.customGPerCup=parseInt(sp.get('densCustom'))||135;
const urlPreset=sp.get('preset'); if(urlPreset&&PRESETS[urlPreset]) S.selectedPreset=urlPreset; if(sp.has('useCure')) S.useCure = sp.get('useCure')==='1'; if(sp.has('sugar')) S.targetSugar = parseFloat(sp.get('sugar')) || 0;
const urlTimeMode=sp.get('timeMode'); if(urlTimeMode&&VALID_TIME_MODES.includes(urlTimeMode)) S.timeMode=urlTimeMode;
const urlCureMode=sp.get('cureTargetMode'); if(urlCureMode&&VALID_CURE_MODES.includes(urlCureMode)){ S.cureTargetMode=urlCureMode; }
if(sp.has('targetPPM')){ S.targetPPM=Math.min(250,Math.max(80,parseInt(sp.get('targetPPM'))||156)); }
const urlProductType=sp.get('productType'); if(urlProductType&&VALID_PRODUCT_TYPES.includes(urlProductType)){ S.productType=urlProductType; }
if(sp.has('pickupPct')){ S.pickupPct=Math.min(25,Math.max(5,parseInt(sp.get('pickupPct'))||10)); }
document.querySelectorAll('input[name="dens"]').forEach(rb=>{ rb.checked=(rb.value===DState.preset); }); }
// Theme function applyTheme(){ app.classList.remove('theme-dark','theme-light'); let icon='A',label='Theme: Auto'; if(S.theme==='dark'){app.classList.add('theme-dark');icon='D';label='Theme: Dark';} if(S.theme==='light'){app.classList.add('theme-light');icon='L';label='Theme: Light';} themeToggle.setAttribute('data-icon',icon); themeToggle.setAttribute('aria-label',label); themeToggle.title=label; } function cycleTheme(){ S.theme=(S.theme==='auto')?'dark':(S.theme==='dark')?'light':'auto'; applyTheme(); stateToParams(); track('brine_theme',{theme:S.theme}); }
// Render / UI function setHelpHTML(html){ tHelp.innerHTML=html; } function calculateModeRanges(mode){ // --- START NEW LOGIC --- // Add a hard-coded, wider range for 'wet' mode for better usability. if (mode === 'wet') { return {min: 2.0, max: 8.0, recommendedMin: 4.0, recommendedMax: 6.0}; } // --- END NEW LOGIC ---
const presetsForMode=Object.values(PRESETS).filter(p=>p.mode===mode); if(presetsForMode.length===0){ const fallbacks={eq:[1.0,2.0],wet:[5.0,10.0],dry:[1.0,2.5]}; const [min,max]=fallbacks[mode]||[1.0,2.0]; return {min,max,recommendedMin:min,recommendedMax:max}; } const targets=presetsForMode.map(p=>p.target); const presetMin=Math.min(...targets); const presetMax=Math.max(...targets); const padding=0.5; const min=Math.max(0.5,presetMin-padding); const max=presetMax+padding; return {min,max,recommendedMin:presetMin,recommendedMax:presetMax}; } function renderPresetChips(){ if(!presetChipsContainer)return; presetChipsContainer.innerHTML=''; if(S.mode==='time'){presetChipsContainer.style.display='none';return;} presetChipsContainer.style.display='flex'; const presetsForMode=Object.entries(PRESETS).filter(([k,v])=>v.mode===S.mode); presetsForMode.forEach(([key,preset])=>{ const chip=document.createElement('button'); chip.type='button'; chip.className='preset-chip'; chip.textContent=preset.label; chip.setAttribute('data-preset-key',key); chip.setAttribute('aria-label',`${S.selectedPreset===key?'Deselect':'Select'} ${preset.label} preset`); if(S.selectedPreset===key)chip.classList.add('active'); chip.setAttribute('aria-pressed', S.selectedPreset===key ? 'true' : 'false'); chip.addEventListener('click',()=>{ if(S.selectedPreset===key){ S.selectedPreset=null; S.cureLevel='default'; updateCureLabel(); updateCureInfo(); renderPresetChips(); stateToParams(); track('brine_preset_clear',{preset:key,mode:S.mode}); return; } S.selectedPreset=key;
// Set product type if specified if(preset.productType) S.productType=preset.productType;
// Set PPM/percent values based on current mode if(S.cureTargetMode==='ppm' && preset.targetPPM){ S.targetPPM=preset.targetPPM; const ppmPresetEl=$('ppm-preset'); if(ppmPresetEl){ // Try to match preset value, otherwise set to custom if(['120','156','200'].includes(String(preset.targetPPM))){ ppmPresetEl.value=String(preset.targetPPM); }else{ ppmPresetEl.value='custom'; const ppmCustomEl=$('ppm-custom'); if(ppmCustomEl) ppmCustomEl.value=preset.targetPPM; } } }
if(S.mode!==preset.mode){
S.target=preset.target; S.targetSugar=preset.sugar; S.useCure=preset.useCure;
S.cureLevel=preset.cureLevel||'default';
updateCureLabel();
updateCureInfo();
setMode(preset.mode);
}else{
const rangeInfo=calculateModeRanges(S.mode);
if(preset.target
if(modeChanged && !S.selectedPreset && MODE_DEFAULTS[m]){
const d=MODE_DEFAULTS[m];
S.target=d.target; S.targetSugar=d.sugar; S.useCure=d.useCure;
target.value=S.target; targetSugar.value=S.targetSugar; useCure.checked=S.useCure;
}else{
if(S.target
panels.salt.classList.remove('hidden'); panels.time.classList.add('hidden'); panels.salt.setAttribute('aria-labelledby','tab-'+m); panels.time.setAttribute('aria-labelledby','tab-time');
if(m==='eq'){ lblTarget.textContent='Target salt (% of meat + water)'; const rangeStr=`${rangeInfo.recommendedMin.toFixed(1)}–${rangeInfo.recommendedMax.toFixed(1)}%`; setHelpHTML(`Recommended range ${rangeStr}. Use 0 water for dry EQ.`); } if(m==='wet'){ lblTarget.textContent='Target salt (% of water only)'; const rangeStr=`${rangeInfo.recommendedMin.toFixed(1)}–${rangeInfo.recommendedMax.toFixed(1)}%`; setHelpHTML(`Typical range ${rangeStr} of the water weight.`); } if(m==='dry'){ lblTarget.textContent='Target salt (% of meat only)'; const rangeStr=`${rangeInfo.recommendedMin.toFixed(1)}–${rangeInfo.recommendedMax.toFixed(1)}%`; setHelpHTML(`Typical range ${rangeStr} by meat weight.`); }
const showOptions=(m!=='time'); const showSugar=(m!=='time'); const cureBasic=$('cure-basic-controls'); const cureAdvanced=$('cure-advanced-controls'); if(cureBasic) cureBasic.classList.toggle('hidden',!showOptions); if(cureAdvanced) cureAdvanced.classList.toggle('hidden',!showOptions); sugarWrap.classList.toggle('hidden',!showSugar);
const sugarLabel=(m==='dry')?'Target Sugar (% of meat only)':'Target Sugar (% of meat + water)'; sugarWrap.querySelector('label').textContent=sugarLabel;
updateCureLabel(); updateCureInfo(); updateUnits(S.unit); render(); renderPresetChips(); track('brine_mode',{mode:S.mode}); } let lastUnit='metric'; function updateUnits(u){ S.unit=u; lastUnit=u; const imp=(u==='imperial'); unitBtns.i.setAttribute('aria-pressed',imp?'true':'false'); unitBtns.m.setAttribute('aria-pressed',imp?'false':'true'); if(imp){ lblThick.textContent='Max diameter or thickness (inches)'; thick.value=S.thickIn.toFixed(1); thick.step='0.1'; }else{ lblThick.textContent='Max diameter or thickness (centimeters)'; thick.value=(S.thickIn*CM_PER_IN).toFixed(1); thick.step='0.1'; } meatImp.classList.toggle('hidden',!imp); waterImp.classList.toggle('hidden',!imp); meatMet.classList.toggle('hidden',imp); waterMet.classList.toggle('hidden',imp); lblMeat.textContent=imp?'Meat weight (lb/oz)':'Meat weight (grams)'; lblWater.textContent=imp?'Water volume (cups/quarts/gallons)':'Water weight (grams)'; densControls.classList.toggle('hidden',!imp); volumes.classList.add('hidden'); render(); } function render(){ tVal.textContent=S.target.toFixed(1)+'%'; sugarVal.textContent=S.targetSugar.toFixed(2)+'%'; const needWater=(S.mode==='wet'); if(S.unit==='imperial'){ meatLb.setAttribute('aria-invalid',S.meatG>0?'false':'true'); meatG.setAttribute('aria-invalid','false'); waterVol.setAttribute('aria-invalid',needWater&&S.waterG<=0?'true':'false'); waterGEl.setAttribute('aria-invalid','false'); }else{ meatG.setAttribute('aria-invalid',S.meatG>0?'false':'true'); meatLb.setAttribute('aria-invalid','false'); meatOz.setAttribute('aria-invalid','false'); waterGEl.setAttribute('aria-invalid',needWater&&S.waterG<=0?'true':'false'); waterVol.setAttribute('aria-invalid','false'); } if(S.mode==='eq'&&S.waterG===0){ const rangeInfo=calculateModeRanges('eq'); const rangeStr=`${rangeInfo.recommendedMin.toFixed(1)}–${rangeInfo.recommendedMax.toFixed(1)}%`; setHelpHTML(`Recommended range ${rangeStr}. Dry equilibrium (no added water).`); } const ready=isReady(); readyHint.textContent='';
if(ready){ const { total, tableSalt, cureSalt, accelerator, sugar, warn, cureRate, ingoingBasisG } = calc();
summary.method.textContent=`${S.mode.charAt(0).toUpperCase()+S.mode.slice(1)} Brine`; summary.total.textContent=(S.unit==='imperial')?fmtImperial(total):fmtMetric(total); summary.meat.textContent=(S.unit==='imperial')?fmtImperial(S.meatG):fmtMetric(S.meatG); summary.water.textContent=(S.unit==='imperial')?fmtImperial(S.waterG):fmtMetric(S.waterG); summary.water.closest('div').style.display=(S.mode==='dry'||S.waterG===0)?'none':'block'; summary.total.closest('div').style.display=(S.mode==='dry')?'none':'block';
summary.saltPct.textContent=`${S.target.toFixed(1)}%`; summary.salt.textContent=(S.unit==='imperial')?`${fmtImperial(tableSalt)} (${tableSalt.toFixed(0)} g)`:fmtMetric(tableSalt);
summary.sugarWrap.classList.toggle('hidden',sugar<=0); summary.sugarPct.textContent=`${S.targetSugar.toFixed(2)}%`; summary.sugar.textContent=(S.unit==='imperial')?`${fmtImperial(sugar)} (${sugar.toFixed(0)} g)`:fmtMetric(sugar); summary.cureWrap.classList.toggle('hidden',cureSalt<=0); // Update cure percentage display in summary if(S.cureTargetMode==='ppm'){ const ppmPct=(S.targetPPM/PPM_PER_PERCENT).toFixed(3); summary.curePct.textContent=`${ppmPct}% (${S.targetPPM} ppm)`; }else{ const legacyPct=(S.cureLevel==='bacon')?CURE_PCT_BACON:CURE_SALT_PERCENT; const legacyPPM=(S.cureLevel==='bacon')?120:156; summary.curePct.textContent=`${legacyPct}% (~${legacyPPM} ppm)`; } summary.cure.textContent=(S.unit==='imperial')?`${fmtImperial(cureSalt)} (${cureSalt.toFixed(0)} g)`:fmtMetric(cureSalt); // Update accelerator in summary const acceleratorWrap=$('summary-accelerator-wrap'); if(acceleratorWrap && accelerator>0){ acceleratorWrap.classList.remove('hidden'); $('summary-accelerator').textContent=(S.unit==='imperial')?`${fmtImperial(accelerator)} (${accelerator.toFixed(0)} g)`:fmtMetric(accelerator); }else if(acceleratorWrap){ acceleratorWrap.classList.add('hidden'); }
if(S.unit==='imperial'){ renderVolumeBoxes(tableSalt); } else { volumes.classList.add('hidden'); }
if(S.mode==='wet'&&warn){ modeWarn.textContent=warn; modeWarn.classList.remove('hidden'); } else { modeWarn.classList.add('hidden'); }
saltResults.classList.remove('hidden'); }else{ saltResults.classList.add('hidden'); }
// Show/hide cure mode toggle and sync inline help if(cureModeToggle){ cureModeToggle.classList.toggle('hidden',!S.useCure); } if(cureHelpTrigger){ cureHelpTrigger.disabled=!S.useCure; } syncCureHelpVisibility();
// Show/hide advanced controls (only in PPM mode) const cureAdvancedControls=$('cure-advanced-controls'); if(cureAdvancedControls){ cureAdvancedControls.classList.toggle('hidden',!(S.useCure && S.cureTargetMode==='ppm')); }
// Show/hide pickup % (only for PPM mode in wet/eq) const pickupWrap=$('pickup-pct-wrap'); if(pickupWrap){ pickupWrap.classList.toggle('hidden',!(S.cureTargetMode==='ppm' && S.mode!=='dry')); }
// Lock PPM controls for bacon-pumped (mandatory 120 ppm) const ppmPreset=$('ppm-preset'); const ppmCustom=$('ppm-custom'); const baconLockNote=$('bacon-pumped-lock-note'); const lockBacon=(S.productType==='bacon-pumped' && S.cureTargetMode==='ppm');
if(ppmPreset){ ppmPreset.disabled=lockBacon; if(lockBacon) ppmPreset.value='120'; } if(ppmCustom){ const customSelected=(ppmPreset && ppmPreset.value==='custom'); ppmCustom.classList.toggle('hidden',!customSelected); ppmCustom.disabled=lockBacon; } if(baconLockNote){ baconLockNote.classList.toggle('hidden',!lockBacon); }
copyBtn.disabled=!ready; printBtn.disabled=!ready;
// Time calculation based on mode if(S.timeMode==='brine'){ const hrs=Math.ceil(Math.pow(S.thickIn,2)*BRINE_DIFFUSION_RATES[S.shape]); $('time-out').textContent=hoursToDH(hrs); }else{ // S.timeMode === 'cure' const thicknessSq=Math.pow(S.thickIn,2); const hrs=Math.ceil(thicknessSq*CURE_K[S.shape]); const days=(hrs/24).toFixed(1); const [kLo,kHi]=CURE_K_RANGE[S.shape]; const rangeLo=Math.ceil(thicknessSq*kLo); const rangeHi=Math.ceil(thicknessSq*kHi); const daysLo=(rangeLo/24).toFixed(1); const daysHi=(rangeHi/24).toFixed(1); $('time-out').textContent=`${days} days (Range: ${daysLo}-${daysHi} days)`; }
// Validate thickness - warn if unrealistic for home curing if(S.thickIn > 6){ thicknessWarning.classList.remove('hidden'); }else{ thicknessWarning.classList.add('hidden'); }
updatePrintView(); updateMathFormulas(); updateTimeExamples(); stateToParams(); }
// Update time examples function updateTimeExamples(){ if(!timeExamplesList) return;
let examples=[]; if(S.timeMode==='brine'){ // Flavor brine examples examples=[ '1″ chicken breast (flat) ≈ 8 hours', '1.5″ pork chop (flat) ≈ 18 hours', '2″ turkey breast (tubular) ≈ 48 hours (2 days)' ]; }else{ // Equilibrium cure examples examples=[ '1″ duck breast (flat) ≈ 5 days', '1.5″ pork belly/bacon (flat) ≈ 11 days', '2″ pastrami flat (flat) ≈ 20 days' ]; }
timeExamplesList.innerHTML=examples.map(ex=>`
// Update math formulas function updateMathFormulas(){ if(!mathSaltFormula || !mathTimeFormula) return;
// Salt calculation formula
if(isReady()){
const { total, tableSalt } = calc();
let formula='';
if(S.mode==='eq'){
formula=`Equilibrium: Salt = (Meat + Water) × Target%
`;
formula+=`Example: (${S.meatG.toFixed(0)}g + ${S.waterG.toFixed(0)}g) × ${S.target.toFixed(1)}% = ${tableSalt.toFixed(1)}g`;
}else if(S.mode==='wet'){
formula=`Wet Brine: Salt = Water × Target%
`;
formula+=`Example: ${S.waterG.toFixed(0)}g × ${S.target.toFixed(1)}% = ${tableSalt.toFixed(1)}g`;
}else if(S.mode==='dry'){
formula=`Dry Brine: Salt = Meat × Target%
`;
formula+=`Example: ${S.meatG.toFixed(0)}g × ${S.target.toFixed(1)}% = ${tableSalt.toFixed(1)}g`;
}
mathSaltFormula.innerHTML=formula;
}
// Time calculation formula
const thickDisp=(S.unit==='imperial')?`${S.thickIn.toFixed(1)} in`:`${(S.thickIn*CM_PER_IN).toFixed(1)} cm`;
if(S.timeMode==='brine'){
const k=BRINE_DIFFUSION_RATES[S.shape];
const hrs=Math.ceil(Math.pow(S.thickIn,2)*k);
mathTimeFormula.innerHTML=`Flavor Brine: Time (hours) = thickness² × diffusion rate
`;
mathTimeFormula.innerHTML+=`Example: (${S.thickIn.toFixed(1)} in)² × ${k} hrs/in² = ${hrs} hours
`;
mathTimeFormula.innerHTML+=`Shape: ${S.shape} (${S.shape==='flat'?'faster':'slower'} diffusion)`;
}else{
const k=CURE_K[S.shape];
const thicknessSq=Math.pow(S.thickIn,2);
const hrs=Math.ceil(thicknessSq*k);
const days=(hrs/24).toFixed(1);
mathTimeFormula.innerHTML=`Equilibrium Cure: Time (hours) = thickness² × cure constant
`;
mathTimeFormula.innerHTML+=`Example: (${S.thickIn.toFixed(1)} in)² × ${k} hrs/in² = ${hrs} hours (${days} days)
`;
mathTimeFormula.innerHTML+=`Shape: ${S.shape} (${S.shape==='flat'?'faster':'slower'} equilibration)`;
}
}
// Print view updater function updatePrintView(){ const ready=isReady(); if(!ready) return;
const { total, tableSalt, cureSalt, accelerator, sugar, cureRate } = calc(); const f=(S.unit==='imperial')?fmtImperial:fmtMetric; const g=(val)=>`(${val.toFixed(0)} g)`; const f_g=(val)=>(S.unit==='imperial')?`${f(val)} ${g(val)}`:f(val);
$('print-date').textContent=`Generated: ${new Date().toLocaleDateString()}`;
$('print-method').textContent=`Method: ${S.mode.charAt(0).toUpperCase()+S.mode.slice(1)} Brine`; $('print-salt-pct').textContent=`Salt: ${S.target.toFixed(1)}%`; $('print-sugar-pct').textContent=`Sugar: ${S.targetSugar.toFixed(2)}%`;
const presetName=S.selectedPreset&&PRESETS[S.selectedPreset]?PRESETS[S.selectedPreset].label:null; $('print-preset-li').classList.toggle('print-hidden',!presetName); if(presetName){ $('print-preset').textContent=`Preset: ${presetName}`; }
$('print-meat').textContent=`Meat: ${f_g(S.meatG)}`; const showWater=(S.waterG>0 && S.mode!=='dry'); $('print-water-li').classList.toggle('print-hidden',!showWater); $('print-water').textContent=`Water: ${f_g(S.waterG)}`; $('print-total-li').classList.toggle('print-hidden',S.mode==='dry'); $('print-total').textContent=`Total Weight: ${f_g(total)}`;
$('print-salt').textContent=`Salt (by weight): ${f_g(tableSalt)} (${S.target.toFixed(1)}%)`; $('print-sugar-li').classList.toggle('print-hidden',sugar<=0); $('print-sugar').textContent=`Sugar: ${f_g(sugar)} (${S.targetSugar.toFixed(2)}%)`; $('print-cure-li').classList.toggle('print-hidden',cureSalt<=0); if(cureSalt>0){ const ppmPct=(S.cureTargetMode==='ppm')?(S.targetPPM/PPM_PER_PERCENT).toFixed(3):((S.cureLevel==='bacon')?CURE_PCT_BACON:CURE_SALT_PERCENT).toFixed(3); const displayPPM=(S.cureTargetMode==='ppm')?S.targetPPM:((S.cureLevel==='bacon')?120:156);
let basisNote=''; if(S.cureTargetMode==='ppm'){ basisNote=(S.mode==='dry' || S.productType==='bacon-dry')?', ingoing basis = meat only':`, ingoing basis = meat + ${S.pickupPct}% pickup`; }
$('print-cure').textContent=`Curing Salt #1: ${f_g(cureSalt)} — ${ppmPct}% (${displayPPM} ppm${basisNote})`;
// Add new paragraph explaining basis (create new element if needed) let cureBasisP=$('print-cure-basis'); if(!cureBasisP && S.cureTargetMode==='ppm'){ cureBasisP=document.createElement('p'); cureBasisP.id='print-cure-basis'; cureBasisP.style.fontSize='11px'; cureBasisP.style.color='#64748b'; cureBasisP.style.marginTop='4px'; $('print-cure').parentNode.appendChild(cureBasisP); }
if(cureBasisP && S.cureTargetMode==='ppm'){ if(S.mode==='dry' || S.productType==='bacon-dry'){ cureBasisP.textContent='PPM calculated on meat weight only (dry cure).'; }else{ cureBasisP.textContent=`PPM calculated on ingoing basis: ${S.meatG.toFixed(0)}g meat + ${S.pickupPct}% pickup = ${ingoingBasisG.toFixed(0)}g total.`; } cureBasisP.classList.remove('print-hidden'); }else if(cureBasisP){ cureBasisP.classList.add('print-hidden'); } }
// Add accelerator to print (create new list item if needed) if(accelerator>0){ let accLi=$('print-accelerator-li'); if(!accLi){ // Create if doesn't exist accLi=document.createElement('li'); accLi.id='print-accelerator-li'; const accSpan=document.createElement('span'); accSpan.id='print-accelerator'; accLi.appendChild(accSpan); $('print-ingredients-list').appendChild(accLi); } accLi.classList.remove('print-hidden'); $('print-accelerator').textContent=`Sodium Ascorbate/Erythorbate: ${f_g(accelerator)} — 550 ppm on ingoing basis`; }else{ const accLi=$('print-accelerator-li'); if(accLi) accLi.classList.add('print-hidden'); }
$('print-salt-vol').classList.toggle('print-hidden',S.unit!=='imperial'); if(S.unit==='imperial'){ const avgDc=(DENSITY_PRESETS.dc.range[0]+DENSITY_PRESETS.dc.range[1])/2; const v=volumesFromGrams(tableSalt,avgDc); let volText=''; if(v.cups>0.5){ volText=`~${v.cups.toFixed(1)} cups`; } else if(v.tbsp>1){ volText=`~${v.tbsp.toFixed(1)} Tbsp`; } else { volText=`~${v.tsp.toFixed(0)} tsp`; } $('print-salt-vol').textContent=`Volume (Diamond Crystal): ${volText}`; } }
// Events themeToggle.addEventListener('click',cycleTheme); tabs.eq.addEventListener('click',()=>setMode('eq')); tabs.wet.addEventListener('click',()=>setMode('wet')); tabs.dry.addEventListener('click',()=>setMode('dry')); tabs.time.addEventListener('click',()=>setMode('time')); tablist.addEventListener('keydown',(e)=>{ if(e.key!=='ArrowLeft'&&e.key!=='ArrowRight')return; e.preventDefault(); const order=['eq','wet','dry','time']; let i=order.indexOf(S.mode); i=e.key==='ArrowRight'?(i+1)%order.length:(i-1+order.length)%order.length; const next=order[i]; tabs[next].focus(); setMode(next); }); unitBtns.m.addEventListener('click',()=>{updateUnits('metric');track('brine_unit',{unit:'metric'});}); unitBtns.i.addEventListener('click',()=>{updateUnits('imperial');track('brine_unit',{unit:'imperial'});});
target.addEventListener('input',e=>{S.target=parseFloat(e.target.value)||0; S.selectedPreset=null; S.cureLevel='default'; render(); renderPresetChips();}); function updMeatImp(){const lb=parseFloat(meatLb.value)||0,oz=parseFloat(meatOz.value)||0; S.meatG=(lb*16+oz)*G.GRAMS_PER_OZ; S.selectedPreset=null; S.cureLevel='default'; render(); renderPresetChips();} meatLb.addEventListener('input',updMeatImp); meatOz.addEventListener('input',updMeatImp); meatG.addEventListener('input',()=>{S.meatG=parseFloat(meatG.value)||0; S.selectedPreset=null; S.cureLevel='default'; render(); renderPresetChips();}); function updWaterImp(){const amt=parseFloat(waterVol.value)||0; const u=waterUnit.value; S.waterG=(u==='cup')?amt*G.G_PER_CUP:(u==='quart')?amt*G.G_PER_QUART:amt*G.G_PER_GAL; S.selectedPreset=null; S.cureLevel='default'; render(); renderPresetChips();} waterVol.addEventListener('input',updWaterImp); waterUnit.addEventListener('change',updWaterImp); waterGEl.addEventListener('input',()=>{S.waterG=parseFloat(waterGEl.value)||0; S.selectedPreset=null; S.cureLevel='default'; render(); renderPresetChips();});
thick.addEventListener('input',()=>{ const val=parseFloat(thick.value)||0; const newThickIn=(S.unit==='imperial')?val:(val/CM_PER_IN); S.thickIn=parseFloat(newThickIn.toFixed(4)); S.selectedPreset=null; render(); }); shape.addEventListener('change',()=>{S.shape=shape.value; S.selectedPreset=null; render();});
copyBtn.addEventListener('click', async ()=>{ if(copyInProgress) return; copyInProgress=true; const { tableSalt, cureSalt, accelerator, sugar, cureRate, ingoingBasisG } = calc(); const modeName=S.mode.charAt(0).toUpperCase()+S.mode.slice(1); const presetName=S.selectedPreset&&PRESETS[S.selectedPreset]?PRESETS[S.selectedPreset].label:null;
let txt=`BRINING PLAN\n`; txt+=`Method: ${modeName} Brine - ${S.target.toFixed(1)}% Salt\n`; if(presetName) txt+=`Preset: ${presetName}\n`; txt+=`\n--- Weights ---\n`; txt+=`Meat: ${S.unit==='imperial'?fmtImperial(S.meatG):fmtMetric(S.meatG)}\n`; if(S.mode!=='dry'&&S.waterG>0){ txt+=`Water: ${S.unit==='imperial'?fmtImperial(S.waterG):fmtMetric(S.waterG)}\n`; } txt+=`\n--- Ingredients ---\n`; txt+=`Salt (by weight): ${S.unit==='imperial'?fmtImperial(tableSalt):fmtMetric(tableSalt)}`; if(S.unit==='imperial') txt+=` (${tableSalt.toFixed(0)} g)`; txt+=`\n`; if(sugar>0){ txt+=`Sugar: ${S.unit==='imperial'?fmtImperial(sugar):fmtMetric(sugar)}`; if(S.unit==='imperial') txt+=` (${sugar.toFixed(0)} g)`; txt+=`\n`; } if(cureSalt>0){ const ppmPct=(S.cureTargetMode==='ppm')?(S.targetPPM/PPM_PER_PERCENT).toFixed(3):((S.cureLevel==='bacon')?CURE_PCT_BACON:CURE_SALT_PERCENT).toFixed(3); const displayPPM=(S.cureTargetMode==='ppm')?S.targetPPM:((S.cureLevel==='bacon')?120:156);
txt+=`Curing Salt #1: ${S.unit==='imperial'?fmtImperial(cureSalt):fmtMetric(cureSalt)}`; if(S.unit==='imperial') txt+=` (${cureSalt.toFixed(0)} g)`;
// Add basis information for PPM mode if(S.cureTargetMode==='ppm'){ if(S.mode==='dry' || S.productType==='bacon-dry'){ txt+=` — ${ppmPct}% (${displayPPM} ppm, ingoing basis = meat only)`; }else{ txt+=` — ${ppmPct}% (${displayPPM} ppm, ingoing basis = meat + ${S.pickupPct}% pickup)`; } }else{ txt+=` — ${ppmPct}% (~${displayPPM} ppm)`; } txt+=`\n`;
// Add detailed basis calculation in PPM mode if(S.cureTargetMode==='ppm'){ if(S.mode==='dry' || S.productType==='bacon-dry'){ txt+=` (PPM basis: meat weight only)\n`; }else{ txt+=` (PPM basis: meat weight + ${S.pickupPct}% pickup = ${ingoingBasisG.toFixed(0)}g)\n`; } } } if(accelerator>0){ txt+=`Sodium Ascorbate/Erythorbate: ${S.unit==='imperial'?fmtImperial(accelerator):fmtMetric(accelerator)}`; if(S.unit==='imperial') txt+=` (${accelerator.toFixed(0)} g)`; txt+=` — 550 ppm on ingoing basis (required for pumped bacon)\n`; }
const ok=await copy(txt); showToast(ok?'Plan copied!':'Copy failed'); copyInProgress=false; track('brine_copy',{mode:S.mode,unit:S.unit,cured:S.useCure,cureMode:S.cureTargetMode}); });
copyTime.addEventListener('click', async ()=>{ if(copyInProgress) return; copyInProgress=true; const thicknessSq=Math.pow(S.thickIn,2); const thickDisp=(S.unit==='imperial')?`${S.thickIn.toFixed(1)} in`:`${(S.thickIn*CM_PER_IN).toFixed(1)} cm`; let timeText=''; if(S.timeMode==='brine'){ const hrs=Math.ceil(thicknessSq*BRINE_DIFFUSION_RATES[S.shape]); timeText=`${hoursToDH(hrs)}\n(Plan ±20% around this estimate)`; }else{ const hrs=Math.ceil(thicknessSq*CURE_K[S.shape]); const days=(hrs/24).toFixed(1); const [kLo,kHi]=CURE_K_RANGE[S.shape]; const rangeLo=Math.ceil(thicknessSq*kLo); const rangeHi=Math.ceil(thicknessSq*kHi); const daysLo=(rangeLo/24).toFixed(1); const daysHi=(rangeHi/24).toFixed(1); timeText=`${days} days (Range: ${daysLo}-${daysHi} days)\n(Minimum time for full equilibrium)`; } const modeLabel=S.timeMode==='brine'?'Flavor Brining':'Equilibrium Cure'; const tempReminder='\n\nTemperature: Keep at 36-38°F (2-3°C). Colder temps increase time; verify your fridge temperature.'; const txt=`BRINING TIME (${modeLabel})\nShape: ${S.shape.charAt(0).toUpperCase()+S.shape.slice(1)}\nThickness: ${thickDisp}\nTime: ${timeText}${tempReminder}`; const ok=await copy(txt); showToast(ok?'Time copied!':'Copy failed'); copyInProgress=false; track('brine_time_copy',{shape:S.shape,timeMode:S.timeMode}); });
// Density listeners const densRadios=document.querySelectorAll('input[name="dens"]'); densRadios.forEach(r=>{ r.addEventListener('change',(e)=>{ if(e.target.checked){ DState.preset=e.target.value; render(); } }); }); densCustom.addEventListener('input',(e)=>{ let val=parseInt(e.target.value)||135; val=Math.max(80,Math.min(350,val)); DState.customGPerCup=val; const customRadio=document.querySelector('input[name="dens"][value="custom"]'); if(customRadio) customRadio.checked=true; DState.preset='custom'; render(); }); densRange.addEventListener('change',(e)=>{ DState.showRange=e.target.checked; render(); });
// New feature listeners useCure.addEventListener('change',(e)=>{ S.useCure=e.target.checked; S.selectedPreset=null; S.cureLevel='default'; cureHelpOverride=S.useCure?true:null; updateCureInfo(); render(); renderPresetChips(); });
if(cureHelpTrigger){ cureHelpTrigger.addEventListener('click',()=>{ if(!S.useCure){ S.useCure=true; useCure.checked=true; cureHelpOverride=true; updateCureInfo(); render(); renderPresetChips(); track('brine_cure_help_toggle',{open:true,reason:'auto-enable'}); return; }
const expanded=cureHelpTrigger.getAttribute('aria-expanded')==='true'; cureHelpOverride=!expanded; syncCureHelpVisibility(); track('brine_cure_help_toggle',{open:cureHelpOverride}); }); } targetSugar.addEventListener('input',(e)=>{ S.targetSugar=parseFloat(e.target.value)||0; S.selectedPreset=null; S.cureLevel='default'; render(); renderPresetChips(); });
// Cure mode toggle (% vs PPM) document.querySelectorAll('input[name="cure-mode"]').forEach(radio=>{ radio.addEventListener('change',(e)=>{ S.cureTargetMode=e.target.value; S.selectedPreset=null; updateCureLabel(); updateCureInfo(); render(); renderPresetChips(); }); });
// Product type selector const productTypeEl=$('product-type'); if(productTypeEl){ productTypeEl.addEventListener('change',(e)=>{ S.productType=e.target.value;
// Auto-lock bacon-pumped to 120 ppm if(S.productType==='bacon-pumped' && S.cureTargetMode==='ppm'){ S.targetPPM=120; const preset=$('ppm-preset'); if(preset) preset.value='120'; }
S.selectedPreset=null; updateCureLabel(); updateCureInfo(); render(); }); }
// PPM preset selector const ppmPresetEl=$('ppm-preset'); if(ppmPresetEl){ ppmPresetEl.addEventListener('change',(e)=>{ if(e.target.value==='custom'){ const customInput=$('ppm-custom'); if(customInput){ S.targetPPM=Math.min(250,Math.max(80,parseInt(customInput.value)||156)); } }else{ S.targetPPM=parseInt(e.target.value); } S.selectedPreset=null; updateCureLabel(); updateCureInfo(); render(); }); }
// PPM custom input const ppmCustomInput=$('ppm-custom'); if(ppmCustomInput){ ppmCustomInput.addEventListener('input',(e)=>{ // Clamp to valid range let val=parseInt(e.target.value); if(isNaN(val)) val=156; S.targetPPM=Math.min(250,Math.max(80,val)); S.selectedPreset=null; updateCureLabel(); updateCureInfo(); render(); }); }
// Pickup % input const pickupPctInput=$('pickup-pct'); if(pickupPctInput){ pickupPctInput.addEventListener('input',(e)=>{ // Clamp to valid range [5, 25] let val=parseInt(e.target.value); if(isNaN(val)) val=10; S.pickupPct=Math.min(25,Math.max(5,val)); e.target.value=S.pickupPct; // Force clamped value back into input updateCureLabel(); render(); }); }
// Time mode listeners function updateTimeMode(mode){ S.timeMode=mode; timeBrineBtn.setAttribute('aria-pressed',mode==='brine'?'true':'false'); timeCureBtn.setAttribute('aria-pressed',mode==='cure'?'true':'false'); if(mode==='brine'){ timeBrineBtn.style.background='var(--accent)'; timeBrineBtn.style.color='#fff'; timeCureBtn.style.background='#fff'; timeCureBtn.style.color='var(--accent)'; timeResultTitle.textContent='Brining time estimate'; timeResultHelper.innerHTML='Guideline for flavor brining (e.g., poultry, chops) at 34–39°F. Plan ±20% around this estimate.'; }else{ timeCureBtn.style.background='var(--accent)'; timeCureBtn.style.color='#fff'; timeBrineBtn.style.background='#fff'; timeBrineBtn.style.color='var(--accent)'; timeResultTitle.textContent='Minimum equilibrium cure time'; timeResultHelper.innerHTML='Minimum time for full equilibrium cure (e.g., bacon, ham) at 34–39°F. Colder fridges or higher fat content may take longer.'; } render(); track('brine_time_mode',{timeMode:mode}); } timeBrineBtn.addEventListener('click',()=>updateTimeMode('brine')); timeCureBtn.addEventListener('click',()=>updateTimeMode('cure'));
// Show math toggle listeners if(showMathSaltBtn && mathSaltContent){ showMathSaltBtn.addEventListener('click',()=>{ const isExpanded=showMathSaltBtn.getAttribute('aria-expanded')==='true'; showMathSaltBtn.setAttribute('aria-expanded',!isExpanded?'true':'false'); mathSaltContent.classList.toggle('hidden'); track('brine_show_math',{type:'salt',expanded:!isExpanded}); }); } if(showMathTimeBtn && mathTimeContent){ showMathTimeBtn.addEventListener('click',()=>{ const isExpanded=showMathTimeBtn.getAttribute('aria-expanded')==='true'; showMathTimeBtn.setAttribute('aria-expanded',!isExpanded?'true':'false'); mathTimeContent.classList.toggle('hidden'); track('brine_show_math',{type:'time',expanded:!isExpanded}); }); }
// Print logic printBtn.addEventListener('click',()=>{ track('brine_print',{mode:S.mode,unit:S.unit}); const printContent=$('print-view'); if(!printContent){ console.error('Print view element not found!'); return; }
const iframe=document.createElement('iframe'); iframe.style.position='absolute'; iframe.style.width='0'; iframe.style.height='0'; iframe.style.border='0'; iframe.style.left='-9999px'; iframe.style.top='-9999px'; iframe.setAttribute('aria-hidden','true'); iframe.setAttribute('title','Print Content'); document.body.appendChild(iframe); const iDoc=iframe.contentWindow.document;
const allStyles=document.querySelectorAll('style'); allStyles.forEach(s=>{ iDoc.head.appendChild(s.cloneNode(true)); });
iDoc.body.innerHTML = `
setTimeout(()=>{ try{ iframe.contentWindow.focus(); iframe.contentWindow.print(); } catch(e){ console.error('Print failed:',e); } finally{ setTimeout(()=>{ document.body.removeChild(iframe); },1000); } },500); });
// Init paramsToState(); useCure.checked=S.useCure; targetSugar.value=S.targetSugar; updateTimeMode(S.timeMode);
// Initialize cure mode controls document.querySelectorAll('input[name="cure-mode"]').forEach(radio=>{ radio.checked=(radio.value===S.cureTargetMode); }); if(productTypeEl) productTypeEl.value=S.productType; if(ppmPresetEl){ if(['120','156','200'].includes(String(S.targetPPM))){ ppmPresetEl.value=String(S.targetPPM); }else{ ppmPresetEl.value='custom'; if(ppmCustomInput) ppmCustomInput.value=S.targetPPM; } } if(pickupPctInput) pickupPctInput.value=S.pickupPct;
let modeSetFromPreset=false; if(S.selectedPreset&&PRESETS[S.selectedPreset]){ const preset=PRESETS[S.selectedPreset]; S.target=preset.target; S.targetSugar=preset.sugar; S.useCure=preset.useCure; S.cureLevel=preset.cureLevel||'default'; if(preset.productType) S.productType=preset.productType; if(preset.targetPPM && S.cureTargetMode==='ppm') S.targetPPM=preset.targetPPM; if(preset.pickupPct) S.pickupPct=preset.pickupPct; target.value=S.target; targetSugar.value=S.targetSugar; useCure.checked=S.useCure; if(pickupPctInput && preset.pickupPct) pickupPctInput.value=preset.pickupPct; if(S.mode!==preset.mode){ setMode(preset.mode); modeSetFromPreset=true; } } applyTheme(); if(!modeSetFromPreset) setMode(S.mode); updateCureLabel(); updateCureInfo(); updateUnits(S.unit); render(); renderPresetChips(); })();
Volume is approximate—weigh salt for accuracy. Keep brining ≤ 40°F (4°C). Discard used brine.
This calculator is part of our broader set of BBQ planning tools — check out our growing BBQ Tools Collection for tools on wood pairing, cook timelines, and more.
How to use this calculator
- Pick your mode: Equilibrium (meat + water), Wet (% of water only), Dry (% of meat only), or Brining Time.
- Set a target %:
- Equilibrium: 1.2–1.8% (start at 1.5%).
- Wet: 5–7% for general use (up to 10% for short soaks).
- Dry: ~2% of meat weight (1.5–2.0% is the sweet spot).
- Enter weights/volume: Weigh meat in grams, if possible. For wet brines, enter water volume (Imperial) or water weight in grams (Metric)
- Get your result: Use the salt grams. Imperial users also see ounces and volume estimates (Diamond Crystal, Morton, or custom density), with an optional range to reflect packing variability.
- Copy & cook: Hit Copy to save the recipe. Keep everything ≤ 40°F (4°C) while brining and discard used brine.
Recommended targets (quick reference)
- Chicken pieces / pork chops / fish:
- Wet: 5–7% for 20–60 min (fish) or 1–8 h (meat).
- Dry: ~2% and rest 8–24 h.
- Whole chicken / turkey breast:
- Wet: 5–7%, 8–24 h; air-dry skin before cooking.
- Dry: ~2%, 24–48 h uncovered for crisp skin.
- Big roasts (pork shoulder, brisket):
- Prefer dry (≈2%) or equilibrium; allow 24–72 h.
- Fish fillets (~1″): 2–4% wet brine for 20–60 min; longer/stronger soaks may need a brief fresh-water “freshening” before cooking.
Why “weigh it” beats “teaspoon it”
Salt density varies a lot (Diamond Crystal vs. Morton). Grams never lie; spoons do. Use the gram result for the brine itself, and treat the volume equivalents as ballpark.The calculator shows ranges (and supports a custom density) to make that variability explicit.
Safety & good habits
- Keep brining ≤ 40°F (4°C).
- Use non-reactive containers (food-safe plastic, stainless, glass).
- Discard used brine.
- If the label says “contains up to X% solution” (a.k.a. “enhanced”), start with shorter/lighter brines or use dry brine.
Related reading & tools
- Learn the “why” and see cut-by-cut timing: Brining.
- Just want to understand the math; see brining math explained.
- Fire management for steady low heat: Snake Method.
- Get thin, clean smoke: blue smoke.
- Wood picks that match your meat: BBQ Wood Pairing Guide.
FAQs
For equilibrium, start at 1.5% (range 1.2–1.8%). For wet brines, 5–7% covers most cooks. For dry brines, ~2% of meat weight.
Yes. Equilibrium uses meat + water as one system. Total system weight × target % = salt grams.
You can, but brands vary. Use this tool’s gram result for accuracy; the Diamond/Morton volumes are estimates.
Use the Brining Time tab for a realistic minimum based on thickness and shape (flat vs. tubular). When in doubt, equilibrium gives you timing flexibility without oversalting.
Dry brine. If you do wet brine, air-dry 4–12 hours on a rack before cooking.
Treat it as partially brined. Go lighter/shorter, or skip brining and just dry brine modestly.
Usually no. Pat very dry. If you oversalted, a short cold soak before cooking can temper the surface.
About the author
James Roller documents South Carolina barbecue for Destination BBQ and authored Going Whole Hog. He researches techniques, interviews pitmasters, creates tools and curates reliable sources so home cooks can succeed.

Mike
Thursday 20th of November 2025
Does entering the turkey weigh for the whole bird correct for the weight of the bones, etc? I entered a 13.2 pound turkey in grams, but not all of that needs to be salted or will take salt, so will I end up with an overly salty bird. Do I need to manually calculate how much meat there actually is or does the calculator do that?
James Roller
Friday 21st of November 2025
@Mike, these are excellent questions and you are not too late for Thanksgiving.
1. Whole bird weight vs bones For the equilibrium brine mode, the calculator expects the whole raw weight of the turkey, just like you entered. It does not try to subtract bones or guess how much is edible meat, and that is intentional.
In an equilibrium brine you are treating the bird and the brine as one system. The math uses the total weight (meat, skin, bones, and water if you are using a wet brine) to hit a target overall salt percentage. The salt then diffuses until everything settles around that level. Because of that:
Using the full 13.2 lb turkey weight is the normal way to do it. You will not oversalt the bird simply because some of that weight is bone. If anything, the bone and less-salty parts slightly dilute the effective salt percentage in the meat.
If you want to be extra cautious, you can always aim for the lower end of the recommended range (for example 1.3–1.4% instead of 1.5%), but you should not need to manually estimate “real meat weight” to get a good result.
2. That “10.5 days” time estimate
On the Time tab there are two very different modes:
Flavor Brine (hours) – for typical holiday brines where you care about seasoning and juiciness. Equilibrium Cure (days) – for long, charcuterie-style cures (bacon, hams, etc.) where you want the very center to match the exact salt level of the outside. If you had Equilibrium Cure selected with “tubular” and a 3 cm value, that 10.5-day estimate is the calculator saying, “This is how long it might take for a full equilibrium cure all the way to the center at fridge temps.” That is useful for something like a cured ham, but it is overkill for a Thanksgiving turkey.
For a turkey, you have two good options instead:
Use Flavor Brine (hours) on the Time tab to get a more realistic window in hours. Or follow the quick-reference guidance on the page:
Whole turkey in a wet brine at 5–7%: roughly 8–24 hours. Dry brine around 2% of meat weight: roughly 24–48 hours, uncovered for better skin.
So no, you are not too late. That 10.5-day number is correct for a full equilibrium cure model, but it is not the target you need for a straightforward holiday turkey.
Mike
Thursday 20th of November 2025
@Mike, Also the the brining time calculator the thing show "10.5 days (Range: 7.8-14.0 days)" for tubular 3sm max thickness. Is that correct? IF so, I am too late for thanksgiving this year.
Mike
Thursday 20th of November 2025
@Mike, This is for a equilibrium brine.