Percent dilution: the conserved amount
HTML for this example
Select the code and use your copy shortcut. Changes in this preview are temporary.
<!doctype html><html lang="en-US"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Percent dilution: the conserved amount</title><style>*{box-sizing:border-box}:root{--ink:#24333d;--muted:#586872;--line:#cfd9de;--accent:#27627b;--soft:#edf3f5;--paper:white;color-scheme:light}body{margin:0;background:#f2f4f4;color:var(--ink);font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}main{max-width:1100px;padding:28px 32px 64px;margin:auto}header{margin-bottom:20px}h1{font-size:clamp(1.5rem,3vw,2.1rem);letter-spacing:-.035em;line-height:1.18;margin:0 0 9px}h2{font-size:1.2rem;line-height:1.3;margin:0 0 15px}h3{font-size:1rem;margin:0 0 8px}p{margin:9px 0 16px}header p{margin-bottom:0}.eyebrow{font-size:.75rem;font-weight:750;text-transform:uppercase;letter-spacing:.12em;color:var(--accent);margin-bottom:8px}.muted,small{color:var(--muted)}.panel{background:var(--paper);border:1px solid var(--line);padding:24px;border-radius:12px}.grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:24px}.fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:15px}.triple{display:grid;grid-template-columns:2fr 1fr 1fr;gap:16px}.below{margin-top:24px}label{display:block;font-size:.9rem;font-weight:650}input,select,textarea,button{font:inherit}input,select,textarea{display:block;width:100%;min-width:0;margin-top:6px;padding:10px 12px;border:1px solid #879da8;border-radius:6px;background:white;color:var(--ink)}input,select{min-height:46px}textarea{resize:vertical;line-height:1.6}button{min-height:44px;border:1px solid var(--accent);border-radius:6px;padding:9px 15px;background:var(--accent);color:white;font-weight:650;cursor:pointer}button.secondary{background:white;color:var(--accent)}button:disabled{opacity:.5;cursor:not-allowed}button:hover:enabled{filter:brightness(.95)}:focus-visible{outline:3px solid #bb6b22;outline-offset:3px}.actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:18px}.result{padding:26px;background:var(--ink);color:white;border-radius:12px}.result .muted{color:#d1e0e7}.big{font-size:clamp(2rem,5vw,3.5rem);line-height:1.15;letter-spacing:-.04em;font-weight:750;font-variant-numeric:tabular-nums;margin:10px 0;overflow-wrap:anywhere}.formula{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere}.error{min-height:24px;color:#a33028;font-weight:650;margin:9px 0 0}.status{font-size:.9rem;color:var(--muted)}.note{padding:12px 16px;background:#fff3de;border-left:3px solid #b67e38;color:#65481d}table{width:100%;border-collapse:collapse;font-size:.92rem}th{text-align:left;color:var(--muted);font-size:.83rem}th,td{padding:10px 8px;border-bottom:1px solid var(--line);vertical-align:top}th:first-child,td:first-child{padding-left:0}details{padding:16px 0;border-top:1px solid var(--line)}summary{font-weight:650;cursor:pointer}pre{white-space:pre-wrap;overflow-wrap:anywhere;font:.87rem/1.6 ui-monospace,Consolas,monospace;background:#edf1f3;border:1px solid #d7e0e4;padding:18px;border-radius:7px}svg{display:block;width:100%;height:auto}.badge{display:inline-block;padding:3px 9px;background:var(--soft);color:var(--accent);font-size:.78rem;font-weight:700;border-radius:25px}.rule{height:1px;background:var(--line);margin:18px 0}[hidden]{display:none!important}a{color:var(--accent)}@media(max-width:720px){main{padding:22px 20px 45px}.grid{grid-template-columns:1fr;gap:18px}.panel{padding:20px}.triple{grid-template-columns:1fr 1fr}.triple>label:first-child{grid-column:1/-1}}@media(max-width:400px){main{padding:18px 16px 36px}.panel{padding:16px}.fields{gap:12px}.result{padding:20px}.big{font-size:2.2rem}th,td{padding:9px 5px}}:root{--accent:#426c72}body{background:#f0f4f3}.conservation{background:#fff;border-top:5px solid #4e7b80;display:grid;grid-template-columns:1fr 1.2fr;gap:24px;padding:26px}.concentration .big{color:#36666d}.mass-balance{display:flex;justify-content:center;align-items:center;gap:24px}.mass-balance>div{background:#e8f1ef;border-bottom:4px solid #6b9390;padding:18px 24px;min-width:120px}.mass-balance small{display:block}.mass-balance b{font-size:1.75rem;display:block;overflow-wrap:anywhere}.balance-equals{font-size:2rem}.summary{padding:12px 20px;background:#e3eeeb;margin:0;font-weight:650;font-size:.95rem}@media(max-width:720px){.conservation{grid-template-columns:1fr;gap:14px;padding:22px}.mass-balance{gap:16px;justify-content:flex-start}.mass-balance>div{min-width:0;flex:1;padding:12px 16px}.mass-balance b{font-size:1.45rem}}</style></head><body><main><header><h1>Percent dilution: the conserved amount</h1><p>Fictional component S: compare the same component mass before and after.</p></header>
<section class="conservation" aria-live="polite"><div class="concentration"><span>Final concentration</span><div class="big" id="result">1% m/v</div><span id="state">Supplied final solution volume</span></div><div class="mass-balance"><div><small>Before</small><b id="before">2 g S</b></div><span class="balance-equals">=</span><div><small>After</small><b id="after">2 g S</b></div></div></section><p id="summary" class="summary">25 mL initial solution → 200 mL final solution · quantity ratio 8</p>
<section class="panel below"><div class="fields"><label for="basis1">Initial percentage basis<select id="basis1"><option value="mv">Mass / volume · g per 100 mL</option><option value="mm">Mass / mass · g per 100 g</option></select></label><label for="basis2">Final percentage basis<select id="basis2"><option value="mv">Mass / volume · g per 100 mL</option><option value="mm">Mass / mass · g per 100 g</option></select></label><label for="c1">Initial concentration (%)<input id="c1" value="8" inputmode="decimal" ></label><label for="q1">Initial solution quantity (mL)<input id="q1" value="25" inputmode="decimal" ></label><label for="mode">Find<select id="mode"><option value="forward">Final concentration from supplied final quantity</option><option value="inverse">Final quantity from target concentration</option></select></label><label for="target">Final solution quantity (mL)<input id="target" value="200" inputmode="decimal" ></label></div><div class="actions"><button type="button" id="calculate">Check conservation</button><button type="button" id="mass" class="secondary">Load independent mass-percent example</button><button type="button" id="reset" class="secondary">Reset volume example</button></div><p id="error" role="status" class="error"></p><p id="feedback" role="status" class="status"></p></section>
<section class="panel below"><h2>Before-and-after ledger</h2><p id="beforeTrace" class="formula"></p><p id="afterTrace" class="formula"></p><p id="ratio" class="formula"></p><p class="note">Final solution quantity is the whole final record, not solvent added. No numerical difference here is an instruction to add solvent.</p></section><details class="below"><summary>Matching bases, zero cases, and scope</summary><p>m/v uses solution volume; m/m uses solution mass. They are independent records; matching numerals do not imply the same physical solution. No density is supplied to connect them. A higher target concentration is outside solvent-only dilution; equal positive concentrations describe no dilution. Zero stock gives zero final concentration for any valid supplied final quantity. Zero-to-zero inverse solving does not determine a unique final quantity.</p><p>Use nonnegative concentration decimals (m/v ≤1,000,000; m/m ≤100) and positive solution quantities ≤1 billion, up to six decimal places. Final quantity must be at least initial quantity. Changing inputs clears the accepted result until the next check. Exact fractions are used before six-place display rounding. This is a paper arithmetic model, with no mixing, measurement, dose, reagent instructions, reactions, losses, stock mixing, or saved records.</p></details></main><script>const $=id=>document.getElementById(id),put=(id,s)=>{$(id).textContent=s};
function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));}
function gcd(a,b){a=a<0n?-a:a;b=b<0n?-b:b;while(b){[a,b]=[b,a%b]}return a||1n;}
function R(n,d=1n){n=BigInt(n);d=BigInt(d);if(d===0n)throw Error('Undefined denominator.');if(d<0n){n=-n;d=-d}const g=gcd(n,d);return {n:n/g,d:d/g};}
function mul(a,b){return R(a.n*b.n,a.d*b.d)}function div(a,b){return R(a.n*b.d,a.d*b.n)}function add(a,b){return R(a.n*b.d+b.n*a.d,a.d*b.d)}function sub(a,b){return R(a.n*b.d-b.n*a.d,a.d*b.d)}function cmp(a,b){const x=a.n*b.d-b.n*a.d;return x<0n?-1:x>0n?1:0}
function parse(s,{max=1000000,positive=false,places=6}={}){const t=String(s).trim();if(!t)throw Error('Enter a value; blank is not zero.');const re=new RegExp('^[0-9]+(?:\.[0-9]{1,'+places+'})?$');if(!re.test(t))throw Error('Use digits and a US decimal point, up to '+places+' places; no signs, commas, or exponents.');const v=Number(t);if(!Number.isFinite(v)||v>max||(positive&&v===0))throw Error('Value must be '+(positive?'greater than zero':'nonnegative')+' and at most '+max+'.');const [a,b='']=t.split('.');return R(BigInt(a+b),10n**BigInt(b.length));}
function format(a,places=6){const neg=a.n<0n,n=neg?-a.n:a.n,scale=10n**BigInt(places),v=n*scale,q=v/a.d,rem=v%a.d,k=q+(rem*2n>=a.d?1n:0n);const f=(k%scale).toString().padStart(places,'0').replace(/0+$/,'');return {text:(neg&&k!==0n?'-':'')+(k/scale)+(f?'.'+f:''),approx:rem!==0n};}
function exact(a){let d=a.d;while(d%2n===0n)d/=2n;while(d%5n===0n)d/=5n;if(d!==1n)return a.n+'/'+a.d;let places=0,p=1n;while(p%a.d!==0n&&places<40){p*=10n;places++}return format(a,places).text;}
function display(a,places=6){if(a.n>0n&&cmp(a,R(1n,10n**BigInt(places)))<0)return '< '+format(R(1n,10n**BigInt(places)),places).text;const f=format(a,places);return (f.approx?'≈ ':'')+f.text;}
function listen(ids,fn){ids.forEach(id=>{ $(id).addEventListener('input',fn);$(id).addEventListener('change',fn); });}
function dilution(cText,qText,target,mode,basis1,basis2){if(!['mv','mm'].includes(basis1)||basis1!==basis2)throw Error('Initial and final percentage bases must match; no density conversion is supplied.');const c=parse(cText,{max:basis1==='mm'?100:1000000}),q=parse(qText,{max:1000000000,positive:true}),mass=div(mul(c,q),R(100n));let finalC,finalQ;if(mode==='forward'){finalQ=parse(target,{max:1000000000,positive:true});if(cmp(finalQ,q)<0)throw Error('Final solution quantity must be at least the initial quantity.');finalC=div(mul(c,q),finalQ);}else if(mode==='inverse'){finalC=parse(target,{max:basis1==='mm'?100:1000000});if(c.n===0n&&finalC.n===0n)return {state:'underdetermined',mass,c,q};if(c.n===0n)throw Error('Zero stock cannot produce a positive target by this dilution model.');if(finalC.n===0n)throw Error('A positive stock cannot reach an exact zero target at a finite final quantity.');if(cmp(finalC,c)>0)throw Error('A higher target is unreachable by solvent-only dilution.');finalQ=div(mul(c,q),finalC);if(cmp(finalQ,R(1000000000n))>0)throw Error('Calculated final quantity exceeds this helper’s 1 billion scope.');}else throw Error('Choose a supported calculation mode.');const after=div(mul(finalC,finalQ),R(100n));return {state:cmp(finalQ,q)===0?'identity':'dilution',c,q,mass,finalC,finalQ,after,ratio:div(finalQ,q)};}
function units(){const mass=$('basis1').value==='mm',u=mass?'g':'mL';const qlabel=document.querySelector('label[for="q1"]'),tlabel=document.querySelector('label[for="target"]');qlabel.firstChild.textContent='Initial solution '+(mass?'mass (g)':'volume (mL)');tlabel.firstChild.textContent=$('mode').value==='inverse'?'Target concentration (%)':'Final solution '+(mass?'mass (g)':'volume (mL)');return {u,basis:mass?'m/m':'m/v'};}
function clear(){units();put('result','—');['state','before','after','summary','beforeTrace','afterTrace','ratio','error'].forEach(id=>put(id,''));put('feedback','Inputs changed. Check conservation for a current record.');}
function calculate(){const {u,basis}=units();try{const r=dilution($('c1').value,$('q1').value,$('target').value,$('mode').value,$('basis1').value,$('basis2').value);if(r.state==='underdetermined'){put('result','Underdetermined');put('state','Zero stock and zero target do not fix a final quantity.');put('before','0 g S');put('after','Not determined');['summary','beforeTrace','afterTrace','ratio'].forEach(id=>put(id,''));}else{put('result',display(r.finalC)+'% '+basis);put('state',r.state==='identity'?'Identity: unchanged quantity, no dilution':'Final solution '+($('basis1').value==='mm'?'mass':'volume')+'; not amount added');put('before',display(r.mass)+' g S');put('after',display(r.after)+' g S');put('summary',exact(r.q)+' '+u+' initial solution → '+exact(r.finalQ)+' '+u+' final solution · quantity ratio '+exact(r.ratio));put('beforeTrace','Before: ('+exact(r.c)+' g S / 100 '+u+' solution) × '+exact(r.q)+' '+u+' = '+exact(r.mass)+' g S');put('afterTrace','After: ('+exact(r.finalC)+' g S / 100 '+u+' solution) × '+exact(r.finalQ)+' '+u+' = '+exact(r.after)+' g S');put('ratio','Final / initial solution quantity = '+exact(r.finalQ)+' / '+exact(r.q)+' = '+exact(r.ratio));}put('error','');put('feedback','Current fictional arithmetic record; no solution has been prepared.');}catch(e){clear();put('feedback','No accepted calculation.');put('error',e.message);}}
listen(['c1','q1','target','basis1','basis2'],clear);$('mode').addEventListener('change',()=>{$('target').value=$('mode').value==='inverse'?'1':'200';clear()});$('calculate').addEventListener('click',calculate);function fixture(basis){$('basis1').value=basis;$('basis2').value=basis;$('c1').value='8';$('q1').value='25';$('target').value='200';$('mode').value='forward';calculate();put('feedback','');} $('mass').addEventListener('click',()=>fixture('mm'));$('reset').addEventListener('click',()=>fixture('mv'));globalThis.Tool={dilution,calculate,clear,exact,cmp};calculate();</script></body></html>