# A chemistry lab manual — experiments, hazards and a titration curve to play with

Recipe No. 24, Labs and workshops. From The know.sh Cookbook: https://know.sh/cookbook/chemistry-lab

Replace the photocopied worksheets with a lab manual that improves every year. Each experiment has the same six parts, and each hazard is a *Risk* section written from your own risk assessment. Beside the titration sits a model: add sodium hydroxide a drop at a time and watch the pH curve climb, jump and level off.

- For: a chemistry teacher writing a year of lab practicals
- You get: a manual of experiments, each hazard a Risk, and a titration to drive
- Time: an evening to draft, a week to check
- Made with: Shelf, Document, Section, Your AI assistant, Elements, Public link

## How it works

1. **Draft the experiments from last year’s sheets.** Give your assistant last year’s sheets and your risk assessments. Ask for a document per experiment in six parts, a section per procedure step, and a *Risk* section per hazard.
2. **Check every hazard against the risk assessment.** Read each *Risk* section beside the risk assessment and the safety data sheet. Where they disagree, the risk assessment wins. Mark every hazard **Key**.
3. **Ask for a titration curve to play with.** Ask for a widget element in the pre-lab: pick the acid, add sodium hydroxide, watch the pH. Check it reads 4.76 at half-equivalence for ethanoic acid.
4. **Share each experiment, answers left out.** Share each experiment with the class, leaving the answer key out of the link. After each practical, have your assistant file what went wrong as *Problem* sections.

## Try this prompt

Your assistant, connected to know.sh (Claude, ChatGPT or a local model):

> In the Pre-lab section of Experiment 7 in my know.sh library, add a widget element: an acid–base titration model. Let the reader choose hydrochloric or ethanoic acid (pKa 4.76) and the concentrations, add sodium hydroxide with a slider and a one-drop button, and plot pH against volume with the equivalence and half-equivalence points marked. Suggest an indicator, and say in the caption what the model leaves out.

Your assistant, connected to know.sh (Claude, ChatGPT or a local model):

> Using know.sh, make a shelf called Chemistry practicals and a document per lab sheet I have given you, titled “Experiment N — the question it answers”, with six sections: Pre-lab, Safety, Procedure, Data table, Calculations, Post-lab questions. Make each hazard a Risk section, taken only from my risk assessments, and file a Question wherever a figure is missing.

## Elements in the specimens

Written as your assistant writes them through the know.sh MCP server. Each reads as plain words until you turn elements on (Account → Elements).

Widget, specimen 1:

```element widget
{
  "title": "An acid–base titration",
  "caption": "Pick the acid, its concentration and volume, and the sodium hydroxide; then add titrant a drop or a few millilitres at a time. The pH is solved exactly from the charge balance, and the note beside it shows the by-hand route for the region you are in. It assumes 25 °C (Kw = 1.0 × 10⁻¹⁴), ideal dilute solutions, one proton per acid and no carbon dioxide from the air; indicator ranges are the usual data-book figures, and an indicator is suggested when its mid-colour falls within 0.10 mL of equivalence.",
  "height": 730,
  "html": "<div class=\"tit\">\n  <div class=\"setup\">\n    <label class=\"wide\">Acid in the flask\n      <select id=\"acid\">\n        <option value=\"hcl\">Hydrochloric acid, strong</option>\n        <option value=\"ethanoic\" selected>Ethanoic (acetic) acid, weak, pKa 4.76</option>\n        <option value=\"methanoic\">Methanoic (formic) acid, weak, pKa 3.75</option>\n      </select>\n    </label>\n    <label>Acid concentration\n      <select id=\"ca\"><option value=\"0.05\">0.050 mol/L</option><option value=\"0.1\" selected>0.100 mol/L</option><option value=\"0.2\">0.200 mol/L</option></select>\n    </label>\n    <label>Acid volume, by pipette\n      <select id=\"va\"><option value=\"10\">10.00 mL</option><option value=\"20\">20.00 mL</option><option value=\"25\" selected>25.00 mL</option></select>\n    </label>\n    <label>NaOH in the burette\n      <select id=\"cb\"><option value=\"0.05\">0.050 mol/L</option><option value=\"0.1\" selected>0.100 mol/L</option><option value=\"0.2\">0.200 mol/L</option></select>\n    </label>\n  </div>\n\n  <svg id=\"plot\" role=\"img\" aria-label=\"Titration curve\"></svg>\n\n  <div class=\"burette\">\n    <label class=\"slide\"><span>NaOH added <b id=\"vbOut\">0.00 mL</b></span>\n      <input id=\"vb\" type=\"range\" min=\"0\" max=\"50\" step=\"0.05\" value=\"0\" aria-describedby=\"say\">\n    </label>\n    <div class=\"adds\" role=\"group\" aria-label=\"Add sodium hydroxide from the burette\">\n      <button type=\"button\" data-add=\"0.05\">One drop</button>\n      <button type=\"button\" data-add=\"0.5\">+0.5 mL</button>\n      <button type=\"button\" data-add=\"1\">+1 mL</button>\n      <button type=\"button\" data-add=\"5\">+5 mL</button>\n      <label class=\"check\"><input type=\"checkbox\" id=\"ahead\" checked> Show the curve to come</label>\n      <button type=\"button\" id=\"again\">Start again</button>\n    </div>\n  </div>\n\n  <dl class=\"figures\">\n    <div><dt>pH now</dt><dd id=\"pOut\">–</dd></div>\n    <div><dt>Equivalence point</dt><dd id=\"eqOut\">–</dd></div>\n    <div><dt>Half-equivalence</dt><dd id=\"halfOut\">–</dd></div>\n    <div><dt>Indicator</dt><dd id=\"indOut\">–</dd></div>\n  </dl>\n  <p class=\"say\" id=\"say\" aria-live=\"polite\"></p>\n  <p class=\"ind\" id=\"ind\" aria-live=\"polite\"></p>\n</div>",
  "css": ".tit { font-family: var(--sans); }\n.setup { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px 16px; }\n.setup .wide { grid-column: 1 / -1; }\n.setup label, .slide { display: grid; gap: 3px; }\n.setup select { width: 100%; min-height: 36px; }\n#plot { display: block; width: 100%; margin-top: 12px; overflow: visible; }\n.axis { stroke: var(--ink); stroke-width: 1; fill: none; }\n.grid { stroke: var(--rule); stroke-width: 1; }\n.tick { font: 400 11px var(--sans); fill: var(--ink-3); font-variant-numeric: tabular-nums; }\n.atitle { font: 400 11.5px var(--sans); fill: var(--ink-2); }\n.band { fill: var(--wash); }\n.bandlab { font: italic 400 11.5px var(--serif); fill: var(--ink-2); }\n.ahead { fill: none; stroke: var(--ink-3); stroke-width: 1.2; stroke-dasharray: 3 3; }\n.done { fill: none; stroke: var(--ink); stroke-width: 2.2; stroke-linejoin: round; }\n.mark { fill: var(--paper); stroke: var(--ink); stroke-width: 1.5; }\n.now { fill: var(--ink); }\n.guide { stroke: var(--ink-2); stroke-width: 1; stroke-dasharray: 1 3; }\n.marklab { font: 500 11.5px var(--sans); fill: var(--ink); }\n.halo { stroke: var(--paper); stroke-width: 4px; stroke-linejoin: round; paint-order: stroke; }\n.burette { margin-top: 6px; }\n.slide b { font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; }\n.slide input { width: 100%; }\n.adds { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 8px; }\n.adds button { font: 500 13.5px/1 var(--sans); color: var(--ink); background: var(--paper); border: 1px solid var(--ink); padding: 9px 11px; min-height: 38px; }\n.adds button:active { background: var(--ink); color: var(--paper); }\n.adds #again { margin-left: auto; border-color: var(--rule); color: var(--ink-2); }\n.check { display: flex; align-items: center; gap: 6px; margin-left: 6px; }\n.figures { display: grid; grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); gap: 0 18px; margin: 12px 0 0; border-top: 1px solid var(--ink); }\n.figures div { padding: 6px 0; border-bottom: 1px solid var(--rule); }\n.figures dt { font: 400 12.5px/1.3 var(--sans); color: var(--ink-3); }\n.figures dd { margin: 2px 0 0; font: 400 16px/1.3 var(--serif); color: var(--ink); font-variant-numeric: tabular-nums; }\n.say, .ind { margin: 8px 0 0; font: 400 15px/1.45 var(--serif); color: var(--ink); }\n.ind { color: var(--ink-2); }\n.say i, .ind i { font-style: italic; }\nbutton:focus-visible, input:focus-visible, select:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }\n@media (max-width: 420px) { .adds #again { margin-left: 0; } }",
  "js": "(function () {\n  var $ = function (id) { return document.getElementById(id); };\n  var svg = $('plot');\n  var KW = 1e-14, DROP = 0.05, NEAR = 0.1, BURETTE = 50;\n  var ACIDS = {\n    hcl: { name: 'hydrochloric acid', strong: true, salt: 'sodium chloride' },\n    ethanoic: { name: 'ethanoic acid', pKa: 4.76, ion: 'ethanoate', salt: 'sodium ethanoate' },\n    methanoic: { name: 'methanoic acid', pKa: 3.75, ion: 'methanoate', salt: 'sodium methanoate' }\n  };\n  // Transition ranges as school data books give them, with the colour on each side.\n  var INDICATORS = [\n    { name: 'Methyl orange', lo: 3.1, hi: 4.4, acid: 'red', base: 'yellow' },\n    { name: 'Methyl red', lo: 4.4, hi: 6.2, acid: 'red', base: 'yellow' },\n    { name: 'Bromothymol blue', lo: 6.0, hi: 7.6, acid: 'yellow', base: 'blue' },\n    { name: 'Phenolphthalein', lo: 8.2, hi: 10.0, acid: 'colourless', base: 'pink' },\n    { name: 'Thymolphthalein', lo: 9.3, hi: 10.5, acid: 'colourless', base: 'blue' }\n  ];\n  var s = { acid: 'ethanoic', ca: 0.1, va: 25, cb: 0.1, vb: 0, ahead: true };\n  var cache = null;\n\n  function acid() { return ACIDS[s.acid]; }\n  function ka() { var a = acid(); return a.strong ? Infinity : Math.pow(10, -a.pKa); }\n  function veq() { return s.ca * s.va / s.cb; }\n  function f2(x) { return (Math.round(x * 100) / 100).toFixed(2); }\n  function mmol(x) { return x < 0.1 ? x.toFixed(3) : f2(x); }\n\n  // The exact pH, ignoring activities: solve the charge balance\n  // [H+] + [Na+] = [OH-] + [A-], with [A-] = C·Ka/(Ka + [H+]) for a weak acid\n  // and [A-] = C for a strong one. The left side minus the right rises with\n  // [H+], so bisection on pH always finds the one root.\n  function pHAt(vb) {\n    var vt = s.va + vb, c = s.ca * s.va / vt, na = s.cb * vb / vt, k = ka(), lo = -2, hi = 16;\n    for (var i = 0; i < 70; i++) {\n      var mid = (lo + hi) / 2, h = Math.pow(10, -mid);\n      var a = k === Infinity ? c : c * k / (k + h);\n      if (h + na - KW / h - a > 0) lo = mid; else hi = mid;\n    }\n    return (lo + hi) / 2;\n  }\n\n  function sci(x) {\n    if (x >= 0.001) return x.toPrecision(3);\n    var e = Math.floor(Math.log10(x)), m = x / Math.pow(10, e);\n    if (m >= 9.995) { m /= 10; e += 1; }\n    var sup = { '-': '⁻', '0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴', '5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹' };\n    return m.toFixed(2) + ' × 10' + String(e).split('').map(function (ch) { return sup[ch]; }).join('');\n  }\n\n  // The route a student takes by hand in each region, and its answer.\n  function byHand(vb) {\n    var a = acid(), nA = s.ca * s.va, nB = s.cb * vb, vt = s.va + vb, rel = (nB - nA) / nA, excess, conc, p;\n    var tot = 'total volume ' + f2(vt) + ' mL';\n    if (Math.abs(rel) < 1e-9) {\n      if (a.strong) return { p: 7, text: '<b>At equivalence.</b> The flask holds only ' + a.salt + ' and water, a neutral salt, so pH = 7.00.' };\n      var c = nA / vt, kb = KW / ka(), oh = Math.sqrt(kb * c);\n      p = 14 + Math.log10(oh);\n      return { p: p, text: '<b>At equivalence.</b> Every acid molecule has become ' + a.ion + ', a weak base that takes H⁺ from water. By hand: K<sub>b</sub> = K<sub>w</sub> ÷ K<sub>a</sub> = ' + sci(kb) + ', c = ' + mmol(nA) + ' mmol ÷ ' + f2(vt) + ' mL = ' + sci(c) + ' mol/L, [OH⁻] = √(K<sub>b</sub>·c) = ' + sci(oh) + ', so pH = 14 − pOH = ' + f2(p) + '.' };\n    }\n    if (nB > nA) {\n      excess = nB - nA; conc = excess / vt; p = 14 + Math.log10(conc);\n      return { p: p, text: '<b>After equivalence.</b> Excess sodium hydroxide sets the pH' + (a.strong ? '' : ' and swamps the ' + a.ion) + '. By hand: [OH⁻] = ' + mmol(excess) + ' mmol ÷ ' + f2(vt) + ' mL = ' + sci(conc) + ' mol/L, so pH = 14 − pOH = ' + f2(p) + '.' };\n    }\n    if (a.strong) {\n      excess = nA - nB; conc = excess / vt; p = -Math.log10(conc);\n      return { p: p, text: (vb === 0 ? '<b>Acid alone.</b> Hydrochloric acid ionises completely' : '<b>Before equivalence.</b> Hydroxide has neutralised part of the acid; the rest ionises completely') + '. By hand: [H⁺] = ' + mmol(excess) + ' mmol ÷ ' + f2(vt) + ' mL = ' + sci(conc) + ' mol/L, so pH = ' + f2(p) + '.' };\n    }\n    if (vb === 0) {\n      p = 0.5 * (a.pKa - Math.log10(s.ca));\n      return { p: p, text: '<b>Acid alone.</b> A weak acid ionises only a little. By hand: [H⁺] ≈ √(K<sub>a</sub>·c), so pH ≈ ½(pK<sub>a</sub> − log c) = ½(' + a.pKa.toFixed(2) + ' − log ' + s.ca.toFixed(3) + ') = ' + f2(p) + '.' };\n    }\n    p = a.pKa + Math.log10(nB / (nA - nB));\n    return { p: p, text: '<b>Buffer region.</b> The flask holds ' + a.name + ' and ' + a.ion + ' together. By hand (Henderson–Hasselbalch): pH = pK<sub>a</sub> + log(' + a.ion + ' ÷ acid) = ' + a.pKa.toFixed(2) + ' + log(' + mmol(nB) + ' mmol ÷ ' + mmol(nA - nB) + ' mmol) = ' + f2(p) + '.' };\n  }\n\n  function setup() {\n    var v = veq(), a = acid(), xmax = Math.max(10, Math.ceil(2 * v / 5) * 5), xs = [], i, d;\n    for (i = 0; i <= 320; i++) xs.push(xmax * i / 320);\n    [1e-5, 3e-5, 1e-4, 3e-4, 1e-3, 2e-3, 5e-3, 1e-2, 2e-2, 4e-2, 7e-2].forEach(function (d) { xs.push(v * (1 - d)); xs.push(v * (1 + d)); xs.push(v * d); });\n    xs.push(v);\n    xs = xs.filter(function (x) { return x >= 0 && x <= xmax; }).sort(function (p, q) { return p - q; });\n    var pts = xs.map(function (x) { return [x, pHAt(x)]; });\n    // An indicator suits the titration when its mid-colour is reached within\n    // 0.10 mL (two drops) either side of equivalence.\n    var lo = pHAt(Math.max(0, v - NEAR)), hi = pHAt(v + NEAR), pe = pHAt(v);\n    var mid = function (n) { return (n.lo + n.hi) / 2; };\n    var fits = INDICATORS.filter(function (n) { return mid(n) >= lo && mid(n) <= hi; });\n    var closest = function (list) { return list.slice().sort(function (p, q) { return Math.abs((p.lo + p.hi) / 2 - pe) - Math.abs((q.lo + q.hi) / 2 - pe); })[0]; };\n    var pick = fits.length ? closest(fits) : closest(INDICATORS);\n    cache = { v: v, xmax: xmax, pts: pts, jump: [lo, hi], pe: pe, fits: fits, pick: pick, half: a.strong ? null : pHAt(v / 2) };\n    var slider = $('vb'); slider.max = xmax;\n    if (s.vb > xmax) s.vb = xmax;\n  }\n\n  function draw() {\n    var c = cache, a = acid();\n    var W = Math.max(260, Math.round(svg.getBoundingClientRect().width) || 588), H = W < 440 ? 230 : 252;\n    var L = 30, R = 12, T = 12, B = 34, pw = W - L - R, ph = H - T - B;\n    var X = function (v) { return L + v / c.xmax * pw; };\n    var Y = function (p) { return T + (14 - Math.max(0, Math.min(14, p))) / 14 * ph; };\n    var out = [], i;\n    svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H); svg.setAttribute('height', H);\n\n    var n = c.pick;\n    out.push('<rect class=\"band\" x=\"' + L + '\" y=\"' + Y(n.hi) + '\" width=\"' + pw + '\" height=\"' + (Y(n.lo) - Y(n.hi)) + '\"/>');\n    for (i = 0; i <= 14; i += 2) {\n      out.push('<line class=\"grid\" x1=\"' + L + '\" x2=\"' + (L + pw) + '\" y1=\"' + Y(i) + '\" y2=\"' + Y(i) + '\"/>');\n      out.push('<text class=\"tick\" x=\"' + (L - 6) + '\" y=\"' + (Y(i) + 4) + '\" text-anchor=\"end\">' + i + '</text>');\n    }\n    var steps = [1, 2, 5, 10, 20, 25, 50], step = steps.filter(function (st) { return c.xmax / st <= (W < 440 ? 5 : 8); })[0] || 50;\n    for (var x = 0; x <= c.xmax + 1e-9; x += step) {\n      out.push('<line class=\"axis\" x1=\"' + X(x) + '\" x2=\"' + X(x) + '\" y1=\"' + (T + ph) + '\" y2=\"' + (T + ph + 4) + '\"/>');\n      out.push('<text class=\"tick\" x=\"' + X(x) + '\" y=\"' + (T + ph + 16) + '\" text-anchor=\"middle\">' + x + '</text>');\n    }\n    out.push('<path class=\"axis\" d=\"M' + L + ' ' + T + 'V' + (T + ph) + 'H' + (L + pw) + '\"/>');\n    out.push('<text class=\"atitle\" x=\"' + (L + pw) + '\" y=\"' + (H - 2) + '\" text-anchor=\"end\">NaOH added, mL</text>');\n    out.push('<text class=\"atitle\" x=\"' + (L + 4) + '\" y=\"' + (T + 10) + '\">pH</text>');\n    out.push('<text class=\"bandlab halo\" x=\"' + (L + 6) + '\" y=\"' + (Y(n.hi) - 4) + '\">' + n.name.toLowerCase() + ' changes colour</text>');\n\n    var line = function (pts) { return pts.map(function (p, j) { return (j ? 'L' : 'M') + X(p[0]).toFixed(1) + ' ' + Y(p[1]).toFixed(1); }).join(''); };\n    var pNow = pHAt(s.vb);\n    var done = c.pts.filter(function (p) { return p[0] < s.vb; }).concat([[s.vb, pNow]]);\n    var rest = [[s.vb, pNow]].concat(c.pts.filter(function (p) { return p[0] > s.vb; }));\n    if (s.ahead && rest.length > 1) out.push('<path class=\"ahead\" d=\"' + line(rest) + '\"/>');\n    if (done.length > 1) out.push('<path class=\"done\" d=\"' + line(done) + '\"/>');\n\n    var ex = X(c.v), ey = Y(c.pe), right = ex < L + pw * 0.62;\n    out.push('<line class=\"guide\" x1=\"' + ex + '\" x2=\"' + ex + '\" y1=\"' + T + '\" y2=\"' + (T + ph) + '\"/>');\n    out.push('<circle class=\"mark\" cx=\"' + ex + '\" cy=\"' + ey + '\" r=\"4.5\"/>');\n    out.push('<text class=\"marklab halo\" x=\"' + (ex + (right ? 7 : -7)) + '\" y=\"' + (T + 12) + '\" text-anchor=\"' + (right ? 'start' : 'end') + '\">Equivalence</text>');\n    if (c.half !== null) {\n      var hx = X(c.v / 2), hy = Y(c.half);\n      out.push('<line class=\"guide\" x1=\"' + L + '\" x2=\"' + hx + '\" y1=\"' + hy + '\" y2=\"' + hy + '\"/>');\n      out.push('<circle class=\"mark\" cx=\"' + hx + '\" cy=\"' + hy + '\" r=\"4.5\"/>');\n      out.push('<text class=\"marklab halo\" x=\"' + (hx + 7) + '\" y=\"' + (hy + 16) + '\">Half-equivalence: pH = pKa</text>');\n    }\n    out.push('<circle class=\"now\" cx=\"' + X(s.vb) + '\" cy=\"' + Y(pNow) + '\" r=\"5\"/>');\n    svg.innerHTML = out.join('');\n    svg.setAttribute('aria-label', 'Titration curve of pH against NaOH added, 0 to ' + c.xmax + ' mL, for ' + s.ca.toFixed(3) + ' mol/L ' + a.name + '. It rises steeply at the equivalence point, ' + f2(c.v) + ' mL, pH ' + f2(c.pe) + '. Now at ' + f2(s.vb) + ' mL, pH ' + f2(pNow) + '.');\n    return pNow;\n  }\n\n  function colourIn(n, p) {\n    if (p < n.lo) return n.acid;\n    if (p > n.hi) return n.base;\n    return 'changing, between ' + n.acid + ' and ' + n.base;\n  }\n\n  function render() {\n    var c = cache, a = acid(), p = draw(), hand = byHand(s.vb), n = c.pick;\n    $('vb').value = s.vb;\n    $('vbOut').textContent = f2(s.vb) + ' mL';\n    $('pOut').textContent = f2(p);\n    $('eqOut').textContent = f2(c.v) + ' mL, pH ' + f2(c.pe);\n    $('halfOut').textContent = c.half === null ? 'None: strong acid' : f2(c.v / 2) + ' mL, pH ' + f2(c.half);\n    $('indOut').textContent = n.name + ', ' + n.lo.toFixed(1) + '–' + n.hi.toFixed(1);\n    var say = hand.text;\n    if (Math.abs(hand.p - p) >= 0.05) say += ' <i>The shortcut is off by ' + Math.abs(hand.p - p).toFixed(2) + ' here: so close to the start or to equivalence it leaves out the acid’s own ionisation or water’s. The exact pH, ' + f2(p) + ', allows for both.</i>';\n    if (c.v > BURETTE) say += ' <i>Equivalence needs more than one 50 mL burette: take a smaller aliquot of acid or a stronger NaOH.</i>';\n    $('say').innerHTML = say;\n    var others = c.fits.filter(function (m) { return m !== n; }).map(function (m) { return m.name.toLowerCase(); });\n    var why = c.fits.length\n      ? n.name + ' suits it best: its mid-colour, pH ' + ((n.lo + n.hi) / 2).toFixed(1) + ', is nearest the equivalence pH and falls inside the jump, where 0.10 mL (two drops) either side of equivalence takes the pH from ' + f2(c.jump[0]) + ' to ' + f2(c.jump[1]) + '.' + (others.length ? ' Also inside it: ' + others.join(', ') + '.' : '')\n      : 'No common indicator reaches its mid-colour within 0.10 mL of equivalence (pH ' + f2(c.jump[0]) + ' to ' + f2(c.jump[1]) + '); ' + n.name.toLowerCase() + ' comes closest. Add the last millilitre drop by drop, or use a pH meter.';\n    $('ind').textContent = why + ' In the flask now: ' + colourIn(n, p) + '.';\n  }\n\n  function set(v) { s.vb = Math.max(0, Math.min(cache.xmax, Math.round(v / DROP) * DROP)); s.vb = Math.round(s.vb * 100) / 100; render(); }\n\n  ['acid', 'ca', 'va', 'cb'].forEach(function (id) {\n    $(id).addEventListener('change', function () {\n      s[id] = id === 'acid' ? this.value : Number(this.value);\n      s.vb = 0; setup(); render();\n    });\n  });\n  $('vb').addEventListener('input', function () { set(Number(this.value)); });\n  document.querySelectorAll('[data-add]').forEach(function (b) {\n    b.addEventListener('click', function () { set(s.vb + Number(b.getAttribute('data-add'))); });\n  });\n  $('again').addEventListener('click', function () { set(0); });\n  $('ahead').addEventListener('change', function () { s.ahead = this.checked; render(); });\n  document.addEventListener('know-tokens', render);\n  var lastW = 0;\n  if (window.ResizeObserver) new ResizeObserver(function (en) { var w = Math.round(en[0].contentRect.width); if (w !== lastW) { lastW = w; requestAnimationFrame(render); } }).observe(svg.parentNode);\n  else window.addEventListener('resize', render);\n\n  setup(); render();\n})();"
}
```

Table, specimen 3:

```element table
{
  "caption": "Burette readings, vinegar diluted ten times, 25.00 mL aliquots",
  "columns": [
    {
      "label": "Trial",
      "align": "left"
    },
    {
      "label": "Final reading, mL",
      "align": "right",
      "numeric": true
    },
    {
      "label": "Initial reading, mL",
      "align": "right",
      "numeric": true
    },
    {
      "label": "Titre, mL",
      "align": "right",
      "numeric": true
    }
  ],
  "rows": [
    [
      "Rough",
      "21.30",
      "0.20",
      "21.10"
    ],
    [
      "1",
      "20.75",
      "0.20",
      "20.55"
    ],
    [
      "2",
      "21.05",
      "0.55",
      "20.50"
    ],
    [
      "3",
      "20.90",
      "0.35",
      "20.55"
    ]
  ],
  "notes": "Trials 1 to 3 agree within 0.10 mL, so they are concordant; the rough titre is left out of the mean. Mean titre: **20.53 mL**."
}
```
