Match purpose to unit

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>Match purpose to unit</title><style>*{box-sizing:border-box}html{color-scheme:light}body{margin:0;background:var(--bg,#eef1f4);color:var(--ink,#233449);font:16px/1.5 system-ui,-apple-system,Segoe UI,sans-serif}main{max-width:1040px;margin:auto;padding:26px 28px 44px}header{margin-bottom:20px}h1{font-size:clamp(26px,4vw,36px);letter-spacing:-.035em;line-height:1.15;margin:5px 0 10px}h2{font-size:21px;line-height:1.3;margin:0 0 15px}h3{font-size:17px;margin:0 0 9px}p{margin:8px 0 16px}.kicker{font-size:12px;text-transform:uppercase;letter-spacing:.12em;font-weight:750;color:var(--accent,#315b87)}.muted,small{color:#556473}.panel{margin:20px 0;padding:24px;border:1px solid var(--line,#ccd5df);border-radius:10px;background:#fff}.hero{padding:24px;background:var(--hero,#183b5c);color:#fff;border-radius:12px;margin:18px 0}.hero .kicker,.hero small{color:#d9e5ef}.big{font-size:clamp(32px,5vw,52px);line-height:1.15;font-weight:750;letter-spacing:-.035em;overflow-wrap:anywhere;margin:10px 0 15px}.row{display:flex;flex-wrap:wrap;gap:15px;align-items:end}.row>label{flex:1;min-width:140px}.grid{display:grid;grid-template-columns:1fr 1fr;gap:20px}.thirds{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:15px}label{display:block;font-size:14px;font-weight:650}input,select,textarea{font:inherit;font-size:16px;color:inherit;display:block;width:100%;margin-top:6px;padding:11px 12px;border:1px solid #a9b4c1;border-radius:6px;background:white;min-width:0}input[type=checkbox]{display:inline;width:auto;margin:0 8px 0 0;accent-color:var(--accent,#315b87)}textarea{min-height:100px;resize:vertical}button{font:inherit;font-weight:650;min-height:44px;border:1px solid var(--accent,#315b87);border-radius:6px;background:var(--accent,#315b87);color:white;padding:10px 16px;cursor:pointer}button.secondary{background:white;color:var(--accent,#315b87)}button:disabled{opacity:.5;cursor:default}button[aria-pressed=true]{box-shadow:0 0 0 3px #eab655}button:hover:not(:disabled){filter:brightness(.94)}:focus-visible{outline:3px solid #e5a72f;outline-offset:3px}.actions{display:flex;flex-wrap:wrap;gap:10px;margin:18px 0 0}[hidden]{display:none!important}.error{color:#a3232a;font-weight:650}.error:not(:empty){padding:12px;background:#fff0ed;border-left:4px solid #ab3439;margin:14px 0}.note{padding:14px;background:#edf2f6;border-radius:6px;margin:14px 0}.mono,pre{font-family:ui-monospace,Consolas,monospace;font-variant-numeric:tabular-nums}pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:14px;line-height:1.7;margin:0}.mono{overflow-wrap:anywhere}table{width:100%;border-collapse:collapse;table-layout:fixed;font-size:14px}th,td{text-align:left;vertical-align:top;overflow-wrap:anywhere;padding:11px 8px;border-bottom:1px solid #d4dce4}th{font-size:12px;text-transform:uppercase;letter-spacing:.035em;color:#536372}th:first-child,td:first-child{padding-left:0}.tag{display:inline-block;padding:4px 8px;border:1px solid currentColor;border-radius:4px;font-size:12px;font-weight:750}.rule{border-top:1px solid #cbd4dd;padding-top:18px}.line{height:30px;border-bottom:1px solid #aab5bf}.stat{font-size:34px;line-height:1.15;font-weight:750}.stat small{font:14px system-ui;display:block;margin-top:8px}details{margin:20px 0;border-top:1px solid #cbd4dd;padding:18px 0}summary{font-weight:700;cursor:pointer}details p{margin-top:15px}a{color:var(--accent,#315b87)}footer{font-size:13px;color:#556473;margin-top:25px}fieldset{min-width:0;border:1px solid #c8d2db;border-radius:7px;padding:18px;margin:15px 0}legend{font-weight:750;padding:0 6px}@media(max-width:720px){main{padding:20px}.panel,.hero{padding:20px}.thirds{gap:10px}}@media(max-width:540px){main{padding:16px}.grid,.thirds{grid-template-columns:1fr}.panel,.hero{padding:18px}.row{gap:12px}.row>label{min-width:100px}h1{font-size:27px}th,td{padding:10px 5px;font-size:13px}.big{font-size:37px}}.quiz-sheet{background:#fff;padding:25px;border:1px solid #c6d2d9;border-top:5px solid #597c8c}.sheet-heading{display:flex;justify-content:space-between;gap:15px;flex-wrap:wrap;margin-bottom:24px}.matching-grid{display:grid;grid-template-columns:1.3fr 1fr;gap:30px}.premise{border-bottom:1px solid #d3dce2;padding:10px 0 18px}.matching-grid aside{background:#eef4f7;padding:20px}.matching-grid aside p{border-bottom:1px solid #cbd9df;padding:10px 0}.matching-grid aside b{display:inline-block;min-width:25px}.quiz-sheet #feedback:not(:empty){margin-top:22px;border-top:2px solid #5d8190;padding-top:20px}@media(max-width:540px){.quiz-sheet{padding:18px}.matching-grid{grid-template-columns:1fr;gap:20px}.matching-grid aside{grid-row:1}.matching-grid aside p{padding:5px 0;margin:5px 0}}</style></head><body style="--bg:#edf3f5;--accent:#3f6c80"><main><header><div class="kicker">Working example</div><h1>Match purpose to unit</h1><p class="muted">Use the supplied source and shared bank; use each response at most once.</p></header><section class="quiz-sheet"><div class="sheet-heading"><span class="tag">Student practice</span><span class="muted">3 questions · 4 responses · 1 unused</span></div><div class="matching-grid"><div><h2>Measurement purposes</h2><div id="premises"></div></div><aside><h2>Shared response bank</h2><div id="bank"></div></aside></div><div class="actions"><button type="button" id="check">Check responses</button><button type="button" id="reset" class="secondary">Reset answers</button><button type="button" id="reorder" class="secondary">Reorder bank</button></div><p id="error" class="error" role="alert"></p><div id="feedback" aria-live="polite"></div></section><details open><summary>Supplied source record · handout U for the original set</summary><div id="handout"></div></details><details><summary>Teacher key · separate from student practice</summary><div id="teacher"></div></details><details><summary>Edit a teacher-authored set</summary><p>Supply 3–8 premises and 4–10 homogeneous responses, with more responses than premises. IDs and correct response IDs must be distinct. Each premise links to a supplied source mapping. Source quality and ambiguity still require teacher review.</p><label for="author">Set JSON · supplied source mappings<textarea id="author" rows="14" spellcheck="false"></textarea></label><div class="actions"><button type="button" id="apply">Apply supplied set</button></div><p id="authorError" class="error" role="alert"></p></details><details><summary>Practice limits</summary><p>Missing, unknown, or reused choices produce no score. Editing a selection clears feedback. Reordering preserves answer IDs and selections. This is local practice feedback, not a stored school grade or proof of mastery. Keep required DOCX/PDF worksheets and their teacher keys separately.</p></details><footer>Local example · No information is sent or saved.</footer></main><script>const $=id=>document.getElementById(id);const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));function gcd(a,b){return b?gcd(b,a%b):a}function rat(n,d=1n){const g=gcd(n,d);return {n:n/g,d:d/g}}function mul(a,b){return rat(a.n*b.n,a.d*b.d)}function div(a,b){return rat(a.n*b.d,a.d*b.n)}function rounded(r,p=6){const scale=10n**BigInt(p),q=(r.n*scale*2n+r.d)/(2n*r.d),s=q.toString().padStart(p+1,'0');return p?s.slice(0,-p)+'.'+s.slice(-p):s}function show(r,unit=''){if(r.n>0n&&r.n*1000000n<r.d)return '<0.000001 '+unit;return '≈ '+rounded(r,6).replace(/([.][0-9]*?)0+$/,'$1').replace(/[.]$/,'')+' '+unit}function decimal(s,name,max=1000000,places=6){if(!new RegExp('^[0-9]+(?:[.][0-9]{1,'+places+'})?$').test(s)||s.length>35)throw Error(name+': enter a nonnegative dot-decimal number, up to '+places+' decimal places.');let [a,b='']=s.split('.');const r=rat(BigInt(a+b),10n**BigInt(b.length));if(r.n>BigInt(max)*r.d)throw Error(name+': maximum is '+max+'.');return r}function whole(s,name,max){if(!/^[0-9]+$/.test(s)||s.length>15||Number(s)>max)throw Error(name+': enter an integer from 0 to '+max+'.');return Number(s)}function onForm(fn,clear){$('form').addEventListener('submit',e=>{e.preventDefault();fn()});$('form').addEventListener('input',clear);$('form').addEventListener('change',clear)}

const original={responses:[{id:'A',label:'Meter, m'},{id:'B',label:'Kilogram, kg'},{id:'C',label:'Second, s'},{id:'D',label:'Kelvin, K'}],sources:[{id:'U1',response:'A',text:'Length uses m in handout U.'},{id:'U2',response:'B',text:'Mass uses kg in handout U.'},{id:'U3',response:'C',text:'Elapsed time uses s in handout U.'},{id:'U4',response:'D',text:'Temperature uses K in handout U.'}],premises:[{id:'P1',label:'Length of a desk edge',correct:'A',source:'U1'},{id:'P2',label:'Mass of a sample',correct:'B',source:'U2'},{id:'P3',label:'Elapsed time of a run',correct:'C',source:'U3'}]};let quiz=JSON.parse(JSON.stringify(original)),order=quiz.responses.map(x=>x.id);function validateSet(q){if(!q||!Array.isArray(q.premises)||!Array.isArray(q.responses)||!Array.isArray(q.sources))throw Error('Supply premises, responses, and sources arrays.');if(q.premises.length<3||q.premises.length>8||q.responses.length<4||q.responses.length>10||q.responses.length<=q.premises.length)throw Error('Use 3–8 premises, 4–10 responses, and more responses than premises.');for(const list of [q.premises,q.responses,q.sources])if(list.some(x=>!x||typeof x.id!=='string'||!/^[A-Za-z][A-Za-z0-9_-]{0,20}$/.test(x.id))||new Set(list.map(x=>x.id)).size!==list.length)throw Error('Each list needs unique short letter/number IDs.');if([...q.premises,...q.responses].some(x=>typeof x.label!=='string'||!x.label.trim()||x.label.length>160))throw Error('Supply brief nonempty labels up to 160 characters.');if(new Set(q.premises.map(x=>x.correct)).size!==q.premises.length)throw Error('Correct responses must be distinct under the one-use rule.');for(const p of q.premises){const s=q.sources.find(x=>x.id===p.source);if(!q.responses.some(x=>x.id===p.correct)||!s||s.response!==p.correct||typeof s.text!=='string'||!s.text.trim())throw Error('Every key needs a known response and matching supplied source record.')}return q}function grade(q,choices){if(choices.length!==q.premises.length||choices.some(x=>!x))throw Error('Incomplete: answer every premise before checking.');if(choices.some(x=>!q.responses.some(r=>r.id===x)))throw Error('Unknown response ID; no score.');if(new Set(choices).size!==choices.length)throw Error('Reuse violation: use each response at most once. No score.');return q.premises.filter((p,i)=>p.correct===choices[i]).length}function clear(){ $('feedback').textContent='';$('error').textContent='';}function render(choices=[]){$('premises').innerHTML=quiz.premises.map((p,i)=>'<label for="answer-'+i+'" class="premise"><b>'+esc(p.id)+'</b> · '+esc(p.label)+'<select id="answer-'+i+'"><option value="">Unanswered — choose an ID</option>'+order.map(id=>{const r=quiz.responses.find(x=>x.id===id);return '<option value="'+esc(id)+'">'+esc(id+' · '+r.label)+'</option>'}).join('')+'</select></label>').join('');quiz.premises.forEach((_,i)=>{$('answer-'+i).value=choices[i]||'';$('answer-'+i).onchange=clear});$('bank').innerHTML=order.map(id=>{const r=quiz.responses.find(x=>x.id===id);return '<p><b>'+esc(id)+'</b> '+esc(r.label)+'</p>'}).join('');$('handout').textContent=quiz.sources.map(s=>s.id+': '+s.text+' Response '+s.response+'.').join(' ');$('teacher').innerHTML='<p>'+quiz.premises.map(p=>esc(p.id+' → '+p.correct+' · '+p.source+': '+quiz.sources.find(s=>s.id===p.source).text)).join('<br>')+'</p><p>Unused response IDs: '+quiz.responses.filter(r=>!quiz.premises.some(p=>p.correct===r.id)).map(x=>esc(x.id)).join(', ')+'</p>';clear()}
$('check').onclick=()=>{clear();try{const choices=quiz.premises.map((_,i)=>$('answer-'+i).value),score=grade(quiz,choices);$('feedback').innerHTML='<h2>'+score+' / '+quiz.premises.length+' correct pairs</h2>'+quiz.premises.map((p,i)=>'<p><b>'+esc(p.id)+': '+(choices[i]===p.correct?'Matches':'Does not match')+'</b> · '+esc(p.source+': '+quiz.sources.find(s=>s.id===p.source).text)+'</p>').join('')}catch(e){$('error').textContent=e.message}};$('reset').onclick=()=>render();$('reorder').onclick=()=>{const choices=quiz.premises.map((_,i)=>$('answer-'+i).value);order=order.join(',')==='A,B,C,D'?['D','B','A','C']:[...order].reverse();render(choices)};$('author').value=JSON.stringify(original,null,2);$('apply').onclick=()=>{try{const next=validateSet(JSON.parse($('author').value));quiz=next;order=quiz.responses.map(x=>x.id);render();$('authorError').textContent='';document.querySelector('.sheet-heading').innerHTML='<span class="tag">Supplied teacher set</span><span>'+quiz.premises.length+' questions · '+quiz.responses.length+' responses</span>';}catch(e){$('authorError').textContent=e.message;clear()}};validateSet(quiz);render();</script></body></html>