feat: C — extract Bedside reference into ES modules

Split the Bedside clinical reference section out of calculators.js
(~1220 lines) into 20 focused ES modules under public/js/bedside/.
Each module owns one clinical topic (cardiac, seizure, sepsis, burns,
etc.) and exports an init() wired up by bedside/index.js. Shared
helpers live in shared.js and also set window._EM for back-compat
with the 4 remaining call sites in calculators.js.

Load order: classic defer calculators.js first, then module script
bedside/index.js. Handlers bind at DOM-ready; runtime _EM lookups
resolve after both are evaluated.

Makes bedside content editable per-section, shrinks calculators.js
from 4111 to 2891 lines, and keeps the existing Playwright smoke
suite (e2e/tests/bedside-smoke.spec.js) as the behavioral contract.
This commit is contained in:
Daniel 2026-04-20 22:16:43 +02:00
parent c4e30b4f3e
commit ddf9ca805b
23 changed files with 1978 additions and 1220 deletions

View file

@ -18,6 +18,7 @@
<script defer src="/js/calc-math.js"></script>
<script defer src="/js/drugs-loader.js"></script>
<script defer src="/js/calculators.js"></script>
<script type="module" src="/js/bedside/index.js"></script>
<script defer src="/js/e2e-bootstrap.js"></script>
</body>
</html>

View file

@ -447,6 +447,7 @@
<script defer src="/js/calc-math.js"></script>
<script defer src="/js/drugs-loader.js"></script>
<script defer src="/js/calculators.js"></script>
<script type="module" src="/js/bedside/index.js"></script>
<script defer src="/js/learningHub.js"></script>
<script defer src="/js/admin.js"></script>
<script defer src="/js/adminMilestones.js"></script>

View file

@ -0,0 +1,80 @@
// ============================================================
// bedside/age-weight.js
// BEDSIDE — AGE → WEIGHT (single source for all sub-sections).
// Top estimator wiring: #bedside-age, #bedside-formula,
// #bedside-weight, #btn-bedside-clear.
// ============================================================
export function init() {
document.addEventListener('input', function(e) {
if (e.target && e.target.id === 'bedside-age') recomputeFromAge();
if (e.target && e.target.id === 'bedside-weight') {
e.target.dataset.userTyped = '1';
}
});
document.addEventListener('change', function(e) {
if (e.target && e.target.id === 'bedside-formula') recomputeFromAge(true);
});
document.addEventListener('click', function(e) {
if (e.target.closest && e.target.closest('#btn-bedside-clear')) {
document.getElementById('bedside-age').value = '';
var wtField = document.getElementById('bedside-weight');
wtField.value = '';
delete wtField.dataset.userTyped;
var note = document.getElementById('bedside-estimate-note');
if (note) note.innerHTML = '';
}
});
// Recompute weight from age using the currently selected formula.
// If forceFromFormula is true (formula dropdown changed), overwrite even user-typed weight.
function recomputeFromAge(forceFromFormula) {
var raw = document.getElementById('bedside-age').value;
var note = document.getElementById('bedside-estimate-note');
if (!raw || !raw.trim()) { if (note) note.innerHTML = ''; return; }
var months = window._parseAgeMonths ? window._parseAgeMonths(raw) : null;
if (months == null || isNaN(months) || months < 0) {
if (note) note.innerHTML = '<span style="color:var(--red);">Could not parse age. Try "3y", "18 months", "15 days".</span>';
return;
}
var formula = document.getElementById('bedside-formula').value;
var est = window._PED_MATH.estimateWeightFromAgeMonths(months);
var pickedWeight = formula === 'bestguess' ? est.all.bestGuess : est.all.apls;
var pickedLabel = formula === 'bestguess' ? bestGuessBand(months) : aplsBand(months);
var wtField = document.getElementById('bedside-weight');
if (forceFromFormula || !wtField.dataset.userTyped) {
wtField.value = pickedWeight;
delete wtField.dataset.userTyped; // treat as computed again
}
var ageTxt = months < 12
? (Math.round(months * 10) / 10) + ' months'
: (Math.round(months / 12 * 10) / 10) + ' years';
if (note) note.innerHTML = '<strong>' + pickedWeight + ' kg</strong> ' +
'<span style="color:var(--g500);">(' + ageTxt + ', ' + pickedLabel + ')</span> · ' +
'APLS: ' + est.all.apls + ' kg · Best Guess: ' + est.all.bestGuess + ' kg. <em>You can override the weight field.</em>';
}
function aplsBand(m) {
if (m < 12) return 'APLS 0-12 mo';
var y = m / 12;
if (y <= 5) return 'APLS 1-5 yr';
return 'APLS 6-12 yr';
}
function bestGuessBand(m) {
if (m < 12) return 'Best Guess 1-11 mo';
var y = m / 12;
if (y <= 5) return 'Best Guess 1-4 yr';
return 'Best Guess 5-14 yr';
}
// Top bedside weight is estimator-only. Sections have their own weight fields and
// read them directly (no auto-propagation). Expose the reader on window for
// safety (some older code paths may look it up).
if (typeof window !== 'undefined') {
window._getBedsideWeight = function() {
var el = document.getElementById('bedside-weight');
return el ? parseFloat(el.value) || null : null;
};
}
}

View file

@ -0,0 +1,83 @@
// ============================================================
// bedside/agitation.js
// AGITATION — step 1/2/3 pathway, drugs.json-backed.
// ============================================================
import { S } from './shared.js';
// Fallback inline data — mirrors drugs.json agitation section, split into
// step-2 (PO/IN) and step-3 (IV/IM) subgroups by the route field.
// Display-name column intentionally drops the route suffix from the JSON
// name so the table still shows "Lorazepam" / "Midazolam" / "Ketamine".
var AGIT_FALLBACK = [
// Step 2 — PO / IN
{ name: 'Lorazepam PO', display: 'Lorazepam', step: 2, dose_mg_per_kg: 0.05, max_mg: 2, unit: 'mg', route: 'PO', notes: '0.05 mg/kg. May repeat in 30 min.' },
{ name: 'Midazolam PO', display: 'Midazolam', step: 2, dose_mg_per_kg: 0.5, max_mg: 10, unit: 'mg', route: 'PO', notes: '0.5 mg/kg (max 10 mg)' },
{ name: 'Midazolam IN', display: 'Midazolam', step: 2, dose_mg_per_kg: 0.3, max_mg: 10, unit: 'mg', route: 'IN (5 mg/mL)', notes: '0.3 mg/kg split between nares (max 10 mg)' },
{ name: 'Olanzapine (ODT)', display: 'Olanzapine (ODT)', step: 2, dose_mg_per_kg: null, max_mg: null, unit: 'mg', route: 'PO / ODT', notes: 'Age ≥6 yr. Avoid IM + benzo combo (risk of resp depression).', dose_display_if_under_30kg: '2.5-5 mg', dose_display_if_30kg_or_more: '5-10 mg' },
{ name: 'Diphenhydramine PO', display: 'Diphenhydramine', step: 2, dose_mg_per_kg: 1, max_mg: 50, unit: 'mg', route: 'PO', notes: '1 mg/kg. Adjunct only; sedating.' },
// Step 3 — IV / IM
{ name: 'Midazolam IV/IM', display: 'Midazolam', step: 3, dose_mg_per_kg: 0.1, max_mg: 5, unit: 'mg', route: 'IV / IM', notes: '0.05-0.1 mg/kg IV, 0.1-0.15 mg/kg IM (max 10 mg)' },
{ name: 'Lorazepam IV/IM', display: 'Lorazepam', step: 3, dose_mg_per_kg: 0.1, max_mg: 4, unit: 'mg', route: 'IV / IM', notes: '0.05-0.1 mg/kg (max 4 mg). May cause resp depression.' },
{ name: 'Haloperidol', display: 'Haloperidol', step: 3, dose_mg_per_kg: 0.05, max_mg: null, unit: 'mg', route: 'IM / IV', notes: 'Avoid &lt;3 yr. Risk: QT, EPS, NMS. ECG if repeated.', dose_display_if_under_40kg: '0.025-0.075 mg/kg', dose_display_if_40kg_or_more: '2.5-5 mg' },
{ name: 'Olanzapine IM', display: 'Olanzapine', step: 3, dose_mg_per_kg: null, max_mg: null, unit: 'mg', route: 'IM', notes: 'Avoid benzo co-administration (resp depression, hypotension)', dose_display_if_under_40kg: '2.5-5 mg', dose_display_if_40kg_or_more: '5-10 mg' },
{ name: 'Ketamine IM', display: 'Ketamine', step: 3, dose_mg_per_kg: 4, max_mg: 500, unit: 'mg', route: 'IM', notes: '4-5 mg/kg IM (rescue for severe excited delirium). Monitor airway.' },
{ name: 'Droperidol', display: 'Droperidol', step: 3, dose_mg_per_kg: 0.05, max_mg: null, unit: 'mg', route: 'IM / IV', notes: 'Effective but QT concern — get ECG.', dose_display_prefix: '0.03-0.07 mg/kg' }
];
function agitDrugs() {
var s = window._DRUGS && window._DRUGS.sections && window._DRUGS.sections.agitation;
if (s && s.drugs && s.drugs.length) {
// Merge per-drug hints from fallback (display name, step) when the
// JSON doesn't carry them — keeps rendering identical.
return s.drugs.map(function(dg) {
var fb = AGIT_FALLBACK.filter(function(x) { return x.name === dg.name; })[0] || {};
return Object.assign({}, fb, dg);
});
}
return AGIT_FALLBACK;
}
// Render one drug row, handling the agitation-specific display quirks:
// weight-conditional dose strings (Olanzapine, Haloperidol) and hand-
// computed "prefix = value mg" strings (Droperidol, Haloperidol &lt;40 kg).
function agitRowFor(wt, dg) {
var dose;
if (dg.dose_display_if_under_30kg && dg.dose_display_if_30kg_or_more) {
dose = wt < 30 ? dg.dose_display_if_under_30kg : dg.dose_display_if_30kg_or_more;
} else if (dg.dose_display_if_under_40kg && dg.dose_display_if_40kg_or_more) {
dose = wt < 40 ? dg.dose_display_if_under_40kg + ' = ' + S.d(wt, dg.dose_mg_per_kg) + ' mg' : dg.dose_display_if_40kg_or_more;
} else if (dg.dose_display_prefix) {
dose = dg.dose_display_prefix + ' = ' + S.d(wt, dg.dose_mg_per_kg) + ' mg';
} else if (dg.dose_mg_per_kg != null) {
dose = S.dStr(wt, dg.dose_mg_per_kg, dg.max_mg, ' ' + dg.unit);
} else {
dose = dg.dose_display || '';
}
return S.drugRow(dg.display || dg.name, dose, dg.route, dg.notes);
}
function calcAgit() {
var wt = parseFloat(document.getElementById('agit-weight').value);
var el = document.getElementById('agit-result');
if (!wt) { el.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter weight (kg).</p>'; return; }
var drugs = agitDrugs();
var step2 = drugs.filter(function(dg) { return dg.step === 2; });
var step3 = drugs.filter(function(dg) { return dg.step === 3; });
var html = '<div style="padding:10px 14px;border-radius:8px;background:var(--purple-light);border:1.5px solid var(--purple);margin-bottom:10px;"><strong style="color:var(--purple);">Agitation management — ' + wt + ' kg</strong></div>';
html += S.stepBox('#10b981', 'Step 1 — Non-pharmacologic', 'Dim lights, quiet room, familiar caregiver present, reduce stimulation. Rule out hypoglycemia, hypoxia, pain, ↑ICP, toxic ingestion, infection.');
html += S.stepBox('#3b82f6', 'Step 2 — Oral / intranasal (cooperative)', 'First-line for moderate agitation if the patient will accept PO/IN.');
html += S.drugTable(step2.map(function(dg) { return agitRowFor(wt, dg); }).join(''));
html += S.stepBox('#f59e0b', 'Step 3 — IM / IV (severe, uncooperative, or safety risk)', 'Require continuous monitoring. Consider restraints only as last resort.');
html += S.drugTable(step3.map(function(dg) { return agitRowFor(wt, dg); }).join(''));
html += '<div style="padding:10px;background:#fef3c7;border-radius:6px;font-size:12px;color:#92400e;margin-top:10px;"><strong>Monitoring:</strong> Continuous SpO2 + HR after parenteral sedation. Have airway equipment, flumazenil, naloxone, and IV access ready.</div>';
html += S.ref('AAP Clinical Report on Pediatric Agitation. ACEP Guidelines for Acute Agitation.');
el.innerHTML = html;
}
export function init() {
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-agit-calc' || e.target.closest('#btn-agit-calc')) calcAgit();
});
}

View file

@ -0,0 +1,90 @@
// ============================================================
// bedside/airway.js
// AIRWAY / RSI.
// ============================================================
import { S } from './shared.js';
function calcAirway() {
var wt = parseFloat(document.getElementById('airway-weight').value);
var age = parseFloat(document.getElementById('airway-age').value);
var el = document.getElementById('airway-result');
if (!wt) { el.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter weight (kg).</p>'; return; }
// Equipment sizing
var ettUncuff = age ? Math.round(((age/4) + 4) * 2) / 2 : null;
var ettCuff = age ? Math.round(((age/4) + 3.5) * 2) / 2 : null;
var ettDepth = ettUncuff ? Math.round(ettUncuff * 3 * 10) / 10 : null;
var bladeSize = age != null ? (age < 1 ? 'Miller 0-1' : age < 2 ? 'Miller 1 / Mac 1' : age < 8 ? 'Miller 2 / Mac 2' : 'Miller/Mac 3') : '—';
var lmaSize = wt < 5 ? '1' : wt < 10 ? '1.5' : wt < 20 ? '2' : wt < 30 ? '2.5' : wt < 50 ? '3' : wt < 70 ? '4' : '5';
var html = '<div style="padding:10px 14px;border-radius:8px;background:var(--blue-light);border:1.5px solid var(--blue);margin-bottom:10px;"><strong style="color:var(--blue-dark);">RSI doses &amp; equipment — ' + wt + ' kg' + (age ? ', ~' + age + ' yr' : '') + '</strong></div>';
// Pre-medication
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:10px 0 4px;">Pre-medication (optional)</h5>';
html += S.drugTable(
S.drugRow('Atropine', S.dStr(wt, 0.02, 1, ' mg') + ' (min 0.1 mg)', 'IV', 'Consider &lt;1 yr or if using succinylcholine, or bradycardia risk') +
S.drugRow('Lidocaine 2%', S.dStr(wt, 1.5, 100, ' mg'), 'IV over 1 min', 'For ↑ICP, asthma; give 2-3 min pre-induction') +
S.drugRow('Fentanyl', S.dStr(wt, 2, 150, ' mcg') + ' (2-3 mcg/kg)', 'IV over 1 min', 'Blunts sympathetic response; avoid if hypotensive')
);
// Induction
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 4px;">Induction</h5>';
html += S.drugTable(
S.drugRow('Ketamine', S.dStr(wt, 1.5, 150, ' mg') + ' (1-2 mg/kg)', 'IV', 'Preserves BP. First-line for shock, asthma, sepsis') +
S.drugRow('Etomidate', S.dStr(wt, 0.3, 30, ' mg'), 'IV', 'Hemodynamically neutral. Avoid in septic shock (adrenal suppression)') +
S.drugRow('Propofol', S.dStr(wt, 2, 200, ' mg') + ' (1-2 mg/kg)', 'IV', 'Causes hypotension. Avoid in shock') +
S.drugRow('Midazolam', S.dStr(wt, 0.2, 10, ' mg'), 'IV', 'Less optimal induction — slower onset')
);
// Paralytics
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 4px;">Paralytics</h5>';
html += S.drugTable(
S.drugRow('Rocuronium', S.dStr(wt, 1.2, 100, ' mg') + ' (1-1.2 mg/kg)', 'IV', '30-60 sec onset; 30-45 min duration. First-line non-depolarizing.') +
S.drugRow('Succinylcholine', S.dStr(wt, (wt < 10 ? 2 : 1.5), 150, ' mg'), 'IV', '<10kg: 2 mg/kg; ≥10kg: 1.5 mg/kg. Avoid in burns/crush/hyperK/MH/NM dz. IM option: 4 mg/kg.') +
S.drugRow('Vecuronium', S.dStr(wt, 0.1, 10, ' mg'), 'IV', '2-3 min onset; longer duration than roc')
);
// Post-intubation sedation
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 4px;">Maintenance sedation / analgesia</h5>';
html += S.drugTable(
S.drugRow('Midazolam gtt', S.dStr(wt, 0.05, 2, ' mg') + '-' + S.dStr(wt, 0.2, 10, ' mg') + '/hr', 'IV infusion', '0.05-0.2 mg/kg/hr') +
S.drugRow('Fentanyl gtt', S.dStr(wt, 1, 100, ' mcg') + '-' + S.dStr(wt, 4, 200, ' mcg') + '/hr', 'IV infusion', '1-4 mcg/kg/hr') +
S.drugRow('Ketamine gtt', S.dStr(wt, 0.5, 30, ' mg') + '-' + S.dStr(wt, 2, 100, ' mg') + '/hr', 'IV infusion', '0.5-2 mg/kg/hr. Good for asthma') +
S.drugRow('Dexmedetomidine gtt', '0.2-1.4 mcg/kg/hr', 'IV infusion', 'No resp depression; causes bradycardia + hypotension') +
S.drugRow('Rocuronium gtt', '10-12 mcg/kg/min', 'IV infusion', 'If continued paralysis needed — only with adequate sedation')
);
// Equipment
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 4px;">Equipment sizing</h5>';
html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;font-size:12px;">';
if (age != null) {
html += '<div style="padding:10px;background:var(--g50);border-radius:6px;"><strong>ETT (uncuffed):</strong> ' + ettUncuff + ' mm<br><strong>ETT (cuffed):</strong> ' + ettCuff + ' mm<br><strong>Depth at lip:</strong> ~' + ettDepth + ' cm</div>';
html += '<div style="padding:10px;background:var(--g50);border-radius:6px;"><strong>Laryngoscope blade:</strong><br>' + bladeSize + '</div>';
} else {
html += '<div style="padding:10px;background:var(--g50);border-radius:6px;"><em>Enter age for ETT/blade sizing.</em><br>ETT (uncuffed) = (age/4) + 4<br>ETT (cuffed) = (age/4) + 3.5<br>Depth = ETT size &times; 3</div>';
}
html += '<div style="padding:10px;background:var(--g50);border-radius:6px;"><strong>LMA size:</strong> ' + lmaSize + '<br>OPA = corner mouth → angle mandible<br>NPA = tip nose → tragus</div>';
html += '<div style="padding:10px;background:var(--g50);border-radius:6px;"><strong>Suction catheter (Fr):</strong> ETT size &times; 2<br><strong>NG tube:</strong> ETT &times; 2<br><strong>Chest tube:</strong> 4 &times; ETT</div>';
html += '</div>';
// Ventilator starting settings
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 4px;">Ventilator — starting settings</h5>';
html += '<div style="padding:10px;background:var(--blue-light);border-radius:6px;font-size:12px;color:var(--g700);">';
html += '<strong>Mode:</strong> Volume or pressure control<br>';
html += '<strong>Tidal volume:</strong> 6-8 mL/kg (= ' + S.d(wt, 6) + '-' + S.d(wt, 8) + ' mL). Use lower (4-6 mL/kg) for ARDS.<br>';
html += '<strong>Rate:</strong> Infant 25-30, Child 15-25, Adolescent 12-16<br>';
html += '<strong>PEEP:</strong> 5 cmH2O (↑ for oxygenation problems)<br>';
html += '<strong>FiO2:</strong> Start 100%, wean to SpO2 92-97%<br>';
html += '<strong>I:E ratio:</strong> 1:2 (1:3-4 for obstructive disease)';
html += '</div>';
html += S.ref('Harriet Lane Handbook 23rd ed / PALS 2020. Always confirm doses against institutional protocols.');
el.innerHTML = html;
}
export function init() {
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-airway-calc' || e.target.closest('#btn-airway-calc')) calcAirway();
});
}

View file

@ -0,0 +1,82 @@
// ============================================================
// bedside/anaphylaxis.js
// ANAPHYLAXIS MANAGEMENT.
// ============================================================
import { S } from './shared.js';
function d(wt, mgPerKg, max) { var v = Math.round(wt * mgPerKg * 100) / 100; return max ? Math.min(v, max) : v; }
// Lookup a drug entry (by name) from window._DRUGS.sections.anaphylaxis,
// falling back to a hardcoded record so rendering stays identical if the
// JSON fetch hasn't resolved (or failed). Returns an object exposing the
// per-kg + max fields needed by S.dStr.
var ANAPH_FALLBACK = {
'Epinephrine IM': { dose_mg_per_kg: 0.01, max_mg: 0.5, unit: 'mg' },
'Normal saline bolus': { dose_mg_per_kg: 20, max_mg: null, unit: 'mL' },
'Diphenhydramine': { dose_mg_per_kg: 1.25, max_mg: 50, unit: 'mg' },
'Ranitidine': { dose_mg_per_kg: 1, max_mg: 50, unit: 'mg' },
'Dexamethasone': { dose_mg_per_kg: 0.6, max_mg: 16, unit: 'mg' },
'Methylprednisolone': { dose_mg_per_kg: 2, max_mg: 125, unit: 'mg' },
'Glucagon': { dose_mg_per_kg: 0.02, max_mg: 1, unit: 'mg' }
};
function anaphDrug(name) {
var s = window._DRUGS && window._DRUGS.sections && window._DRUGS.sections.anaphylaxis;
if (s && s.drugs) {
var hit = s.drugs.filter(function(x) { return x.name === name; })[0];
if (hit) return hit;
}
return ANAPH_FALLBACK[name] || {};
}
function calcAnaphylaxis() {
var wt = parseFloat(document.getElementById('anaph-weight').value);
var age = document.getElementById('anaph-age') ? document.getElementById('anaph-age').value : null; // kept for parity; not currently branched on
void age;
var resultDiv = document.getElementById('anaph-result');
if (!wt) { resultDiv.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter weight (kg).</p>'; return; }
var epi = anaphDrug('Epinephrine IM');
var fluids = anaphDrug('Normal saline bolus');
var dph = anaphDrug('Diphenhydramine');
var rnt = anaphDrug('Ranitidine');
var dex = anaphDrug('Dexamethasone');
var mpn = anaphDrug('Methylprednisolone');
var glc = anaphDrug('Glucagon');
var epiDose = d(wt, epi.dose_mg_per_kg, epi.max_mg);
var epiVol = Math.round(epiDose * 10) / 10; // 1:1000 = 1 mg/mL
var autoInjector = wt < 10 ? 'Draw up manually' : wt <= 25 ? 'EpiPen Jr / Auvi-Q 0.15 mg' : 'EpiPen / Auvi-Q 0.3 mg';
var html = '<div style="padding:14px;border-radius:10px;background:#fee2e2;border:2px solid #ef4444;margin-bottom:16px;">';
html += '<div style="font-size:18px;font-weight:700;color:#dc2626;margin-bottom:6px;"><i class="fas fa-triangle-exclamation"></i> STEP 1: Epinephrine IM — GIVE IMMEDIATELY</div>';
html += '<div style="font-size:15px;font-weight:600;color:var(--g800);margin-bottom:4px;">Epinephrine 1:1000 (1 mg/mL): <span style="color:#dc2626;">' + epiDose + ' mg (' + epiVol + ' mL)</span> IM to lateral thigh</div>';
html += '<div style="font-size:13px;color:var(--g600);">' + epi.dose_mg_per_kg + ' mg/kg (max ' + epi.max_mg + ' mg) | Auto-injector: ' + autoInjector + '</div>';
html += '<div style="font-size:12px;color:#991b1b;margin-top:6px;">May repeat every 5-15 minutes if symptoms persist. No contraindications in anaphylaxis.</div>';
html += '</div>';
html += '<div style="overflow-x:auto;-webkit-overflow-scrolling:touch;margin-bottom:12px;"><table style="width:100%;min-width:560px;border-collapse:collapse;font-size:13px;"><thead><tr style="background:var(--g100);"><th style="text-align:left;padding:6px 8px;">Step</th><th style="padding:6px 8px;">Drug</th><th style="padding:6px 8px;">Dose</th><th style="padding:6px 8px;">Route</th><th style="padding:6px 8px;">Notes</th></tr></thead><tbody>';
html += '<tr><td style="font-weight:600;color:var(--blue);">STEP 2</td><td style="font-weight:600;">Position</td><td>Supine + legs elevated</td><td>—</td><td style="font-size:12px;color:var(--g500);">If dyspneic: sitting position. If vomiting: recovery position</td></tr>';
html += '<tr><td style="font-weight:600;color:var(--blue);">STEP 3</td><td style="font-weight:600;">O2</td><td>High flow 10-15 L/min</td><td>Face mask</td><td style="font-size:12px;color:var(--g500);">100% O2. Prepare for airway management</td></tr>';
html += '<tr><td style="font-weight:600;color:var(--blue);">STEP 4</td><td style="font-weight:600;">IV fluids</td><td>NS ' + S.dStr(wt, fluids.dose_mg_per_kg, fluids.max_mg, ' mL') + ' bolus</td><td>IV/IO</td><td style="font-size:12px;color:var(--g500);">Repeat up to 60 mL/kg for hypotension</td></tr>';
html += '<tr><td style="font-weight:600;color:var(--blue);">STEP 5</td><td style="font-weight:600;">Diphenhydramine</td><td>' + S.dStr(wt, dph.dose_mg_per_kg, dph.max_mg) + '</td><td>IV/IM/PO</td><td style="font-size:12px;color:var(--g500);">H1 blocker. NOT first-line — adjunct only</td></tr>';
html += '<tr><td></td><td style="font-weight:600;">Ranitidine</td><td>' + S.dStr(wt, rnt.dose_mg_per_kg, rnt.max_mg) + '</td><td>IV over 5 min</td><td style="font-size:12px;color:var(--g500);">H2 blocker. Optional adjunct</td></tr>';
html += '<tr><td style="font-weight:600;color:var(--blue);">STEP 6</td><td style="font-weight:600;">Dexamethasone</td><td>' + S.dStr(wt, dex.dose_mg_per_kg, dex.max_mg) + '</td><td>IV/IM/PO</td><td style="font-size:12px;color:var(--g500);">Prevents biphasic reaction (4-6 hrs later)</td></tr>';
html += '<tr><td></td><td style="font-weight:600;">Methylprednisolone</td><td>' + S.dStr(wt, mpn.dose_mg_per_kg, mpn.max_mg) + '</td><td>IV</td><td style="font-size:12px;color:var(--g500);">Alternative steroid</td></tr>';
html += '<tr style="background:#fef3c7;"><td style="font-weight:600;color:#dc2626;">IF REFRACTORY</td><td style="font-weight:600;">Epinephrine gtt</td><td>0.1-1 mcg/kg/min</td><td>IV infusion</td><td style="font-size:12px;color:var(--g500);">For persistent hypotension despite fluids + IM epi</td></tr>';
html += '<tr style="background:#fef3c7;"><td></td><td style="font-weight:600;">Glucagon</td><td>' + S.dStr(wt, glc.dose_mg_per_kg, glc.max_mg) + '</td><td>IV/IM</td><td style="font-size:12px;color:var(--g500);">For patients on beta-blockers not responding to epi</td></tr>';
html += '</tbody></table></div>';
html += '<div style="padding:10px 12px;background:var(--amber-light);border-radius:6px;font-size:12px;color:#92400e;"><strong>Observe minimum 4-6 hours</strong> after last dose of epinephrine (biphasic reactions occur in 5-20% of cases). Discharge with EpiPen prescription and anaphylaxis action plan. Refer to allergist.</div>';
html += '<p style="font-size:11px;color:var(--g400);margin:8px 0 0;">ASCIA Anaphylaxis Guidelines 2021. WAO Anaphylaxis Guidance 2020. AAP/ACAAI Practice Parameters.</p>';
resultDiv.innerHTML = html;
}
export function init() {
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-anaph-calc' || e.target.closest('#btn-anaph-calc')) calcAnaphylaxis();
});
}

View file

@ -0,0 +1,59 @@
// ============================================================
// bedside/antiemetics.js
// ANTIEMETICS — drugs.json-backed.
// ============================================================
import { S } from './shared.js';
// Fallback inline data — used if /data/drugs.json hasn't loaded yet.
// Must match drugs.json shape for the antiemetics section.
var EMET_FALLBACK = [
{ name: 'Ondansetron', dose_mg_per_kg: 0.15, max_mg: 8, unit: 'mg', route: 'PO / ODT / IV', notes: 'First-line. Max single 8 mg. Repeat q8h. QT prolongation — avoid with other QT drugs. &lt;6 mo: limited data.', weight_band_dose: { under_15kg: '2 mg', '15_to_30kg': '4 mg', '30kg_or_more': '8 mg' } },
{ name: 'Metoclopramide', dose_mg_per_kg: 0.15, max_mg: 10, unit: 'mg', route: 'IV / IM / PO', notes: '0.1-0.15 mg/kg (max 10 mg). Give with diphenhydramine to prevent EPS / dystonia.' },
{ name: 'Dimenhydrinate', dose_mg_per_kg: 1.25, max_mg: 50, unit: 'mg', route: 'PO / IV / IM / PR', notes: '1.25 mg/kg (max 50 mg) q6h. ≥2 yr.' },
{ name: 'Diphenhydramine', dose_mg_per_kg: 1, max_mg: 50, unit: 'mg', route: 'PO / IV / IM', notes: '1 mg/kg (max 50 mg) q6h. Adjunct, sedating.' },
{ name: 'Promethazine', dose_mg_per_kg: 0.25, max_mg: 25, unit: 'mg', route: 'PO / IV / IM', notes: '0.25-1 mg/kg. <strong>CONTRAINDICATED &lt;2 yr</strong> (resp depression). Tissue injury if IV extrav.' },
{ name: 'Dexamethasone', dose_mg_per_kg: 0.15, max_mg: 10, unit: 'mg', route: 'IV / PO', notes: 'Adjunct, esp. chemo-induced. 0.15 mg/kg (max 10 mg).' },
{ name: 'Scopolamine patch', dose_mg_per_kg: null, max_mg: null, unit: 'mg', dose_display: '1.5 mg patch', route: 'Transdermal', notes: '≥12 yr. Motion sickness. Apply 4h before exposure.' }
];
// Prefer JSON-backed data if loaded; fall back to the inline list above.
function emetDrugs() {
var s = window._DRUGS && window._DRUGS.sections && window._DRUGS.sections.antiemetics;
return (s && s.drugs && s.drugs.length) ? s.drugs : EMET_FALLBACK;
}
// Iterate the drug list to produce S.drugRow(...) output, handling the
// special cases (Ondansetron weight-band prefix, Scopolamine fixed dose).
function emetRows(wt, drugs) {
return drugs.map(function(dg) {
var dose;
if (dg.name === 'Ondansetron' && dg.weight_band_dose) {
var bd = dg.weight_band_dose;
var band = wt < 15 ? bd.under_15kg : wt < 30 ? bd['15_to_30kg'] : bd['30kg_or_more'];
dose = band + ' (weight band) OR ' + S.dStr(wt, dg.dose_mg_per_kg, dg.max_mg, ' ' + dg.unit) + ' (' + dg.dose_mg_per_kg + ' ' + dg.unit + '/kg)';
} else if (dg.dose_display) {
dose = dg.dose_display;
} else {
dose = S.dStr(wt, dg.dose_mg_per_kg, dg.max_mg, ' ' + dg.unit);
}
return S.drugRow(dg.name, dose, dg.route, dg.notes);
}).join('');
}
function calcEmet() {
var wt = parseFloat(document.getElementById('emet-weight').value);
var el = document.getElementById('emet-result');
if (!wt) { el.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter weight (kg).</p>'; return; }
var html = '<div style="padding:10px 14px;border-radius:8px;background:var(--blue-light);border:1.5px solid var(--blue);margin-bottom:10px;"><strong style="color:var(--blue-dark);">Antiemetic doses — ' + wt + ' kg</strong></div>';
html += S.drugTable(emetRows(wt, emetDrugs()));
html += '<div style="padding:10px;background:#fef3c7;border-radius:6px;font-size:12px;color:#92400e;margin-top:10px;"><strong>Pearls:</strong> Ondansetron is first-line in ED for acute gastroenteritis vomiting — single PO/ODT dose increases oral rehydration success. Avoid anticholinergics + opioid combinations. Check QTc if stacking ondansetron + other QT drugs.</div>';
html += S.ref('AAP gastroenteritis guidance; WHO essential medicines; Lexicomp.');
el.innerHTML = html;
}
export function init() {
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-emet-calc' || e.target.closest('#btn-emet-calc')) calcEmet();
});
}

View file

@ -0,0 +1,65 @@
// ============================================================
// bedside/antimicrobials.js
// ANTIMICROBIALS (empiric, by age + infection type).
// ============================================================
import { S } from './shared.js';
var REG = {
neonate: {
sepsis: { first: 'Ampicillin + gentamicin', alt: 'Add cefotaxime if meningitis suspected or gram-neg concern', duration: '7-10 days; 21 days if confirmed meningitis', notes: 'Cover GBS, E. coli, Listeria. Add acyclovir if HSV risk (maternal lesions, vesicles, seizures).' },
meningitis: { first: 'Ampicillin + cefotaxime (or gentamicin) + acyclovir', alt: 'Add vancomycin if gram-positive cocci', duration: '14-21 days depending on organism', notes: 'HSV coverage essential. LP + HSV PCR.' },
pna: { first: 'Ampicillin + gentamicin', alt: 'Add cefotaxime for gram-neg', duration: '7-10 days', notes: 'Same as sepsis coverage.' },
uti: { first: 'Ampicillin + gentamicin', alt: 'Cefotaxime if sensitivities known', duration: '10-14 days', notes: 'Evaluate for sepsis. Renal US and VCUG workup.' },
skin: { first: 'Ampicillin + gentamicin', alt: 'Add vancomycin if MRSA risk', duration: '7-10 days', notes: 'Omphalitis, mastitis — broad coverage.' },
ent: { first: 'Discuss with infectious diseases', alt: '—', duration: '—', notes: 'Rare in neonates. Any ear/throat infection warrants sepsis eval.' },
neutropenic: { first: 'Cefepime or piperacillin-tazobactam', alt: 'Add vancomycin if indwelling line, severe mucositis, or MRSA', duration: 'Until ANC recovery + afebrile ≥48h', notes: 'Oncology/ID consult.' },
ic: { first: 'Ampicillin + gentamicin + metronidazole', alt: 'Or piperacillin-tazobactam', duration: '7-14 days', notes: 'NEC: add metronidazole/clindamycin for anaerobes.' },
bone: { first: 'Ampicillin + gentamicin', alt: 'Vancomycin if MRSA risk', duration: '3-6 weeks (IV then PO)', notes: 'Usually hematogenous. S. aureus, GBS, E. coli.' }
},
infant: {
sepsis: { first: 'Ampicillin + ceftriaxone (or gentamicin)', alt: '+ vancomycin if MRSA / severe', duration: '7-10 days', notes: 'Add acyclovir if HSV suspected (<6 wks). Fever in <90 days requires workup.' },
meningitis: { first: 'Ceftriaxone + vancomycin (± ampicillin if &lt;6 wks for Listeria)', alt: '+ acyclovir if HSV', duration: '10-14 days (longer for Listeria/gram-neg)', notes: 'Dexamethasone if H. influenzae suspected.' },
pna: { first: 'Ampicillin (or ceftriaxone)', alt: 'Add azithromycin if atypical', duration: '7-10 days', notes: 'S. pneumoniae most common. RSV/viral often primary.' },
uti: { first: 'Ceftriaxone or cefotaxime', alt: 'Ampicillin + gentamicin', duration: '10-14 days', notes: 'US kidney/bladder if first febrile UTI.' },
skin: { first: 'Cefazolin or clindamycin', alt: 'Vancomycin if MRSA risk', duration: '7-10 days', notes: 'Consider MRSA if purulent or local resistance >10%.' },
ent: { first: 'Amoxicillin (90 mg/kg/day) for AOM', alt: 'Amox-clav if recent abx/treatment failure', duration: 'AOM 10 days &lt;2 yr; 7 days ≥2 yr', notes: 'Observe mild AOM if &gt;6 mo and no severe features.' },
neutropenic: { first: 'Cefepime or piperacillin-tazobactam', alt: '+ vancomycin for severe/mucositis/line', duration: 'Until ANC recovery + afebrile', notes: 'Oncology/ID consult.' },
ic: { first: 'Ceftriaxone + metronidazole', alt: 'Piperacillin-tazobactam', duration: '5-7 days (uncomplicated), longer for complicated', notes: 'Appendicitis — surgical consult.' },
bone: { first: 'Cefazolin ± vancomycin', alt: 'Clindamycin', duration: '3-4 weeks (IV then PO transition)', notes: 'S. aureus most common. Kingella in &lt;4 yr.' }
},
child: {
sepsis: { first: 'Ceftriaxone + vancomycin', alt: 'Piperacillin-tazobactam if intra-abdominal', duration: '7-14 days', notes: 'Expand if specific source. Add antifungal if prolonged neutropenia.' },
meningitis: { first: 'Ceftriaxone + vancomycin', alt: '+ dexamethasone (Hib coverage)', duration: '10-14 days', notes: 'Lumbar puncture. Dexamethasone 0.15 mg/kg q6h x 4 days.' },
pna: { first: 'Ampicillin OR amoxicillin (high dose)', alt: 'Ceftriaxone if hospitalized; add azithromycin for atypical', duration: '5-7 days (uncomplicated)', notes: 'Mycoplasma if &gt;5 yr. Consider viral causes.' },
uti: { first: 'Cephalexin or TMP-SMX (PO)', alt: 'Ceftriaxone if ill', duration: '7-10 days (10-14 if pyelo)', notes: 'Culture-guided de-escalation.' },
skin: { first: 'Cephalexin or clindamycin', alt: 'TMP-SMX or doxycycline if MRSA', duration: '5-10 days', notes: 'Abscess — incision &amp; drainage primary tx.' },
ent: { first: 'Amoxicillin 90 mg/kg/day (AOM); Penicillin V for strep pharyngitis', alt: 'Amox-clav; cephalexin if non-anaphylactic PCN allergy', duration: 'AOM 5-10 days; strep 10 days', notes: 'Strep pharyngitis dose: PCN V 250 mg BID-TID (&lt;27 kg) or 500 mg BID (≥27 kg).' },
neutropenic: { first: 'Cefepime OR piperacillin-tazobactam', alt: '+ vancomycin', duration: 'Until ANC recovery + afebrile', notes: 'Add empiric antifungal if fever &gt;4-7 days.' },
ic: { first: 'Ceftriaxone + metronidazole', alt: 'Piperacillin-tazobactam', duration: '5-7 days (uncomplicated appendicitis)', notes: 'Surgical consult.' },
bone: { first: 'Cefazolin ± vancomycin', alt: 'Clindamycin', duration: '3-4 weeks total', notes: 'S. aureus, group A strep. Consider MRSA if severe.' }
}
};
function lookupAbx() {
var age = document.getElementById('abx-age').value;
var inf = document.getElementById('abx-infection').value;
var el = document.getElementById('abx-result');
var r = REG[age] && REG[age][inf];
if (!r) { el.innerHTML = '<p style="color:var(--red);font-size:13px;">No regimen found.</p>'; return; }
var ageLbl = { neonate: 'Neonate (0-28 d)', infant: 'Infant (1-3 mo)', child: 'Child (>3 mo)' }[age];
var infLbl = document.querySelector('#abx-infection option[value="' + inf + '"]').textContent;
var html = '<div style="padding:10px 14px;border-radius:8px;background:var(--blue-light);border:1.5px solid var(--blue);margin-bottom:10px;"><strong style="color:var(--blue-dark);">' + ageLbl + ' — ' + infLbl + '</strong></div>';
html += '<div style="padding:12px;background:#d1fae5;border-radius:6px;margin-bottom:10px;"><strong style="color:#065f46;">First-line:</strong> ' + r.first + '</div>';
html += '<div style="padding:12px;background:var(--g50);border-radius:6px;margin-bottom:10px;"><strong>Alternative / add:</strong> ' + r.alt + '</div>';
html += '<div style="padding:12px;background:var(--g50);border-radius:6px;margin-bottom:10px;"><strong>Duration:</strong> ' + r.duration + '</div>';
html += '<div style="padding:10px;background:#fef3c7;border-radius:6px;font-size:12px;color:#92400e;"><strong>Notes:</strong> ' + r.notes + '</div>';
html += S.ref('AAP Red Book, IDSA guidelines, Harriet Lane. Always tailor to local resistance patterns and culture results.');
el.innerHTML = html;
}
export function init() {
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-abx-lookup' || e.target.closest('#btn-abx-lookup')) lookupAbx();
});
}

155
public/js/bedside/burns.js Normal file
View file

@ -0,0 +1,155 @@
// ============================================================
// bedside/burns.js
// BURNS — Lund-Browder body parts + Parkland formula.
// ============================================================
import { S } from './shared.js';
// Lund-Browder age-adjusted region percentages. Regions marked "*" change with age.
// Column order: infant, young (1-5), child (5-10), adol (10-15), adult (>15).
var LB = {
head: { label: 'Head', vals: [18, 13, 11, 9, 7], ageSensitive: true },
neck: { label: 'Neck', vals: [2, 2, 2, 2, 2] },
ant_trunk: { label: 'Anterior trunk', vals: [13, 13, 13, 13, 13] },
post_trunk:{ label: 'Posterior trunk', vals: [13, 13, 13, 13, 13] },
r_buttock: { label: 'Right buttock', vals: [2.5, 2.5, 2.5, 2.5, 2.5] },
l_buttock: { label: 'Left buttock', vals: [2.5, 2.5, 2.5, 2.5, 2.5] },
genital: { label: 'Genitalia', vals: [1, 1, 1, 1, 1] },
r_uparm: { label: 'R upper arm', vals: [4, 4, 4, 4, 4] },
l_uparm: { label: 'L upper arm', vals: [4, 4, 4, 4, 4] },
r_forearm: { label: 'R forearm', vals: [3, 3, 3, 3, 3] },
l_forearm: { label: 'L forearm', vals: [3, 3, 3, 3, 3] },
r_hand: { label: 'R hand', vals: [2.5, 2.5, 2.5, 2.5, 2.5] },
l_hand: { label: 'L hand', vals: [2.5, 2.5, 2.5, 2.5, 2.5] },
r_thigh: { label: 'R thigh', vals: [5.5, 8, 8.5, 9, 9.5], ageSensitive: true },
l_thigh: { label: 'L thigh', vals: [5.5, 8, 8.5, 9, 9.5], ageSensitive: true },
r_leg: { label: 'R lower leg', vals: [5, 5.5, 6, 6.5, 7], ageSensitive: true },
l_leg: { label: 'L lower leg', vals: [5, 5.5, 6, 6.5, 7], ageSensitive: true },
r_foot: { label: 'R foot', vals: [3.5, 3.5, 3.5, 3.5, 3.5] },
l_foot: { label: 'L foot', vals: [3.5, 3.5, 3.5, 3.5, 3.5] }
};
var AGE_KEYS = ['infant','young','child','adol','adult'];
function ageIdx() { return Math.max(0, AGE_KEYS.indexOf(document.getElementById('burn-age').value)); }
function renderBodyParts() {
var wrap = document.getElementById('burn-bodyparts-wrapper');
if (!wrap) return;
var idx = ageIdx();
var html = '<div style="background:var(--g50);border-radius:8px;padding:10px 12px;">';
html += '<div style="font-size:12px;color:var(--g600);margin-bottom:8px;line-height:1.4;">For each involved body region, enter <strong>% of that region burned (2° or deeper)</strong>. Leave 0 if uninvolved. TBSA = sum of (region size × % involvement).</div>';
html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:6px;">';
Object.keys(LB).forEach(function(key) {
var r = LB[key];
var max = r.vals[idx];
var isAgeDep = r.ageSensitive;
html += '<div style="display:flex;align-items:center;gap:8px;padding:4px 8px;background:white;border-radius:6px;border:1px solid var(--g200);">' +
'<label style="flex:1;font-size:12px;color:var(--g700);">' + r.label + ' <span style="color:var(--g400);">(' + max + '%' + (isAgeDep ? '<span title="age-adjusted">*</span>' : '') + ')</span></label>' +
'<input type="number" min="0" max="100" step="5" value="0" data-burn-region="' + key + '" data-burn-max="' + max + '" style="width:68px;padding:3px 6px;border:1px solid var(--g300);border-radius:4px;font-size:12px;text-align:right;"> %' +
'</div>';
});
html += '</div></div>';
wrap.innerHTML = html;
updateLive();
}
function resetParts() {
var inputs = document.querySelectorAll('[data-burn-region]');
inputs.forEach(function(i) { i.value = 0; });
document.getElementById('burn-tbsa').value = '';
updateLive();
}
function computeTbsa() {
var idx = ageIdx();
var total = 0;
Object.keys(LB).forEach(function(key) {
var el = document.querySelector('[data-burn-region="' + key + '"]');
if (!el) return;
var pct = parseFloat(el.value) || 0;
if (pct < 0) pct = 0; if (pct > 100) pct = 100;
total += LB[key].vals[idx] * (pct / 100);
});
return Math.round(total * 10) / 10;
}
function updateLive() {
var t = computeTbsa();
var live = document.getElementById('burn-tbsa-live');
if (live) live.textContent = t > 0 ? 'Computed TBSA: ' + t + '%' : '';
}
function calcBurn() {
var wt = parseFloat(document.getElementById('burn-weight').value);
var override = document.getElementById('burn-tbsa').value;
var tbsa = override !== '' ? parseFloat(override) : computeTbsa();
var el = document.getElementById('burn-result');
if (!wt) { el.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter weight (kg).</p>'; return; }
if (!tbsa || tbsa <= 0) { el.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter % involvement per region, or override TBSA.</p>'; return; }
var total = Math.round(4 * wt * tbsa);
var first8 = Math.round(total / 2);
var rateFirst = Math.round(first8 / 8);
var next16 = total - first8;
var rateNext = Math.round(next16 / 16);
var maint = wt <= 10 ? wt * 4 : wt <= 20 ? 40 + (wt - 10) * 2 : 60 + (wt - 20);
maint = Math.round(maint);
var html = '<div style="padding:10px 14px;border-radius:8px;background:#fee2e2;border:1.5px solid #ef4444;margin-bottom:10px;"><strong style="color:#dc2626;">Burn fluid resuscitation — ' + wt + ' kg, ' + tbsa + '% TBSA (2° or deeper)</strong></div>';
html += '<div style="padding:12px;background:#fef2f2;border-radius:6px;margin-bottom:10px;"><strong>Parkland formula:</strong> 4 mL × kg × %TBSA = <strong>' + total + ' mL LR over 24 hours</strong>' +
'<br>&nbsp;&nbsp;<strong>First 8 h</strong> (from time of burn): ' + first8 + ' mL (~<strong>' + rateFirst + ' mL/hr</strong>)' +
'<br>&nbsp;&nbsp;<strong>Next 16 h</strong>: ' + next16 + ' mL (~<strong>' + rateNext + ' mL/hr</strong>)</div>';
html += '<div style="padding:12px;background:var(--g50);border-radius:6px;margin-bottom:10px;"><strong>Plus maintenance fluids (4-2-1):</strong> ' + maint + ' mL/hr (D5 1/2NS ± 20 mEq KCl/L once UOP established). Consider dextrose in children &lt;30 kg.</div>';
html += '<div style="padding:12px;background:var(--g50);border-radius:6px;margin-bottom:10px;"><strong>Titrate to UOP:</strong> target 1-2 mL/kg/hr (infants / children), 0.5-1 mL/kg/hr (adolescents). <strong>Clinical response trumps formula.</strong></div>';
// Breakdown of what regions contributed
var idx = ageIdx();
var breakdown = '';
Object.keys(LB).forEach(function(key) {
var el2 = document.querySelector('[data-burn-region="' + key + '"]');
if (!el2) return;
var pct = parseFloat(el2.value) || 0;
if (pct > 0) {
var contrib = Math.round(LB[key].vals[idx] * pct / 100 * 10) / 10;
breakdown += '<div>' + LB[key].label + ': ' + pct + '% of ' + LB[key].vals[idx] + '% = ' + contrib + '%</div>';
}
});
if (breakdown) {
html += '<details style="margin-bottom:10px;"><summary style="cursor:pointer;font-size:13px;font-weight:600;color:var(--g700);">Region breakdown</summary><div style="padding:10px;background:var(--g50);border-radius:6px;font-size:12px;margin-top:6px;">' + breakdown + '</div></details>';
}
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 6px;">Other pearls</h5>';
html += '<div style="padding:10px;background:var(--g50);border-radius:6px;font-size:12px;">';
html += '<strong>Rule of palm:</strong> Patient\'s palm + fingers ≈ 1% TBSA — good for scattered burns.<br>';
html += '<strong>First-degree burns DO NOT count</strong> toward TBSA or Parkland.<br>';
html += '<strong>Analgesia:</strong> Morphine 0.05-0.1 mg/kg IV q2h, or fentanyl 1-2 mcg/kg IV q30-60 min.<br>';
html += '<strong>Tetanus</strong> prophylaxis if indicated. Tdap/Td ± tetanus immunoglobulin.';
html += '</div>';
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 6px;">Burn center referral (ABA)</h5>';
html += '<div style="padding:10px;background:#fef3c7;border-radius:6px;font-size:12px;color:#92400e;">Partial-thickness &gt;10% TBSA; any full-thickness; face/hands/feet/genital/perineum/major joints; electrical/chemical/inhalation; associated trauma; significant comorbidities; pediatric burns in non-pediatric center.</div>';
html += S.ref('ABA Advanced Burn Life Support (ABLS) 2018. Parkland formula: Baxter 1968. Lund-Browder 1944 chart (age-adjusted regions).');
el.innerHTML = html;
}
export function init() {
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-burn-calc' || e.target.closest('#btn-burn-calc')) calcBurn();
if (e.target.id === 'btn-burn-reset' || e.target.closest('#btn-burn-reset')) resetParts();
// Render body-parts table the first (or any) time the Burns sub-pill is activated
var bp = e.target.closest && e.target.closest('[data-em="burns"]');
if (bp) setTimeout(renderBodyParts, 0);
// Also render if the top-level Bedside tab is clicked (ensures table ready before user navigates)
var nav = e.target.closest && e.target.closest('[data-calc="bedside"]');
if (nav) setTimeout(renderBodyParts, 50);
});
document.addEventListener('change', function(e) {
if (e.target && e.target.id === 'burn-age') renderBodyParts();
});
document.addEventListener('input', function(e) {
if (e.target && e.target.dataset && e.target.dataset.burnRegion) updateLive();
});
// Also attempt render on these events in case component mounts later
document.addEventListener('DOMContentLoaded', renderBodyParts);
document.addEventListener('tabChanged', function() { setTimeout(renderBodyParts, 100); });
if (document.readyState !== 'loading') setTimeout(renderBodyParts, 0);
}

View file

@ -0,0 +1,103 @@
// ============================================================
// bedside/cardiac.js
// CARDIAC ARREST / PALS.
// ============================================================
import { S } from './shared.js';
function getWt() {
return parseFloat(document.getElementById('cardiac-weight').value) || 0;
}
function title(k) {
return { general: 'PALS General Doses', asystole: 'Asystole / PEA', brady: 'Bradycardia', svt: 'SVT', vfib: 'VF / Pulseless VT', vt: 'Stable VT' }[k] || k;
}
function cardiacShow(kind) {
var wt = getWt();
var el = document.getElementById('cardiac-result');
if (!wt) { el.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter weight (kg).</p>'; return; }
var html = '<div style="padding:10px 14px;border-radius:8px;background:var(--red)10;border:1.5px solid var(--red);margin-bottom:10px;"><strong style="color:var(--red);">' + title(kind) + ' — ' + wt + ' kg</strong></div>';
if (kind === 'general') {
html += S.drugTable(
S.drugRow('Epinephrine 1:10,000', S.dStr(wt, 0.01, 1, ' mg') + ' (' + S.dStr(wt, 0.1, 10, ' mL') + ')', 'IV / IO', '0.01 mg/kg = 0.1 mL/kg of 1:10,000. Max single dose 1 mg. Repeat q3-5 min.') +
S.drugRow('Epinephrine ET', S.dStr(wt, 0.1, 2.5, ' mg'), 'ETT', '0.1 mg/kg (1 mL/kg of 1:10,000) if no IV/IO') +
S.drugRow('Amiodarone', S.dStr(wt, 5, 300, ' mg'), 'IV / IO bolus', '5 mg/kg for arrest. Max 300 mg. Repeat up to 2 times (max 15 mg/kg/day).') +
S.drugRow('Lidocaine', S.dStr(wt, 1, 100, ' mg'), 'IV / IO bolus', '1 mg/kg. Alternative to amiodarone for VF/VT.') +
S.drugRow('Atropine', S.dStr(wt, 0.02, 0.5, ' mg') + ' (min 0.1 mg)', 'IV / IO', '0.02 mg/kg. Bradycardia from ↑vagal tone or AV block.') +
S.drugRow('Adenosine (1st)', S.dStr(wt, 0.1, 6, ' mg'), 'IV push + flush', '0.1 mg/kg (max 6 mg). For SVT. Rapid push, double flush.') +
S.drugRow('Adenosine (2nd)', S.dStr(wt, 0.2, 12, ' mg'), 'IV push + flush', '0.2 mg/kg (max 12 mg). Second dose if first unsuccessful.') +
S.drugRow('Sodium bicarb 8.4%', S.dStr(wt, 1, 50, ' mEq'), 'IV / IO', '1 mEq/kg. ONLY if severe metabolic acidosis, hyperK, or TCA OD. Not routine.') +
S.drugRow('Calcium chloride 10%', S.dStr(wt, 20, 1000, ' mg') + ' (' + S.dStr(wt, 0.2, 10, ' mL') + ')', 'IV / IO (central preferred)', 'For hyperK, hypoCa, Mg OD, CCB OD. 20 mg/kg = 0.2 mL/kg.') +
S.drugRow('Calcium gluconate 10%', S.dStr(wt, 60, 3000, ' mg') + ' (' + S.dStr(wt, 0.6, 30, ' mL') + ')', 'IV / IO (peripheral OK)', '60 mg/kg = 0.6 mL/kg. Preferred peripherally.') +
S.drugRow('Magnesium sulfate', S.dStr(wt, 50, 2000, ' mg'), 'IV over 10-20 min', '25-50 mg/kg. Torsades, severe asthma. Max 2 g.') +
S.drugRow('Dextrose 10%', S.dStr(wt, 2, null, ' mL') + ' (0.2 g/kg)', 'IV push', 'For documented hypoglycemia') +
S.drugRow('Defibrillation', '2 J/kg → 4 J/kg → 10 J/kg (max 10 J/kg or adult dose)', 'Pad', 'For VF/pulseless VT. Resume CPR immediately after shock.') +
S.drugRow('Cardioversion (sync)', '0.5-1 J/kg → 2 J/kg', 'Pad', 'For unstable SVT/VT. Sedate if possible.')
);
html += '<div style="padding:10px;background:#fef3c7;border-radius:6px;font-size:12px;color:#92400e;margin-top:10px;"><strong>Concentrations:</strong> Epi 1:10,000 (0.1 mg/mL) for IV arrest. 1:1000 (1 mg/mL) for IM anaphylaxis. Atropine &lt;0.1 mg can cause paradoxical bradycardia.</div>';
} else if (kind === 'asystole') {
html += S.drugTable(
S.drugRow('CPR', '100-120/min, depth 1/3 AP', '—', '15:2 (2 rescuer) or 30:2 (single). Rotate every 2 min.') +
S.drugRow('Epinephrine', S.dStr(wt, 0.01, 1, ' mg') + ' IV/IO', 'IV / IO', '0.01 mg/kg 1:10,000 = 0.1 mL/kg. Q3-5 min. Start early.') +
S.drugRow('Epinephrine ETT', S.dStr(wt, 0.1, 2.5, ' mg'), 'ETT', '0.1 mg/kg if no IV access')
);
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 6px;">Reversible causes (H\'s &amp; T\'s)</h5>';
html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:6px;font-size:12px;">';
html += '<div style="padding:8px;background:var(--g50);border-radius:6px;"><strong>H\'s</strong><br>Hypoxia, Hypovolemia, H+ (acidosis), Hypo/hyperK, Hypoglycemia, Hypothermia</div>';
html += '<div style="padding:8px;background:var(--g50);border-radius:6px;"><strong>T\'s</strong><br>Tension PTX, Tamponade, Toxins, Thrombosis (MI/PE), Trauma</div>';
html += '</div>';
} else if (kind === 'brady') {
html += S.stepBox('#f59e0b', 'Bradycardia with poor perfusion (HR &lt; 60)', 'Start CPR if HR &lt; 60 with poor perfusion despite oxygenation and ventilation.');
html += S.arrow;
html += S.drugTable(
S.drugRow('Epinephrine', S.dStr(wt, 0.01, 1, ' mg'), 'IV / IO', 'First-line. Q3-5 min.') +
S.drugRow('Atropine', S.dStr(wt, 0.02, 0.5, ' mg') + ' (min 0.1 mg)', 'IV / IO', 'If ↑ vagal tone or AV block. Max 1 mg child / 0.5 mg infant.') +
S.drugRow('Transcutaneous pacing', '—', 'Pad', 'For refractory bradycardia not responsive to drugs.') +
S.drugRow('Consider', '—', '—', 'Hypoxia, tension pneumothorax, ↑ICP, toxic ingestion (organo, CCB, BB), heart block')
);
} else if (kind === 'svt') {
html += S.stepBox('#3b82f6', 'SVT — narrow complex, rate usually &gt;220 infant / &gt;180 child', 'Differentiate from sinus tach: abrupt onset/offset, no P waves or abnormal P axis, HR minimally variable.');
html += S.arrow;
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:6px 0;">If STABLE</h5>';
html += S.drugTable(
S.drugRow('Vagal maneuvers', 'Ice to face, blow into syringe, Valsalva', '—', 'Try first in stable patient') +
S.drugRow('Adenosine (1st)', S.dStr(wt, 0.1, 6, ' mg'), 'IV push (proximal) + flush', 'Rapid push, 3-way stopcock with NS flush') +
S.drugRow('Adenosine (2nd)', S.dStr(wt, 0.2, 12, ' mg'), 'IV push + flush', 'If first dose unsuccessful') +
S.drugRow('Procainamide', '15 mg/kg over 30-60 min', 'IV', 'Expert consult. Avoid with amiodarone.') +
S.drugRow('Amiodarone', '5 mg/kg over 20-60 min', 'IV', 'Expert consult.')
);
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:10px 0 4px;">If UNSTABLE</h5>';
html += S.drugTable(S.drugRow('Synchronized cardioversion', '0.5-1 J/kg → 2 J/kg', 'Pad', 'Sedate if possible. Pre-treat with adenosine if IV available.'));
} else if (kind === 'vfib') {
html += S.stepBox('#dc2626', 'VF / Pulseless VT', 'High-quality CPR + defibrillation are key. Minimize interruptions.');
html += S.arrow;
html += S.drugTable(
S.drugRow('Defibrillation (1st)', '2 J/kg', 'Pad', 'Resume CPR immediately after shock.') +
S.drugRow('Defibrillation (2nd)', '4 J/kg', 'Pad', 'After 2 min CPR.') +
S.drugRow('Defibrillation (3rd+)', '≥4 J/kg (max 10 J/kg or adult dose)', 'Pad', '') +
S.drugRow('Epinephrine', S.dStr(wt, 0.01, 1, ' mg'), 'IV / IO', 'After 2nd shock. Q3-5 min.') +
S.drugRow('Amiodarone', S.dStr(wt, 5, 300, ' mg'), 'IV / IO bolus', 'Refractory VF/VT. Max 15 mg/kg/day.') +
S.drugRow('Lidocaine', S.dStr(wt, 1, 100, ' mg'), 'IV / IO', 'Alternative to amiodarone. Then 20-50 mcg/kg/min gtt.') +
S.drugRow('Mg sulfate', S.dStr(wt, 50, 2000, ' mg'), 'IV over 1-2 min', 'For torsades or hypoMg.')
);
} else if (kind === 'vt') {
html += S.stepBox('#f59e0b', 'Stable monomorphic VT', 'Expert consult. Avoid combining QT-prolonging agents.');
html += S.arrow;
html += S.drugTable(
S.drugRow('Amiodarone', '5 mg/kg over 20-60 min', 'IV', '= ' + S.d(wt, 5, 300) + ' mg over 20-60 min') +
S.drugRow('Procainamide', '15 mg/kg over 30-60 min', 'IV', 'Do not combine with amiodarone') +
S.drugRow('Sync cardioversion', '0.5-1 → 2 J/kg', 'Pad', 'If becomes unstable. Sedate.')
);
}
html += S.ref('AHA PALS Guidelines 2020. Always verify against institutional protocols.');
el.innerHTML = html;
}
export function init() {
document.addEventListener('click', function(e) {
var btn = e.target.closest('[data-cardiac]');
if (btn) cardiacShow(btn.dataset.cardiac);
});
}

View file

@ -0,0 +1,40 @@
// ============================================================
// bedside/image-lightbox.js
// IMAGE LIGHTBOX — any element with data-img-src opens fullscreen.
// ============================================================
export function init() {
document.addEventListener('click', function(e) {
var opener = e.target.closest && e.target.closest('[data-img-src]');
if (opener) {
e.preventDefault();
var lb = document.getElementById('img-lightbox');
if (!lb) return;
var src = opener.dataset.imgSrc;
var title = opener.dataset.imgTitle || '';
document.getElementById('img-lightbox-img').src = src;
document.getElementById('img-lightbox-img').alt = title;
document.getElementById('img-lightbox-title').textContent = title;
document.getElementById('img-lightbox-open').href = src;
lb.style.display = 'flex';
return;
}
// Close on close button, on backdrop click (not on image), and on Escape (below)
var closeBtn = e.target.closest && e.target.closest('#img-lightbox-close');
var lb2 = document.getElementById('img-lightbox');
if (!lb2 || lb2.style.display !== 'flex') return;
if (closeBtn || e.target === lb2) {
lb2.style.display = 'none';
document.getElementById('img-lightbox-img').src = '';
}
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
var lb = document.getElementById('img-lightbox');
if (lb && lb.style.display === 'flex') {
lb.style.display = 'none';
document.getElementById('img-lightbox-img').src = '';
}
}
});
}

View file

@ -0,0 +1,46 @@
// ============================================================
// bedside/index.js
// Imports + registers all Bedside section modules on load.
// Loaded as <script type="module"> from index.html and e2e-harness.html.
// ============================================================
import './shared.js'; // side-effect: sets window._EM for back-compat
import * as ageWeight from './age-weight.js';
import * as subNav from './sub-nav.js';
import * as lightbox from './image-lightbox.js';
import * as neonatal from './neonatal.js';
import * as airway from './airway.js';
import * as cardiac from './cardiac.js';
import * as respiratory from './respiratory.js';
import * as ventilation from './ventilation.js';
import * as seizure from './seizure.js';
import * as sepsis from './sepsis.js';
import * as anaphylaxis from './anaphylaxis.js';
import * as sedation from './sedation.js';
import * as agitation from './agitation.js';
import * as antiemetics from './antiemetics.js';
import * as antimicrobials from './antimicrobials.js';
import * as burns from './burns.js';
import * as toxicology from './toxicology.js';
import * as trauma from './trauma.js';
[
ageWeight,
subNav,
lightbox,
neonatal,
airway,
cardiac,
respiratory,
ventilation,
seizure,
sepsis,
anaphylaxis,
sedation,
agitation,
antiemetics,
antimicrobials,
burns,
toxicology,
trauma,
].forEach(function(m) { if (m && typeof m.init === 'function') m.init(); });

View file

@ -0,0 +1,247 @@
// ============================================================
// bedside/neonatal.js
// NEONATAL ASSESSMENT (Fenton growth) + NRP pathway + Apgar.
// ============================================================
import { S } from './shared.js';
// Fenton 2013 LMS data (weight in grams, GA in weeks)
var fentonLMS = {
male: {
22:{L:0.21,M:496,S:0.17},23:{L:0.21,M:575,S:0.17},24:{L:0.21,M:660,S:0.17},25:{L:0.21,M:762,S:0.16},
26:{L:0.21,M:870,S:0.16},27:{L:0.20,M:993,S:0.15},28:{L:0.20,M:1124,S:0.15},29:{L:0.19,M:1272,S:0.14},
30:{L:0.18,M:1430,S:0.14},31:{L:0.17,M:1607,S:0.14},32:{L:0.15,M:1795,S:0.14},33:{L:0.13,M:2008,S:0.13},
34:{L:0.12,M:2230,S:0.13},35:{L:0.10,M:2467,S:0.13},36:{L:0.08,M:2710,S:0.13},37:{L:0.06,M:2948,S:0.12},
38:{L:0.04,M:3195,S:0.12},39:{L:0.02,M:3380,S:0.12},40:{L:0.01,M:3530,S:0.12},41:{L:0.00,M:3660,S:0.12},
42:{L:-0.02,M:3820,S:0.12}
},
female: {
22:{L:0.23,M:474,S:0.17},23:{L:0.22,M:538,S:0.17},24:{L:0.22,M:610,S:0.17},25:{L:0.22,M:705,S:0.16},
26:{L:0.22,M:810,S:0.16},27:{L:0.21,M:920,S:0.15},28:{L:0.21,M:1040,S:0.15},29:{L:0.20,M:1178,S:0.14},
30:{L:0.19,M:1330,S:0.14},31:{L:0.18,M:1500,S:0.14},32:{L:0.16,M:1680,S:0.14},33:{L:0.14,M:1880,S:0.13},
34:{L:0.12,M:2090,S:0.13},35:{L:0.10,M:2310,S:0.13},36:{L:0.08,M:2540,S:0.13},37:{L:0.06,M:2766,S:0.12},
38:{L:0.04,M:3000,S:0.12},39:{L:0.02,M:3180,S:0.12},40:{L:0.01,M:3340,S:0.12},41:{L:0.00,M:3480,S:0.12},
42:{L:-0.02,M:3630,S:0.12}
}
};
function interpolateLMS(table, val) {
var keys = Object.keys(table).map(Number).sort(function(a,b){return a-b;});
if (val <= keys[0]) return table[keys[0]];
if (val >= keys[keys.length-1]) return table[keys[keys.length-1]];
for (var i = 0; i < keys.length - 1; i++) {
if (val >= keys[i] && val <= keys[i+1]) {
var t = (val - keys[i]) / (keys[i+1] - keys[i]);
var a = table[keys[i]], b = table[keys[i+1]];
return { L: a.L + t*(b.L-a.L), M: a.M + t*(b.M-a.M), S: a.S + t*(b.S-a.S) };
}
}
return table[keys[0]];
}
function calcZ(value, L, M, S) {
if (Math.abs(L) < 0.001) return Math.log(value / M) / S;
return (Math.pow(value / M, L) - 1) / (L * S);
}
function zToPercentile(z) {
// Approximation of the standard normal CDF
var a1=0.254829592, a2=-0.284496736, a3=1.421413741, a4=-1.453152027, a5=1.061405429, p=0.3275911;
var sign = z < 0 ? -1 : 1;
var x = Math.abs(z) / Math.sqrt(2);
var t = 1 / (1 + p * x);
var y = 1 - (((((a5*t + a4)*t) + a3)*t + a2)*t + a1)*t * Math.exp(-x*x);
return Math.round(((1 + sign * y) / 2) * 1000) / 10;
}
function classifyGA(weeks, days) {
var total = weeks + (days || 0) / 7;
if (total < 28) return { label: 'Extremely Preterm', color: '#dc2626', icon: 'fa-triangle-exclamation' };
if (total < 32) return { label: 'Very Preterm', color: '#ea580c', icon: 'fa-triangle-exclamation' };
if (total < 34) return { label: 'Moderate Preterm', color: '#d97706', icon: 'fa-circle-exclamation' };
if (total < 37) return { label: 'Late Preterm', color: '#ca8a04', icon: 'fa-circle-info' };
if (total < 39) return { label: 'Early Term', color: '#2563eb', icon: 'fa-circle-info' };
if (total < 41) return { label: 'Full Term', color: '#16a34a', icon: 'fa-circle-check' };
if (total < 42) return { label: 'Late Term', color: '#d97706', icon: 'fa-circle-info' };
return { label: 'Post Term', color: '#dc2626', icon: 'fa-triangle-exclamation' };
}
function classifyWeight(percentile) {
if (percentile < 3) return { label: 'Severely SGA', color: '#dc2626', detail: '<3rd percentile' };
if (percentile < 10) return { label: 'SGA', color: '#ea580c', detail: '<10th percentile' };
if (percentile > 97) return { label: 'Severely LGA', color: '#dc2626', detail: '>97th percentile' };
if (percentile > 90) return { label: 'LGA', color: '#ea580c', detail: '>90th percentile' };
return { label: 'AGA', color: '#16a34a', detail: '10th-90th percentile' };
}
function classifyBirthWeight(grams) {
if (grams < 1000) return { label: 'Extremely Low Birth Weight (ELBW)', color: '#dc2626' };
if (grams < 1500) return { label: 'Very Low Birth Weight (VLBW)', color: '#ea580c' };
if (grams < 2500) return { label: 'Low Birth Weight (LBW)', color: '#d97706' };
if (grams <= 4000) return { label: 'Normal Birth Weight', color: '#16a34a' };
return { label: 'Macrosomia (>4000g)', color: '#ea580c' };
}
function neoAssess() {
var weeks = parseInt(document.getElementById('neo-ga-weeks').value);
var days = parseInt(document.getElementById('neo-ga-days').value) || 0;
var weight = parseFloat(document.getElementById('neo-weight').value);
var sex = document.getElementById('neo-sex').value;
var resultDiv = document.getElementById('neo-result');
if (!weeks || !weight) {
resultDiv.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter GA and birth weight.</p>';
return;
}
var gaDecimal = weeks + days / 7;
var gaClass = classifyGA(weeks, days);
var bwClass = classifyBirthWeight(weight);
// Calculate percentile using Fenton LMS
var lms = interpolateLMS(fentonLMS[sex], gaDecimal);
var z = calcZ(weight, lms.L, lms.M, lms.S);
var percentile = zToPercentile(z);
var wtClass = classifyWeight(percentile);
var expectedWeight = Math.round(lms.M);
var html = '';
// GA Classification
html += '<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px;">';
html += '<div style="flex:1;min-width:200px;padding:14px;border-radius:10px;background:' + gaClass.color + '10;border:1.5px solid ' + gaClass.color + '30;">';
html += '<div style="font-size:12px;color:var(--g500);margin-bottom:4px;">Gestational Age Classification</div>';
html += '<div style="font-size:18px;font-weight:700;color:' + gaClass.color + ';"><i class="fas ' + gaClass.icon + '"></i> ' + gaClass.label + '</div>';
html += '<div style="font-size:13px;color:var(--g600);margin-top:4px;">' + weeks + ' weeks ' + days + ' days (' + gaDecimal.toFixed(1) + ' weeks)</div>';
html += '</div>';
// Weight Classification (AGA/SGA/LGA)
html += '<div style="flex:1;min-width:200px;padding:14px;border-radius:10px;background:' + wtClass.color + '10;border:1.5px solid ' + wtClass.color + '30;">';
html += '<div style="font-size:12px;color:var(--g500);margin-bottom:4px;">Weight for Gestational Age</div>';
html += '<div style="font-size:18px;font-weight:700;color:' + wtClass.color + ';">' + wtClass.label + '</div>';
html += '<div style="font-size:13px;color:var(--g600);margin-top:4px;">' + percentile.toFixed(1) + 'th percentile (' + wtClass.detail + ')</div>';
html += '</div>';
html += '</div>';
// Birth Weight Category
html += '<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px;">';
html += '<div style="flex:1;min-width:200px;padding:14px;border-radius:10px;background:' + bwClass.color + '10;border:1.5px solid ' + bwClass.color + '30;">';
html += '<div style="font-size:12px;color:var(--g500);margin-bottom:4px;">Birth Weight Category</div>';
html += '<div style="font-size:18px;font-weight:700;color:' + bwClass.color + ';">' + bwClass.label + '</div>';
html += '<div style="font-size:13px;color:var(--g600);margin-top:4px;">' + weight + 'g (' + (weight/1000).toFixed(2) + ' kg)</div>';
html += '</div>';
// Stats
html += '<div style="flex:1;min-width:200px;padding:14px;border-radius:10px;background:var(--g50);border:1.5px solid var(--g200);">';
html += '<div style="font-size:12px;color:var(--g500);margin-bottom:4px;">Fenton Growth Data (' + (sex === 'male' ? 'Male' : 'Female') + ')</div>';
html += '<div style="font-size:13px;color:var(--g700);line-height:1.8;">';
html += '<strong>Expected weight (50th %ile):</strong> ' + expectedWeight + 'g<br>';
html += '<strong>Z-score:</strong> ' + z.toFixed(2) + '<br>';
html += '<strong>Percentile:</strong> ' + percentile.toFixed(1) + '%';
html += '</div></div>';
html += '</div>';
// Reference
html += '<p style="font-size:11px;color:var(--g400);margin:8px 0 0;">Fenton TR, Kim JH. A systematic review and meta-analysis to revise the Fenton growth chart for preterm infants. BMC Pediatrics 2013;13:59. GA classification per ACOG/AAP definitions.</p>';
resultDiv.innerHTML = html;
}
// ---- NRP pathway + drug calc -----------------------------------------
function renderPathway() {
var el = document.getElementById('nrp-pathway-static');
if (!el || el.dataset.rendered) return;
var html = '';
html += S.stepBox('#3b82f6', 'BIRTH — ASSESS (first 30 sec)', 'Term? Tone? Breathing/crying? If <strong>all yes</strong> → routine care with mother. If <strong>any no</strong> → warm, dry, stimulate, position airway, clear airway PRN, evaluate HR & respirations.');
html += S.arrow;
html += S.stepBox('#8b5cf6', 'HR &lt; 100 OR apneic/gasping (60 sec)', '<strong>Start PPV</strong> 40-60 breaths/min, room air for term / 21-30% for preterm. Attach SpO2 (right hand) &plusmn; ECG. MR SOPA if ineffective: <em>M</em>ask adjust, <em>R</em>eposition, <em>S</em>uction, <em>O</em>pen mouth, <em>P</em>ressure increase, <em>A</em>lternative airway.');
html += S.arrow;
html += S.stepBox('#f59e0b', 'HR &lt; 100 after 30 sec effective PPV', 'Reassess ventilation — ensure chest rise. Consider increasing FiO2, intubation, or LMA. Continue PPV.');
html += S.arrow;
html += S.stepBox('#ef4444', 'HR &lt; 60 after 30 sec of effective PPV', '<strong>Intubate + Chest compressions</strong> — 3:1 ratio (90 compressions + 30 breaths per minute), FiO2 100%, lower 1/3 sternum, depth 1/3 AP chest.');
html += S.arrow;
html += S.stepBox('#dc2626', 'HR &lt; 60 despite compressions + PPV x 60 sec', '<strong>Epinephrine</strong> 1:10,000 (0.1 mg/mL): <br>&bull; <strong>IV/IO:</strong> 0.01-0.03 mg/kg (0.1-0.3 mL/kg) — preferred<br>&bull; <strong>ETT:</strong> 0.05-0.1 mg/kg (0.5-1 mL/kg) — while IV being placed<br>Repeat q3-5 min. If hypovolemia: <strong>NS 10 mL/kg IV/IO</strong> over 5-10 min.');
html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:8px;margin-top:14px;">';
html += '<div style="padding:10px;background:var(--g50);border-radius:6px;font-size:12px;"><strong>Target SpO2 (preductal):</strong><br>1 min: 60-65%<br>2 min: 65-70%<br>3 min: 70-75%<br>4 min: 75-80%<br>5 min: 80-85%<br>10 min: 85-95%</div>';
html += '<div style="padding:10px;background:var(--g50);border-radius:6px;font-size:12px;"><strong>Initial ETT size:</strong><br>&lt;1 kg / &lt;28 wk: <strong>2.5</strong><br>1-2 kg / 28-34 wk: <strong>3.0</strong><br>2-3 kg / 34-38 wk: <strong>3.5</strong><br>&gt;3 kg / &gt;38 wk: <strong>3.5-4.0</strong></div>';
html += '<div style="padding:10px;background:var(--g50);border-radius:6px;font-size:12px;"><strong>ETT depth (lip):</strong><br>~ 6 + weight(kg) cm</div>';
html += '</div>';
el.innerHTML = html;
el.dataset.rendered = '1';
}
function calcNRP() {
var wt = parseFloat(document.getElementById('nrp-weight').value);
var el = document.getElementById('nrp-result');
if (!wt) { el.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter weight (kg).</p>'; return; }
var epiIvLow = Math.round(wt * 0.01 * 100) / 100;
var epiIvHigh = Math.round(wt * 0.03 * 100) / 100;
var epiEtLow = Math.round(wt * 0.05 * 100) / 100;
var epiEtHigh = Math.round(wt * 0.1 * 100) / 100;
var ns = Math.round(wt * 10);
var d10 = Math.round(wt * 2 * 10) / 10;
var html = '<div style="padding:10px 14px;border-radius:8px;background:var(--red)10;border:1.5px solid var(--red);margin-bottom:10px;"><strong style="color:var(--red);">NRP doses — ' + wt + ' kg</strong></div>';
var pk = function(txt) { return ' <span style="color:var(--g500);font-size:11px;">(' + txt + ')</span>'; };
html += S.drugTable(
S.drugRow('Epinephrine 1:10,000', epiIvLow + '-' + epiIvHigh + ' mg, ' + (Math.round(epiIvLow*10)/10) + '-' + (Math.round(epiIvHigh*10)/10) + ' mL' + pk('0.01-0.03 mg/kg = 0.1-0.3 mL/kg'), 'IV / IO', 'Preferred route. Repeat q3-5 min.') +
S.drugRow('Epinephrine 1:10,000', epiEtLow + '-' + epiEtHigh + ' mg, ' + (Math.round(epiEtLow*10)/10) + '-' + (Math.round(epiEtHigh*10)/10) + ' mL' + pk('0.05-0.1 mg/kg = 0.5-1 mL/kg'), 'ETT', 'While IV being placed.') +
S.drugRow('Normal saline', ns + ' mL' + pk('10 mL/kg'), 'IV / IO', 'Over 5-10 min for volume. Repeat PRN.') +
S.drugRow('Dextrose 10%', d10 + ' mL' + pk('2 mL/kg = 0.2 g/kg'), 'IV slow push', 'For documented hypoglycemia. Then D10 infusion 4-6 mg/kg/min.')
);
html += '<div style="padding:10px;background:#fef3c7;border-radius:6px;font-size:12px;color:#92400e;"><strong>Concentration note:</strong> NRP uses epinephrine <strong>1:10,000</strong> (0.1 mg/mL). NOT 1:1000 (1 mg/mL) — that is IM for anaphylaxis / older patients.</div>';
html += S.ref('AHA/AAP Neonatal Resuscitation Program (NRP) 8th edition, 2020.');
el.innerHTML = html;
}
// ---- Apgar -----------------------------------------------------------
function calcApgar() {
var keys = ['appearance','pulse','grimace','activity','respiration'];
var total = keys.reduce(function(s, k) {
return s + (parseInt(document.getElementById('apgar-' + k).value) || 0);
}, 0);
var el = document.getElementById('apgar-result');
var color, severity, guidance;
if (total >= 7) {
color = '#10b981'; severity = 'Reassuring';
guidance = 'Routine newborn care. Continue reassessment. Repeat at 5 min.';
} else if (total >= 4) {
color = '#f59e0b'; severity = 'Moderately depressed';
guidance = 'Stimulate, clear airway, warm. Give O2 if cyanotic. Ventilate with PPV if HR &lt;100 or apneic/gasping. Reassess q30 sec.';
} else {
color = '#ef4444'; severity = 'Severely depressed';
guidance = 'Full NRP pathway — PPV immediately. Intubate if PPV ineffective. Chest compressions if HR &lt;60. Epinephrine and volume per NRP.';
}
var html = '<div style="padding:10px 14px;border-radius:8px;background:' + color + '10;border:1.5px solid ' + color + ';">';
html += '<strong style="color:' + color + ';font-size:16px;">Apgar: ' + total + '/10 — ' + severity + '</strong>';
html += '<div style="font-size:12px;color:var(--g700);margin-top:6px;line-height:1.5;">' + guidance + '</div></div>';
html += '<p style="font-size:11px;color:var(--g400);margin-top:8px;font-style:italic;">Apgar is a description of status — <strong>never</strong> delay resuscitation while scoring. Follow NRP algorithm based on HR and breathing. Apgar &lt;7 at 5 min: repeat q5 min up to 20 min.</p>';
el.innerHTML = html;
}
export function init() {
// Neonatal assessment
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-neo-assess' || e.target.closest('#btn-neo-assess')) neoAssess();
});
// NRP pathway render + calc
document.addEventListener('DOMContentLoaded', renderPathway);
document.addEventListener('tabChanged', function() { setTimeout(renderPathway, 100); });
if (document.readyState !== 'loading') setTimeout(renderPathway, 0);
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-nrp-calc' || e.target.closest('#btn-nrp-calc')) calcNRP();
// Render NRP pathway when Neonatal sub-pill is clicked (in case component loaded late)
var np = e.target.closest && e.target.closest('[data-em="neonatal"]');
if (np) setTimeout(renderPathway, 0);
var nav = e.target.closest && e.target.closest('[data-calc="bedside"]');
if (nav) setTimeout(renderPathway, 50);
});
// Apgar
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-apgar-calc' || e.target.closest('#btn-apgar-calc')) calcApgar();
});
}

View file

@ -0,0 +1,174 @@
// ============================================================
// bedside/respiratory.js
// RESPIRATORY MANAGEMENT (Asthma, PRAM, Croup, Bronchiolitis).
// Keeps its own local dose/doseStr/drugRow/drugTable helpers —
// the layout differs from S.drugTable (no 'Notes' column alignment).
// ============================================================
import { S } from './shared.js';
function dose(wt, mgPerKg, maxMg) { var d = Math.round(wt * mgPerKg * 10) / 10; return maxMg ? Math.min(d, maxMg) : d; }
// Delegates to the shared S.dStr so the per-kg math is visible to the prescriber.
function doseStr(wt, mgPerKg, maxMg, unit) {
return S.dStr(wt, mgPerKg, maxMg, unit);
}
function drugRow(name, doseText, route, notes) {
return '<tr><td style="font-weight:600;">' + name + '</td><td>' + doseText + '</td><td>' + route + '</td><td style="font-size:12px;color:var(--g500);">' + (notes || '') + '</td></tr>';
}
function drugTable(rows) {
return '<div style="overflow-x:auto;-webkit-overflow-scrolling:touch;margin:8px 0;"><table style="width:100%;min-width:500px;border-collapse:collapse;font-size:13px;"><thead><tr style="background:var(--g100);"><th style="text-align:left;padding:6px 8px;">Drug</th><th style="padding:6px 8px;">Dose</th><th style="padding:6px 8px;">Route</th><th style="padding:6px 8px;">Notes</th></tr></thead><tbody>' + rows + '</tbody></table></div>';
}
function severityBadge(label, color) { return '<span style="display:inline-block;padding:4px 12px;border-radius:6px;background:' + color + '20;color:' + color + ';font-weight:700;font-size:14px;">' + label + '</span>'; }
function asthmaManagement(severity) {
var wt = parseFloat(document.getElementById('resp-weight').value);
var resultDiv = document.getElementById('asthma-result');
if (!wt) { resultDiv.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter weight (kg).</p>'; return; }
var html = '';
if (severity === 'mild') {
html += severityBadge('Mild Exacerbation', '#10b981');
html += '<p style="font-size:13px;margin:8px 0;">Speaks in sentences, no accessory muscle use, SpO2 ≥94%</p>';
html += drugTable(
drugRow('Albuterol (MDI)', '4-8 puffs via spacer', 'Inhaled', 'q20min x 3 doses, then q1-4h') +
drugRow('Albuterol (neb)', doseStr(wt, 0.15, 5, ' mg') + ' (min 2.5 mg)', 'Nebulized', 'q20min x 3 doses') +
drugRow('Dexamethasone', doseStr(wt, 0.6, 16), 'PO/IV', 'Single dose, or 2 days') +
drugRow('Prednisolone', doseStr(wt, 1, 60) + '/day', 'PO', 'Alternative: 3-5 day course')
);
html += '<div style="margin-top:8px;padding:8px 12px;background:#d1fae5;border-radius:6px;font-size:12px;color:#065f46;"><strong>Reassess after 1 hour.</strong> If improving → discharge with albuterol MDI + spacer + oral steroid course. If not improving → escalate to moderate.</div>';
} else if (severity === 'moderate') {
html += severityBadge('Moderate Exacerbation', '#f59e0b');
html += '<p style="font-size:13px;margin:8px 0;">Speaks in phrases, some accessory muscle use, SpO2 90-93%</p>';
html += drugTable(
drugRow('Albuterol (neb)', doseStr(wt, 0.15, 5, ' mg') + ' (min 2.5 mg)', 'Nebulized', 'q20min x 3 doses, then continuous if needed') +
drugRow('Ipratropium', wt < 20 ? '250 mcg' : '500 mcg', 'Nebulized', 'q20min x 3 doses with albuterol') +
drugRow('Dexamethasone', doseStr(wt, 0.6, 16), 'PO/IV/IM', 'Single dose') +
drugRow('O2 supplemental', 'Target SpO2 ≥94%', 'Nasal cannula/mask', 'Titrate to effect')
);
html += '<div style="margin-top:8px;padding:8px 12px;background:#fef3c7;border-radius:6px;font-size:12px;color:#92400e;"><strong>Reassess after 1-2 hours.</strong> If improving → step down to mild protocol. If worsening or no improvement → escalate to severe.</div>';
} else {
html += severityBadge('Severe / Life-Threatening', '#ef4444');
html += '<p style="font-size:13px;margin:8px 0;">Speaks in words only, significant accessory muscle use, SpO2 &lt;90%. Consider ICU.</p>';
html += drugTable(
drugRow('Albuterol continuous', doseStr(wt, 0.5, 20, ' mg') + '/hr', 'Continuous neb', 'Or 0.15-0.3 mg/kg q20min') +
drugRow('Ipratropium', wt < 20 ? '250 mcg' : '500 mcg', 'Nebulized', 'q20min x 3 doses with albuterol') +
drugRow('Dexamethasone', doseStr(wt, 0.6, 16), 'IV', 'Or methylprednisolone 2 mg/kg IV (max 60 mg)') +
drugRow('Magnesium sulfate', doseStr(wt, 50, 2000) + ' IV over 20 min', 'IV', 'Single dose, monitor BP') +
drugRow('Epinephrine (IM)', doseStr(wt, 0.01, 0.5) + ' (1:1000)', 'IM', 'If impending arrest / no IV access') +
drugRow('Terbutaline', doseStr(wt, 0.01, 0.4) + ' SC/IV', 'SC or IV bolus', 'Then 0.1-10 mcg/kg/min infusion') +
drugRow('O2 supplemental', 'Target SpO2 ≥94%', 'High flow / NIPPV', 'Consider BiPAP/CPAP')
);
html += '<div style="margin-top:8px;padding:8px 12px;background:#fee2e2;border-radius:6px;font-size:12px;color:#991b1b;"><strong>Continuous monitoring.</strong> Consider ICU admission. If no response to magnesium → terbutaline infusion. If impending respiratory failure → intubation (ketamine preferred induction agent).</div>';
// Clinical decision points specific to severe asthma
html += '<div style="margin-top:10px;display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:8px;">';
// ABG
html += '<div style="padding:10px 12px;background:white;border:1px solid var(--g200);border-radius:8px;font-size:12px;line-height:1.5;">' +
'<div style="font-weight:700;color:var(--blue-dark);margin-bottom:4px;"><i class="fas fa-flask"></i> When to obtain a blood gas</div>' +
'&bull; Severe exacerbation <strong>not responding</strong> after 1-2 h aggressive therapy<br>' +
'&bull; Impending respiratory failure — altered mentation, fatigue, silent chest<br>' +
'&bull; SpO2 &lt;92% despite supplemental O2<br>' +
'&bull; Suspected CO2 retention (rising PCO2 in a previously tachypneic patient)<br>' +
'<strong style="color:#991b1b;">Interpretation:</strong> asthmatics hyperventilate → expect <em>low</em> PCO2. A <strong>"normal" or rising PCO2 = impending failure</strong>. Don\'t delay treatment for the gas.' +
'</div>';
// Intubation
html += '<div style="padding:10px 12px;background:white;border:1px solid var(--g200);border-radius:8px;font-size:12px;line-height:1.5;">' +
'<div style="font-weight:700;color:#dc2626;margin-bottom:4px;"><i class="fas fa-stethoscope"></i> When to intubate (mostly clinical)</div>' +
'&bull; <strong>Absolute:</strong> apnea, cardiac arrest, coma, inability to protect airway<br>' +
'&bull; Progressive fatigue despite maximal therapy (NIV, mag, terbutaline, heliox)<br>' +
'&bull; Refractory hypoxemia / hypercapnia with acidosis<br>' +
'&bull; Silent chest + deteriorating consciousness<br>' +
'<strong>RSI choice:</strong> <span style="color:#065f46;font-weight:600;">ketamine 1-2 mg/kg</span> (bronchodilator, preserves BP) + <span style="color:#065f46;font-weight:600;">rocuronium</span>. Avoid succs if hyperkalemic. Permissive hypercapnia after tube; allow expiration (low rate, I:E 1:3-4).' +
'</div>';
// Heliox
html += '<div style="padding:10px 12px;background:white;border:1px solid var(--g200);border-radius:8px;font-size:12px;line-height:1.5;">' +
'<div style="font-weight:700;color:#7c3aed;margin-bottom:4px;"><i class="fas fa-wind"></i> Heliox (He/O2)</div>' +
'&bull; Lower gas density → less turbulence → reduced work of breathing through narrowed airways.<br>' +
'&bull; <strong>Consider in:</strong> severe asthma not responding to max therapy (bridge), upper airway obstruction (croup, post-extubation stridor, FB partial).<br>' +
'&bull; Typical mix: <strong>70:30 He:O2</strong> (or 80:20). Delivered via tight non-rebreather or in-line with neb.<br>' +
'&bull; <strong>Limitation:</strong> requires FiO2 ≤ 30-40%. If patient needs higher FiO2, heliox can\'t help.<br>' +
'&bull; Evidence in asthma is mixed — use as adjunct, not substitute.' +
'</div>';
html += '</div>';
}
html += '<p style="font-size:11px;color:var(--g400);margin:12px 0 0;">NAEPP/GINA guidelines. Always use clinical judgment. Verify doses against institutional protocols.</p>';
resultDiv.innerHTML = html;
}
function calcPRAM() {
var total = ['pram-spo2','pram-retractions','pram-scalene','pram-air','pram-wheeze'].reduce(function(s, id) {
return s + parseInt(document.getElementById(id).value);
}, 0);
var severity = total <= 3 ? 'Mild' : total <= 7 ? 'Moderate' : 'Severe';
var color = total <= 3 ? '#10b981' : total <= 7 ? '#f59e0b' : '#ef4444';
document.getElementById('pram-result').innerHTML = '<div style="padding:10px 14px;border-radius:8px;background:' + color + '10;border:1.5px solid ' + color + '30;"><span style="font-size:18px;font-weight:700;color:' + color + ';">PRAM Score: ' + total + '/12 — ' + severity + '</span><p style="font-size:12px;color:var(--g600);margin:4px 0 0;">Mild (0-3): outpatient management. Moderate (4-7): consider oral steroids + frequent bronchodilators. Severe (8-12): aggressive treatment, consider ICU.</p></div>';
}
function calcCroup() {
var wt = parseFloat(document.getElementById('resp-weight').value) || 10;
var total = ['croup-conscious','croup-cyanosis','croup-stridor','croup-air','croup-retractions'].reduce(function(s, id) {
return s + parseInt(document.getElementById(id).value);
}, 0);
var severity, color, treatment;
if (total <= 2) { severity = 'Mild'; color = '#10b981'; treatment = drugTable(drugRow('Dexamethasone', doseStr(wt, 0.6, 16), 'PO (single dose)', 'Preferred corticosteroid') + drugRow('Supportive care', 'Cool mist, comfort measures', '', 'Discharge if tolerating PO')); }
else if (total <= 5) { severity = 'Moderate'; color = '#f59e0b'; treatment = drugTable(drugRow('Dexamethasone', doseStr(wt, 0.6, 16), 'PO/IM', 'Single dose') + drugRow('Racemic epinephrine', '0.5 mL of 2.25% solution', 'Nebulized', 'Observe 2-4 hrs after for rebound') + drugRow('Nebulized epinephrine', '0.5 mL/kg of 1:1000 (max 5 mL)', 'Nebulized', 'Alternative to racemic')); }
else if (total <= 11) { severity = 'Severe'; color = '#ef4444'; treatment = drugTable(drugRow('Dexamethasone', doseStr(wt, 0.6, 16), 'IV/IM', 'Immediate') + drugRow('Racemic epinephrine', '0.5 mL of 2.25% solution', 'Nebulized', 'May repeat q15-20min, observe 2-4 hrs') + drugRow('Nebulized epinephrine', '0.5 mL/kg of 1:1000 (max 5 mL)', 'Nebulized', 'Alternative') + drugRow('Heliox', '70:30 or 80:20', 'Face mask', 'Consider if not responding') + drugRow('O2 supplemental', 'Target SpO2 ≥94%', 'Blow-by preferred', 'Minimize agitation')); }
else { severity = 'Impending Respiratory Failure'; color = '#dc2626'; treatment = '<div style="padding:10px;background:#fee2e2;border-radius:6px;font-size:13px;color:#991b1b;font-weight:600;">Immediate airway management. Prepare for intubation (use ETT 0.5-1 size smaller than predicted). Call anesthesia/ENT. Continue nebulized epinephrine and dexamethasone IV.</div>'; }
document.getElementById('croup-result').innerHTML = '<div style="padding:10px 14px;border-radius:8px;background:' + color + '10;border:1.5px solid ' + color + '30;margin-bottom:12px;"><span style="font-size:18px;font-weight:700;color:' + color + ';">Westley Score: ' + total + '/17 — ' + severity + '</span></div>' + treatment + '<p style="font-size:11px;color:var(--g400);margin:8px 0 0;">Westley WJ et al. Nebulized racemic epinephrine by IPPB for the treatment of croup. Am J Dis Child 1978. Mild ≤2, Moderate 3-5, Severe 6-11, Impending failure ≥12.</p>';
}
function calcBronchiolitis() {
var wt = parseFloat(document.getElementById('resp-weight').value) || 5;
var age = document.getElementById('bronch-age').value;
var spo2 = document.getElementById('bronch-spo2').value;
var hydration = document.getElementById('bronch-hydration').value;
var distress = document.getElementById('bronch-distress').value;
var html = '<h4 style="font-size:15px;font-weight:700;color:var(--g800);margin:0 0 8px;">Bronchiolitis Management</h4>';
var admit = distress === 'severe' || spo2 === 'low' || hydration === 'poor' || age === '<12w';
if (admit) {
html += severityBadge('Admit / Observe', '#ef4444');
html += '<div style="margin:10px 0;">';
if (age === '<12w') html += '<p style="font-size:13px;color:var(--red);font-weight:600;">⚠ Age &lt;12 weeks — high risk for apnea. Monitor closely.</p>';
html += '</div>';
} else {
html += severityBadge('Likely Safe for Discharge', '#10b981');
}
html += '<div style="margin:12px 0;"><strong style="font-size:13px;">Supportive Care (evidence-based):</strong></div>';
html += drugTable(
drugRow('Nasal suctioning', 'Bulb suction or NasalClear', 'Nasal', 'Before feeds and as needed') +
drugRow('O2 supplemental', 'Target SpO2 ≥90%', 'NC / high flow', spo2 === 'low' ? 'Required' : 'If needed') +
drugRow('Hypertonic saline 3%', '4 mL nebulized', 'Nebulized', 'Consider in inpatients (AAP weak recommendation)') +
(hydration === 'poor' ? drugRow('IV fluids', 'NS/LR bolus ' + doseStr(wt, 20, null, ' mL'), 'IV', 'Then maintenance D5 0.45NS') + drugRow('NG feeds', 'If unable to feed orally', 'NG tube', 'Preferred over IV if gut functional') : '')
);
html += '<div style="margin:12px 0;padding:10px 12px;background:var(--amber-light);border-radius:6px;font-size:12px;color:#92400e;"><strong>NOT recommended (AAP 2014/2023):</strong> Albuterol/salbutamol (no benefit in bronchiolitis), epinephrine (no evidence of benefit), systemic corticosteroids (no benefit), antibiotics (unless secondary bacterial infection), chest physiotherapy.</div>';
html += '<p style="font-size:11px;color:var(--g400);margin:8px 0 0;">AAP Clinical Practice Guideline: Management of Bronchiolitis in Infants and Children (2014, reaffirmed 2023). RSV is the most common cause (50-80%).</p>';
document.getElementById('bronch-result').innerHTML = html;
}
export function init() {
// Sub-pill navigation
document.addEventListener('click', function(e) {
var pill = e.target.closest('[data-resp]');
if (pill) {
document.querySelectorAll('[data-resp]').forEach(function(p) { p.classList.remove('active'); });
pill.classList.add('active');
['asthma','croup','bronchiolitis'].forEach(function(s) {
var el = document.getElementById('resp-' + s);
if (el) el.style.display = pill.dataset.resp === s ? '' : 'none';
});
}
if (e.target.closest('[data-asthma-severity]')) asthmaManagement(e.target.closest('[data-asthma-severity]').dataset.asthmaSeverity);
if (e.target.id === 'btn-pram-calc' || e.target.closest('#btn-pram-calc')) calcPRAM();
if (e.target.id === 'btn-croup-calc' || e.target.closest('#btn-croup-calc')) calcCroup();
if (e.target.id === 'btn-bronch-calc' || e.target.closest('#btn-bronch-calc')) calcBronchiolitis();
});
}

View file

@ -0,0 +1,112 @@
// ============================================================
// bedside/sedation.js
// PROCEDURAL SEDATION — agents + reversal, drugs.json-backed.
// ============================================================
// Pulls in shared side-effect (window._EM) in case anything else needs it.
import './shared.js';
function d(wt, mgPerKg, max) { var v = Math.round(wt * mgPerKg * 100) / 100; return max ? Math.min(v, max) : v; }
// Fallback inline data — mirrors drugs.json sedation section. The first
// table's rows group by drug (Ketamine has both IV + IM lines), hence the
// display vs name split. `reverses` marks the final 2 rows as reversal
// agents shown in a separate table.
var SED_FALLBACK = [
{ name: 'Ketamine IV', display: 'Ketamine', dose_mg_per_kg_low: 1.5, dose_mg_per_kg_high: 2, max_mg_low: 100, max_mg_high: 150, unit: 'mg', route: 'IV', onset: '1 min', duration: '15-30 min', notes: 'Dissociative. Preserves airway reflexes.' },
{ name: 'Ketamine IM', display: '', dose_mg_per_kg_low: 4, dose_mg_per_kg_high: 5, max_mg_low: 300, max_mg_high: 400, unit: 'mg', route: 'IM', onset: '3-5 min', duration: '30-60 min', notes: 'Give atropine 0.01 mg/kg to reduce secretions.' },
{ name: 'Midazolam IV', display: 'Midazolam', dose_mg_per_kg_low: 0.05, dose_mg_per_kg_high: 0.1, max_mg_low: 2, max_mg_high: 5, unit: 'mg', route: 'IV', onset: '2-3 min', duration: '30-60 min', notes: 'Anxiolysis. Titrate q3-5 min.' },
{ name: 'Midazolam IN/PO', display: '', dose_mg_per_kg: 0.5, max_mg: 20, unit: 'mg', route: 'IN/PO', onset: '10-15 min', duration: '30-60 min', notes: 'IN (max 10 mg / naris) or PO (max 20 mg).' },
{ name: 'Propofol', display: 'Propofol', dose_mg_per_kg: 1, max_mg: 40, unit: 'mg', route: 'IV', onset: '30 sec', duration: '5-10 min', notes: 'Short procedures. Causes apnea — manage airway.' },
{ name: 'Fentanyl IV', display: 'Fentanyl', dose_mg_per_kg: 1, max_mg: 100, unit: 'mcg', route: 'IV', onset: '2-3 min', duration: '30-60 min', notes: 'Analgesic. Often combined with midazolam.' },
{ name: 'Fentanyl IN', display: '', dose_mg_per_kg: 2, max_mg: 100, unit: 'mcg', route: 'IN', onset: '5-10 min', duration: '30-60 min', notes: 'Intranasal.' },
{ name: 'Nitrous oxide', display: 'Nitrous oxide', dose_display: '50:50 or 70:30 mix', unit: 'mix', route: 'Inhaled',onset: '2-5 min', duration: '5 min off', notes: 'Self-administered via demand valve. Anxiolysis + mild analgesia.' },
{ name: 'Dexmedetomidine IN', display: 'Dexmedetomidine', dose_mg_per_kg_low: 2, dose_mg_per_kg_high: 3, max_mg_low: 100, max_mg_high: null, unit: 'mcg', route: 'IN', onset: '15-30 min', duration: '60-90 min', notes: 'Intranasal. No respiratory depression. Good for imaging.' },
{ name: 'Naloxone', display: 'Naloxone', dose_mg_per_kg: 0.1, max_mg: 2, unit: 'mg', route: 'IV/IM/IN', reverses: 'Opioids (fentanyl, morphine)', notes: 'Max 2 mg. Repeat q2-3 min. Duration shorter than opioids — monitor for re-sedation.' },
{ name: 'Flumazenil', display: 'Flumazenil', dose_mg_per_kg: 0.01, max_mg: 0.2, unit: 'mg', route: 'IV', reverses: 'Benzodiazepines (midazolam)', notes: 'Max 0.2 mg single dose. Repeat q1 min to max 1 mg total. Risk of seizures — use cautiously.' }
];
function sedDrugs() {
var s = window._DRUGS && window._DRUGS.sections && window._DRUGS.sections.sedation;
if (s && s.drugs && s.drugs.length) {
return s.drugs.map(function(dg) {
var fb = SED_FALLBACK.filter(function(x) { return x.name === dg.name; })[0] || {};
return Object.assign({}, fb, dg);
});
}
return SED_FALLBACK;
}
// perKg footer that matches the original inline format exactly.
function sedPerKg(lo, hi, unit) {
unit = unit || 'mg';
return ' <span style="color:var(--g500);font-size:11px;">(' + lo + (hi != null ? '-' + hi : '') + ' ' + unit + '/kg)</span>';
}
// Build the Dose cell text for a sedation drug (supports low/high range
// and single-dose entries; honors custom dose_display for fixed doses).
function sedDoseCell(wt, dg) {
if (dg.dose_display) return dg.dose_display;
var unitLabel = dg.unit === 'mcg' ? ' mcg' : (dg.unit === 'mix' ? '' : ' mg');
var perKgUnit = dg.unit === 'mcg' ? 'mcg' : 'mg';
if (dg.dose_mg_per_kg_low != null) {
var lo = d(wt, dg.dose_mg_per_kg_low, dg.max_mg_low);
if (dg.dose_mg_per_kg_high != null) {
var hi = d(wt, dg.dose_mg_per_kg_high, dg.max_mg_high);
return lo + '-' + hi + unitLabel + sedPerKg(dg.dose_mg_per_kg_low, dg.dose_mg_per_kg_high, perKgUnit);
}
return lo + unitLabel + sedPerKg(dg.dose_mg_per_kg_low, null, perKgUnit);
}
if (dg.dose_mg_per_kg != null) {
var v = d(wt, dg.dose_mg_per_kg, dg.max_mg);
return v + unitLabel + sedPerKg(dg.dose_mg_per_kg, null, perKgUnit);
}
return '';
}
function sedRow(wt, dg, showReverses) {
var displayName = dg.display == null ? dg.name : dg.display;
var nameCell = displayName
? '<td style="font-weight:600;">' + displayName + '</td>'
: '<td></td>';
var dose = sedDoseCell(wt, dg);
if (showReverses) {
return '<tr>' + nameCell + '<td>' + dose + '</td><td>' + dg.route + '</td><td>' + (dg.reverses || '') + '</td><td style="font-size:12px;color:var(--g500);">' + dg.notes + '</td></tr>';
}
return '<tr>' + nameCell + '<td>' + dose + '</td><td>' + dg.route + '</td><td>' + (dg.onset || '') + '</td><td>' + (dg.duration || '') + '</td><td style="font-size:12px;color:var(--g500);">' + dg.notes + '</td></tr>';
}
function calcSedation() {
var wt = parseFloat(document.getElementById('sed-weight').value);
var resultDiv = document.getElementById('sed-result');
if (!wt) { resultDiv.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter weight (kg).</p>'; return; }
var all = sedDrugs();
var agents = all.filter(function(dg) { return !dg.reverses; });
var reversals = all.filter(function(dg) { return !!dg.reverses; });
var html = '<div style="padding:10px 14px;border-radius:8px;background:var(--purple-light);border:1.5px solid var(--purple);margin-bottom:12px;"><span style="font-size:14px;font-weight:700;color:var(--purple);">Procedural Sedation — ' + wt + ' kg patient</span></div>';
// Sedation agents
html += '<h4 style="font-size:14px;font-weight:700;color:var(--g800);margin:0 0 8px;">Sedation Agents</h4>';
html += '<div style="overflow-x:auto;-webkit-overflow-scrolling:touch;margin-bottom:16px;"><table style="width:100%;min-width:640px;border-collapse:collapse;font-size:13px;"><thead><tr style="background:var(--g100);"><th style="text-align:left;padding:6px 8px;">Drug</th><th style="padding:6px 8px;">Dose</th><th style="padding:6px 8px;">Route</th><th style="padding:6px 8px;">Onset</th><th style="padding:6px 8px;">Duration</th><th style="padding:6px 8px;">Notes</th></tr></thead><tbody>';
html += agents.map(function(dg) { return sedRow(wt, dg, false); }).join('');
html += '</tbody></table></div>';
// Reversal agents
html += '<h4 style="font-size:14px;font-weight:700;color:var(--red);margin:0 0 8px;"><i class="fas fa-rotate-left"></i> Reversal Agents</h4>';
html += '<div style="overflow-x:auto;-webkit-overflow-scrolling:touch;"><table style="width:100%;min-width:560px;border-collapse:collapse;font-size:13px;"><thead><tr style="background:#fee2e2;"><th style="text-align:left;padding:6px 8px;">Drug</th><th style="padding:6px 8px;">Dose</th><th style="padding:6px 8px;">Route</th><th style="padding:6px 8px;">Reverses</th><th style="padding:6px 8px;">Notes</th></tr></thead><tbody>';
html += reversals.map(function(dg) { return sedRow(wt, dg, true); }).join('');
html += '</tbody></table></div>';
html += '<div style="margin-top:12px;padding:10px 12px;background:var(--amber-light);border-radius:6px;font-size:12px;color:#92400e;"><strong>Pre-sedation checklist:</strong> NPO status (2h clear liquids, 6h solids), consent, monitoring equipment (pulse ox, capnography, BP), resuscitation equipment at bedside, suction ready, IV access. Minimum monitoring: continuous SpO2, HR, capnography. Provider capable of managing airway must be present.</div>';
html += '<p style="font-size:11px;color:var(--g400);margin:8px 0 0;">AAP Guidelines for Monitoring and Management of Pediatric Patients Before, During, and After Sedation. ASA Practice Guidelines for Sedation and Analgesia by Non-Anesthesiologists.</p>';
resultDiv.innerHTML = html;
}
export function init() {
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-sed-calc' || e.target.closest('#btn-sed-calc')) calcSedation();
});
}

View file

@ -0,0 +1,130 @@
// ============================================================
// bedside/seizure.js
// SEIZURE MANAGEMENT (status epilepticus pathway).
// Keeps its own local `d` + `row` helpers used by the refractory
// infusion rendering.
// ============================================================
import { S } from './shared.js';
function d(wt, mgPerKg, max) { var v = Math.round(wt * mgPerKg * 100) / 100; return max ? Math.min(v, max) : v; }
function row(phase, drug, dose, route, notes) {
return '<tr><td style="font-weight:600;color:var(--blue);">' + phase + '</td><td style="font-weight:600;">' + drug + '</td><td>' + dose + '</td><td>' + route + '</td><td style="font-size:12px;color:var(--g500);">' + notes + '</td></tr>';
}
// Visual pathway row — time badge + step card linked by vertical line
function stepRow(time, color, title, bodyHtml, isLast) {
return '<div style="display:flex;gap:12px;align-items:stretch;">' +
'<div style="flex:0 0 auto;width:70px;display:flex;flex-direction:column;align-items:center;">' +
'<div style="background:' + color + ';color:white;padding:6px 0;border-radius:20px;font-size:12px;font-weight:700;width:60px;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,0.15);">' + time + '</div>' +
(isLast ? '' : '<div style="flex:1;width:3px;background:' + color + '40;margin:2px 0;"></div>') +
'</div>' +
'<div style="flex:1;padding:10px 14px;border-left:4px solid ' + color + ';background:' + color + '10;border-radius:0 8px 8px 0;margin-bottom:' + (isLast ? '4px' : '12px') + ';">' +
'<div style="font-weight:700;color:' + color + ';font-size:13px;margin-bottom:6px;text-transform:uppercase;letter-spacing:0.3px;">' + title + '</div>' +
'<div style="font-size:13px;color:var(--g700);line-height:1.55;">' + bodyHtml + '</div>' +
'</div>' +
'</div>';
}
// Fallback inline data — mirrors the non-refractory seizure entries in
// drugs.json. Refractory infusion drips are kept inline below because
// their dose strings (bolus → gtt range) don't map cleanly to the
// single-row JSON schema.
var SEIZURE_FALLBACK = [
{ name: 'Lorazepam', phase: '1st benzo', dose_mg_per_kg: 0.1, max_mg: 4, unit: 'mg', route: 'IV / IO', label_suffix: ' <span style="color:#065f46;font-weight:600;">(IV preferred)</span>', notes: 'Slow push over 1-2 min.' },
{ name: 'Midazolam', phase: '1st benzo', dose_mg_per_kg: 0.2, max_mg: 10, unit: 'mg', route: 'IM / IN / buccal', notes: 'Use 5 mg/mL concentrate for IN; split between nares.' },
{ name: 'Diazepam', phase: '1st benzo', dose_mg_per_kg: 0.5, max_mg: 20, unit: 'mg', route: 'PR', notes: 'Only if no IV/IM/IN access.' },
{ name: 'Levetiracetam', phase: '2nd-line', dose_mg_per_kg: 60, max_mg: 4500, unit: 'mg', route: 'IV over 5-15 min', label_suffix: ' <span style="color:#065f46;font-weight:600;">(preferred)</span>', notes: 'Best tolerability. No ECG monitoring needed.' },
{ name: 'Fosphenytoin', phase: '2nd-line', dose_mg_per_kg: 20, max_mg: 1500, unit: 'mg PE', route: 'IV over 10 min', notes: 'Max rate 3 mg PE/kg/min. Monitor ECG, BP. Avoid if cardiac dysrhythmia.' },
{ name: 'Valproic acid', phase: '2nd-line', dose_mg_per_kg: 40, max_mg: 3000, unit: 'mg', route: 'IV over 10 min', notes: '<strong>Avoid &lt;2 yr, hepatic / mitochondrial disease, pregnancy.</strong>' },
{ name: 'Phenobarbital', phase: '2nd-line', dose_mg_per_kg: 20, max_mg: 1000, unit: 'mg', route: 'IV over 20 min', notes: 'Last-choice 2nd line. High risk of resp depression + hypotension.' }
];
function seizureDrugs() {
var s = window._DRUGS && window._DRUGS.sections && window._DRUGS.sections.seizure;
if (s && s.drugs && s.drugs.length) {
return s.drugs.map(function(dg) {
var fb = SEIZURE_FALLBACK.filter(function(x) { return x.name === dg.name; })[0] || {};
return Object.assign({}, fb, dg);
});
}
return SEIZURE_FALLBACK;
}
// Build the joined S.drugRow output for one phase ("1st benzo" / "2nd-line").
function seizureRowsByPhase(wt, phase) {
return seizureDrugs().filter(function(dg) { return dg.phase === phase; }).map(function(dg) {
var label = dg.name + (dg.label_suffix || '');
var unitArg = dg.unit && dg.unit !== 'mg' ? ' ' + dg.unit : undefined;
var dose = unitArg
? S.dStr(wt, dg.dose_mg_per_kg, dg.max_mg, unitArg)
: S.dStr(wt, dg.dose_mg_per_kg, dg.max_mg);
return S.drugRow(label, dose, dg.route, dg.notes);
}).join('');
}
function calcSeizure() {
var wt = parseFloat(document.getElementById('seizure-weight').value);
var resultDiv = document.getElementById('seizure-result');
if (!wt) { resultDiv.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter weight (kg).</p>'; return; }
var d10Low = Math.round(wt * 2 * 10) / 10;
var d10High = Math.round(wt * 5 * 10) / 10;
var html = '<div style="padding:10px 14px;border-radius:8px;background:#fee2e2;border:1.5px solid #ef4444;margin-bottom:14px;"><strong style="color:#dc2626;font-size:14px;"><i class="fas fa-bolt"></i> Status Epilepticus Pathway — ' + wt + ' kg</strong><div style="font-size:12px;color:var(--g600);margin-top:3px;">Time = 0 at seizure onset. Do not pause between steps for benzo to take effect — act on the clock.</div></div>';
// 0 min — STABILIZE
html += stepRow('0 min', '#3b82f6', 'Stabilize',
'<strong>ABCs</strong> — position, 100% O2 via NC / NRB, suction. <strong>IV or IO</strong> × 2. Continuous SpO2, ECG, BP. ' +
'<strong>POC glucose</strong> — if &lt;60 mg/dL: <strong>D10W ' + d10Low + '-' + d10High + ' mL IV</strong> (2-5 mL/kg). ' +
'<strong>Labs</strong>: CBC, CMP, Mg, Ca, Phos, VBG, lactate, AED levels, toxicology if unclear etiology. ' +
'<strong>Consider pyridoxine</strong> 100 mg IV in infants &lt;18 mo or INH ingestion. Temperature: treat hyperthermia aggressively.'
);
// 5 min — 1ST BENZO
html += stepRow('5 min', '#8b5cf6', '1st benzodiazepine (give once, pick by access)',
S.drugTable(seizureRowsByPhase(wt, '1st benzo'))
);
// 10 min — 2ND BENZO
html += stepRow('10 min', '#a855f7', '2nd benzodiazepine — if still seizing',
'Repeat same agent + dose <strong>once</strong>. Prepare 2nd-line now (don\'t wait to see if benzo works). If apnea/airway compromise: BVM, consider advanced airway.'
);
// 20 min — 2ND-LINE
html += stepRow('20 min', '#f59e0b', '2nd-line anti-epileptic — pick one (ESETT: equivalent efficacy)',
S.drugTable(seizureRowsByPhase(wt, '2nd-line'))
);
// 30 min — 2nd of 2nd-line (optional)
html += stepRow('30 min', '#ef4444', 'Still seizing? Consider 2nd agent from 2nd-line OR proceed to refractory',
'If the first 2nd-line drug failed, give a different 2nd-line agent <strong>OR</strong> move directly to refractory therapy. Activate ICU, prepare for intubation. Confirm etiology not reversible (electrolytes, glucose, fever, toxin).'
);
// 40 min — REFRACTORY
html += stepRow('40 min', '#dc2626', 'Refractory status — intubate + continuous infusion + continuous EEG',
S.drugTable(
S.drugRow('Midazolam <span style="color:#065f46;font-weight:600;">(1st-line infusion)</span>', 'Bolus ' + d(wt, 0.2, 10) + ' mg → gtt ' + d(wt, 0.05, 2) + '-' + d(wt, 0.5, 18) + ' mg/hr <span style="color:var(--g500);font-size:11px;">(bolus 0.2 mg/kg, gtt 0.05-0.5 mg/kg/hr)</span>', 'IV', 'Titrate to seizure control / burst suppression.') +
S.drugRow('Pentobarbital', 'Bolus ' + d(wt, 5, 200) + ' mg → gtt ' + d(wt, 1, null) + '-' + d(wt, 5, null) + ' mg/hr <span style="color:var(--g500);font-size:11px;">(bolus 5 mg/kg, gtt 1-5 mg/kg/hr)</span>', 'IV', 'Causes hypotension — often need pressors.') +
S.drugRow('Propofol', 'Bolus ' + d(wt, 2, 100) + ' mg → gtt ' + d(wt, 2, null) + '-' + d(wt, 5, null) + ' mg/hr <span style="color:var(--g500);font-size:11px;">(bolus 2 mg/kg, gtt 2-5 mg/kg/hr)</span>', 'IV', '<strong>PRIS risk in children</strong> — limit to &lt;4 mg/kg/hr and duration &lt;48 h.') +
S.drugRow('Ketamine', 'Bolus ' + d(wt, 2, 100) + ' mg → gtt ' + d(wt, 1, null) + '-' + d(wt, 5, null) + ' mg/hr <span style="color:var(--g500);font-size:11px;">(bolus 2 mg/kg, gtt 1-5 mg/kg/hr)</span>', 'IV', 'NMDA antagonist — rescue. Good BP profile; useful if refractory to GABAergics.')
) +
'<div style="margin-top:8px;font-size:12px;color:var(--g600);"><strong>Continuous EEG within 1 h</strong> — target electrographic seizure suppression × 24-48 h, then wean. Reassess etiology: CNS imaging, LP, expanded workup.</div>'
, true);
// Key points
html += '<div style="margin-top:16px;padding:10px 12px;background:#fef3c7;border-radius:6px;font-size:12px;color:#92400e;line-height:1.55;"><strong>Key points</strong><br>' +
'• Do <strong>NOT</strong> wait for benzo response before starting the 2nd-line — parallel preparation.<br>' +
'• Maximum <strong>2 benzo doses</strong> total (1 pre-hospital + 1 in ED, or 2 in ED).<br>' +
'• Respiratory depression is common — have BVM, airway equipment, naloxone, flumazenil accessible (but avoid flumazenil here).<br>' +
'• ESETT trial: levetiracetam = fosphenytoin = valproate for efficacy. Pick by tolerability / contraindications.<br>' +
'• Always reassess: ongoing seizure? Non-convulsive status? Pseudo-seizure? → continuous EEG if any doubt.</div>';
html += '<p style="font-size:11px;color:var(--g400);margin:8px 0 0;font-style:italic;">Based on AES Guidelines 2016 and ESETT (Kapur et al., NEJM 2019). Verify all doses against institutional protocols.</p>';
resultDiv.innerHTML = html;
}
export function init() {
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-seizure-calc' || e.target.closest('#btn-seizure-calc')) calcSeizure();
});
}

137
public/js/bedside/sepsis.js Normal file
View file

@ -0,0 +1,137 @@
// ============================================================
// bedside/sepsis.js
// SEPSIS — Phoenix criteria, febrile-infant rules, first-hour bundle.
// ============================================================
import { S } from './shared.js';
function showSepsis() {
var wt = parseFloat(document.getElementById('sepsis-weight').value);
var age = document.getElementById('sepsis-age').value;
var el = document.getElementById('sepsis-result');
var html = '';
var ageLbl = { neonate: 'Neonate (0-28 d)', infant: 'Young infant (29 d - 3 mo)', child: 'Older child / adolescent' }[age];
html += '<div style="padding:10px 14px;border-radius:8px;background:#fee2e2;border:1.5px solid #ef4444;margin-bottom:12px;"><strong style="color:#dc2626;">Sepsis approach — ' + ageLbl + (wt ? ', ' + wt + ' kg' : '') + '</strong></div>';
// Definition (Phoenix 2024)
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:0 0 6px;">Definition — Phoenix Sepsis Criteria (JAMA 2024)</h5>';
html += '<div style="padding:10px;background:var(--g50);border-radius:6px;font-size:12px;line-height:1.55;margin-bottom:12px;">';
html += '<strong>Sepsis</strong> = suspected or confirmed infection + Phoenix Score ≥2 (organ dysfunction across respiratory, cardiovascular, coagulation, neurological).<br>';
html += '<strong>Septic shock</strong> = sepsis + cardiovascular dysfunction (vasoactive support, or ↑lactate ≥5, or ↓MAP for age).<br>';
html += '<em style="color:var(--g500);">Previous SIRS-based criteria (Goldstein 2005) are now superseded. Still useful for quick clinical screening.</em>';
html += '</div>';
// Quick red flags
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:10px 0 6px;">Red flags (recognize early)</h5>';
html += '<div style="padding:10px;background:#fef2f2;border-radius:6px;font-size:12px;color:#991b1b;">Abnormal behavior / mentation • Fever + ill-appearance • Tachycardia out of proportion to fever • Prolonged cap refill (&gt;3 s) • Cold/mottled extremities • Weak pulses or wide pulse pressure (&quot;warm shock&quot;) • Hypotension is a <strong>LATE</strong> sign in children • Any immune compromise / indwelling line.</div>';
// Age-specific approach
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 6px;">Evaluation &amp; empiric therapy — ' + ageLbl + '</h5>';
if (age === 'neonate') {
html += '<div style="padding:10px;background:var(--g50);border-radius:6px;font-size:12px;margin-bottom:10px;">';
html += '<strong>Workup (full sepsis eval):</strong> CBC + diff, CRP, blood culture, UA + urine culture (cath), <strong>lumbar puncture</strong> (CSF + HSV PCR), CXR if respiratory sx. Consider procalcitonin. Rapid viral panel if available.<br>';
html += '<strong>Early-onset (&lt;72 h):</strong> GBS, E. coli, Listeria. <strong>Late-onset (&gt;72 h):</strong> coag-neg staph, S. aureus, gram-negs, Candida.</div>';
html += S.drugTable(
S.drugRow('Ampicillin', wt ? S.dStr(wt, 100, 2000) : '50-100 mg/kg/dose', 'IV', 'q8-12h depending on age/weight. Covers GBS, Listeria, Enterococcus.') +
S.drugRow('Gentamicin', wt ? S.dStr(wt, 4, 120) : '4-5 mg/kg/dose', 'IV', 'q24-48h. Monitor levels. Peak 5-12, trough &lt;1.') +
S.drugRow('Cefotaxime (add)', wt ? S.dStr(wt, 50, 2000) : '50 mg/kg/dose', 'IV', 'If meningitis suspected or gram-neg concern. Ceftriaxone avoided in hyperbilirubinemia.') +
S.drugRow('Acyclovir', wt ? S.dStr(wt, 20, 1200) : '20 mg/kg/dose', 'IV q8h', 'If HSV risk: maternal genital lesions, vesicles, seizures, CSF pleocytosis, hypothermia, hepatitis.')
);
} else if (age === 'infant') {
html += '<div style="padding:10px;background:var(--g50);border-radius:6px;font-size:12px;margin-bottom:10px;">';
html += '<strong>Workup:</strong> Use validated rules — PECARN, Aronson, Rochester, Step-by-Step (see below). Most warrant CBC + ANC, procalcitonin and/or CRP, blood culture, UA + urine culture. Many warrant LP + admission + empiric abx.<br>';
html += '<strong>Coverage:</strong> GBS, E. coli, Listeria (up to ~6 wk), S. pneumoniae, N. meningitidis, H. influenzae, Salmonella.</div>';
// Febrile-infant rules — collapsible details
html += '<details style="margin-bottom:10px;border:1px solid var(--g200);border-radius:8px;padding:8px 10px;background:white;"><summary style="cursor:pointer;font-weight:600;font-size:13px;color:var(--blue-dark);"><i class="fas fa-list-check"></i> Validated febrile-infant rules (click to expand)</summary>';
html += '<div style="margin-top:10px;font-size:12px;line-height:1.55;">';
// PECARN 2019
html += '<div style="padding:10px 12px;background:#eff6ff;border-radius:6px;margin-bottom:8px;border-left:4px solid #3b82f6;">';
html += '<div style="font-weight:700;color:#1e40af;margin-bottom:4px;">PECARN febrile infant rule (Kuppermann et al., JAMA Pediatrics 2019)</div>';
html += '<strong>Age:</strong> 29-60 days, well-appearing, febrile. <strong>Low-risk if ALL:</strong><br>';
html += '&nbsp;&bull; Normal UA (no LE, no nitrite, &lt;5 WBC/hpf)<br>';
html += '&nbsp;&bull; ANC ≤4,090 /&mu;L<br>';
html += '&nbsp;&bull; Procalcitonin ≤0.5 ng/mL (if procal unavailable: use CRP &le;20 mg/L as proxy)<br>';
html += '<strong>Low-risk</strong> → may defer LP/abx, observe, close follow-up. <strong>Not low-risk</strong> → full workup + empiric abx.';
html += '</div>';
// Aronson 2019
html += '<div style="padding:10px 12px;background:#f0fdf4;border-radius:6px;margin-bottom:8px;border-left:4px solid #16a34a;">';
html += '<div style="font-weight:700;color:#166534;margin-bottom:4px;">Aronson rule (Aronson et al., PEDIATRICS 2019)</div>';
html += '<strong>Age:</strong> 8-60 days, febrile ≥38°C. Point-based score for invasive bacterial infection (IBI):<br>';
html += '&nbsp;&bull; Age 22-28 days: <strong>+1</strong><br>';
html += '&nbsp;&bull; Max temperature ≥38.5°C: <strong>+2</strong><br>';
html += '&nbsp;&bull; ANC ≥5,185 /&mu;L: <strong>+2</strong><br>';
html += '&nbsp;&bull; Abnormal UA: <strong>+3</strong><br>';
html += '<strong>Score 0-1</strong> → low risk. <strong>≥2</strong> → consider full workup + abx. Higher scores → higher IBI risk.';
html += '</div>';
// Rochester
html += '<div style="padding:10px 12px;background:#fffbeb;border-radius:6px;margin-bottom:8px;border-left:4px solid #f59e0b;">';
html += '<div style="font-weight:700;color:#92400e;margin-bottom:4px;">Rochester criteria (Jaskiewicz et al., Pediatrics 1994)</div>';
html += '<strong>Age:</strong> 0-60 days, febrile. <strong>Low-risk if ALL:</strong><br>';
html += '&nbsp;&bull; Previously healthy (term, no perinatal complications, no prior abx/hospitalization)<br>';
html += '&nbsp;&bull; Well-appearing, no focal infection (not skin/bone/joint/soft tissue)<br>';
html += '&nbsp;&bull; WBC 5,000-15,000 /&mu;L and absolute band count ≤1,500 /&mu;L<br>';
html += '&nbsp;&bull; UA ≤10 WBC/hpf<br>';
html += '&nbsp;&bull; If diarrhea: stool ≤5 WBC/hpf<br>';
html += '<strong>Low-risk</strong> → observation without abx possible (NPV &gt;98%). Does not include procal.';
html += '</div>';
// Step-by-Step
html += '<div style="padding:10px 12px;background:#fdf4ff;border-radius:6px;margin-bottom:4px;border-left:4px solid #a855f7;">';
html += '<div style="font-weight:700;color:#6b21a8;margin-bottom:4px;">Step-by-Step (Gomez et al., Pediatrics 2016 — European)</div>';
html += '<strong>Age:</strong> ≤90 days, febrile. Sequential triage; stop at first positive:<br>';
html += '&nbsp;<strong>1.</strong> Ill-appearing? → <em>high risk</em><br>';
html += '&nbsp;<strong>2.</strong> Age ≤21 days? → <em>high risk</em><br>';
html += '&nbsp;<strong>3.</strong> Leukocyturia (UA abnormal)? → <em>high risk</em><br>';
html += '&nbsp;<strong>4.</strong> Procalcitonin ≥0.5 ng/mL? → <em>high risk</em><br>';
html += '&nbsp;<strong>5.</strong> CRP &gt;20 mg/L AND/OR ANC &gt;10,000 /&mu;L? → <em>intermediate</em><br>';
html += '&nbsp;<strong>6.</strong> Otherwise → <em>low risk</em>';
html += '<div style="margin-top:4px;color:var(--g500);">Highest sensitivity for IBI among these rules when procal available.</div>';
html += '</div>';
html += '<div style="margin-top:8px;padding:8px 10px;background:var(--g50);border-radius:6px;font-size:11px;color:var(--g600);"><strong>Practical note:</strong> AAP 2021 Clinical Practice Guideline on the well-appearing febrile infant 8-60 days synthesizes these — ages 8-21 d full workup regardless, 22-28 d inflammatory markers guide LP, 29-60 d UA + markers guide LP/abx. Defer to your local institutional pathway if available.</div>';
html += '</div></details>';
html += S.drugTable(
S.drugRow('Ceftriaxone', wt ? S.dStr(wt, 75, 2000) : '50-75 mg/kg/dose', 'IV / IM', 'q24h (or 100 mg/kg/day divided q12h for meningitis). <strong>Avoid &lt;28 days</strong> if hyperbilirubinemia.') +
S.drugRow('Ampicillin', wt ? S.dStr(wt, 100, 2000) : '50-100 mg/kg/dose', 'IV', 'If &lt;6 wks: add for Listeria coverage.') +
S.drugRow('Vancomycin', wt ? S.dStr(wt, 15, 1000) : '15 mg/kg/dose', 'IV', 'If severe / MRSA risk / meningitis. Target trough 15-20.') +
S.drugRow('Acyclovir', wt ? S.dStr(wt, 20, 1200) : '20 mg/kg/dose', 'IV q8h', '&lt;6 weeks with suspicion of HSV.')
);
} else {
html += '<div style="padding:10px;background:var(--g50);border-radius:6px;font-size:12px;margin-bottom:10px;">';
html += '<strong>Recognition:</strong> Phoenix score or clinical concern + suspected infection. <strong>Workup:</strong> CBC, CRP, procalcitonin, blood cx (+ site-specific cx), lactate, blood gas, glucose, electrolytes, coags, LP if CNS concern. Source-directed imaging.<br>';
html += '<strong>Common sources:</strong> respiratory, UTI, CNS, skin/soft tissue, intra-abdominal, bone/joint, indwelling device.</div>';
html += S.drugTable(
S.drugRow('Ceftriaxone', wt ? S.dStr(wt, 50, 2000) : '50 mg/kg/dose', 'IV', 'q24h (or 100 mg/kg/day divided for meningitis).') +
S.drugRow('Vancomycin', wt ? S.dStr(wt, 15, 1000) : '15 mg/kg/dose', 'IV', 'q6h. If severe, indwelling line, or MRSA prevalence &gt;10%.') +
S.drugRow('Piperacillin-tazobactam', wt ? S.dStr(wt, 100, 4500) : '80-100 mg/kg/dose', 'IV', 'If intra-abdominal / neutropenic.') +
S.drugRow('Clindamycin', wt ? S.dStr(wt, 10, 900) : '10-13 mg/kg/dose', 'IV', 'Adjunct for toxic shock syndrome (toxin suppression).') +
S.drugRow('Acyclovir', wt ? S.dStr(wt, 20, 1200) : '10-20 mg/kg/dose', 'IV q8h', 'If HSV CNS concern.')
);
}
// First-hour bundle
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 6px;">First-hour bundle (SSC Peds 2020)</h5>';
html += S.stepBox('#3b82f6', '0-5 min — Recognize', 'Screen for sepsis; sepsis huddle / activation. Assess ABCs. O2 to SpO2 &gt;94%. Warm patient.');
html += S.stepBox('#8b5cf6', '5-15 min — Access &amp; labs', 'Two IVs or IO. Draw: blood cx (ideally before abx), lactate, CBC, CMP, coags, blood gas, glucose. UA + culture. Source-specific cultures.');
html += S.stepBox('#f59e0b', '15-30 min — Fluids', (wt ? 'NS or LR <strong>' + S.d(wt, 20) + ' mL</strong> bolus over 5-10 min (20 mL/kg). ' : 'NS/LR 10-20 mL/kg bolus over 5-10 min. ') + 'Reassess after each bolus: HR, perfusion, lung sounds, liver size. Repeat up to 40-60 mL/kg; stop if crackles or hepatomegaly.');
html += S.stepBox('#10b981', '30-60 min — Antibiotics + reassess', 'Broad-spectrum empiric abx within 1 hour (ideally 3 hr in sepsis, &le;1 hr in septic shock). Recheck lactate, perfusion.');
html += S.stepBox('#ef4444', '&gt;60 min — Fluid-refractory shock', 'Start vasoactive (<strong>epinephrine 0.05-0.3 mcg/kg/min</strong> for cold shock or <strong>norepinephrine 0.05-0.3 mcg/kg/min</strong> for warm shock). Consider central/IO access. Stress-dose hydrocortisone ' + (wt ? '<strong>' + S.d(wt, 2, 100) + ' mg</strong> IV <span style="color:var(--g500);font-size:11px;">(2 mg/kg, max 100 mg)</span>' : '2 mg/kg IV (max 100 mg)') + ' if catecholamine-resistant. Consider ICU transfer.');
// Key targets
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 6px;">Resuscitation targets</h5>';
html += '<div style="padding:10px;background:var(--g50);border-radius:6px;font-size:12px;">Normal mentation • Cap refill ≤2 s • Warm extremities • Strong peripheral pulses • UOP ≥1 mL/kg/hr • MAP ≥5th %ile for age (&gt;65 mmHg adolescent) • SpO2 ≥94% • Lactate trending down.</div>';
html += S.ref('Phoenix Sepsis Criteria (Schlapbach et al., JAMA 2024). Surviving Sepsis Campaign Pediatric 2020. AAP pediatric sepsis guidance.');
el.innerHTML = html;
}
export function init() {
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-sepsis-show' || e.target.closest('#btn-sepsis-show')) showSepsis();
});
}

View file

@ -0,0 +1,44 @@
// ============================================================
// bedside/shared.js
// Shared string-building helpers used by every Bedside section.
// Pure functions with no side effects. Exports `S`; also sets
// window._EM = S for back-compat with any stray lookups.
// ============================================================
export const S = {
d: function(wt, perKg, max) { var v = Math.round(wt * perKg * 100) / 100; return max ? Math.min(v, max) : v; },
// Dose string — always shows the per-kg used (and max when capped), so the
// prescriber can verify the math without opening a drug ref.
// S.dStr(30, 0.1, 4) → "3 mg (0.1 mg/kg, max 4 mg)"
// S.dStr(60, 0.1, 4) → "4 mg (0.1 mg/kg, max 4 mg · capped)"
// S.dStr(20, 2, 150, ' mcg') → "40 mcg (2 mcg/kg, max 150 mcg)"
dStr: function(wt, perKg, max, unit) {
unit = unit || ' mg';
var uTrim = unit.trim() || 'mg';
var raw = wt * perKg;
var v = Math.round(raw * 100) / 100;
var capped = max != null && v > max;
var val = capped ? max : v;
var perKgStr = '(' + perKg + ' ' + uTrim + '/kg';
if (max != null) perKgStr += ', max ' + max + ' ' + uTrim;
if (capped) perKgStr += ' · capped';
perKgStr += ')';
return val + unit + ' <span style="color:var(--g500);font-size:11px;font-weight:400;">' + perKgStr + '</span>';
},
drugTable: function(rows) {
return '<div style="overflow-x:auto;-webkit-overflow-scrolling:touch;margin:8px 0;"><table style="width:100%;min-width:500px;border-collapse:collapse;font-size:13px;"><thead><tr style="background:var(--g100);"><th style="text-align:left;padding:6px 8px;">Drug</th><th style="text-align:left;padding:6px 8px;">Dose</th><th style="text-align:left;padding:6px 8px;">Route</th><th style="text-align:left;padding:6px 8px;">Notes</th></tr></thead><tbody>' + rows + '</tbody></table></div>';
},
drugRow: function(name, dose, route, notes) {
return '<tr><td style="font-weight:600;padding:5px 8px;border-bottom:1px solid var(--g100);">' + name + '</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">' + dose + '</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">' + route + '</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);font-size:12px;color:var(--g500);">' + (notes || '') + '</td></tr>';
},
stepBox: function(color, title, body) {
return '<div style="border-left:4px solid ' + color + ';background:' + color + '10;padding:10px 14px;border-radius:6px;margin-bottom:10px;">' +
'<div style="font-weight:700;color:' + color + ';font-size:13px;margin-bottom:4px;">' + title + '</div>' +
'<div style="font-size:13px;color:var(--g700);line-height:1.5;">' + body + '</div></div>';
},
arrow: '<div style="text-align:center;color:var(--g400);font-size:16px;margin:-4px 0 6px;">↓</div>',
badge: function(label, color) { return '<span style="display:inline-block;padding:3px 10px;border-radius:6px;background:' + color + '20;color:' + color + ';font-weight:700;font-size:12px;">' + label + '</span>'; },
ref: function(text) { return '<p style="font-size:11px;color:var(--g400);margin:8px 0 0;font-style:italic;">' + text + '</p>'; }
};
if (typeof window !== 'undefined') window._EM = S;

View file

@ -0,0 +1,18 @@
// ============================================================
// bedside/sub-nav.js
// EMERGENCIES SUB-NAV — data-em pill switching.
// ============================================================
export function init() {
document.addEventListener('click', function(e) {
var pill = e.target.closest('[data-em]');
if (!pill) return;
document.querySelectorAll('[data-em]').forEach(function(p) { p.classList.remove('active'); });
pill.classList.add('active');
var sections = ['neonatal','airway','cardiac','respiratory','ventilation','seizure','sepsis','anaphylaxis','sedation','agitation','antiemetics','antimicrobials','burns','toxicology','trauma'];
sections.forEach(function(s) {
var el = document.getElementById('em-' + s);
if (el) el.style.display = pill.dataset.em === s ? '' : 'none';
});
});
}

View file

@ -0,0 +1,120 @@
// ============================================================
// bedside/toxicology.js
// TOXICOLOGY.
// ============================================================
import { S } from './shared.js';
function lookupTox() {
var wt = parseFloat(document.getElementById('tox-weight').value);
var topic = document.getElementById('tox-topic').value;
var el = document.getElementById('tox-result');
var html = '';
if (topic === 'approach') {
html += S.stepBox('#3b82f6', '1. Stabilize (ABCDE)', 'Airway, breathing, circulation, disability (glucose, pupils, GCS), exposure. Naloxone if depressed LOC + resp depression. Dextrose if hypoglycemic. Thiamine in select cases.');
html += S.stepBox('#8b5cf6', '2. Identify toxidrome', '<strong>Sympathomimetic:</strong> HTN, tachy, mydriasis, diaphoresis (cocaine, meth). <strong>Anticholinergic:</strong> hot/dry, mydriasis, tachy, delirium (antihistamines, TCAs). <strong>Cholinergic:</strong> SLUDGE-M, miosis (organophosphates). <strong>Opioid:</strong> miosis, resp depression, ↓LOC. <strong>Sedative-hypnotic:</strong> ↓LOC, normal/low vitals.');
html += S.stepBox('#f59e0b', '3. Decontamination', '<strong>Activated charcoal</strong> 1 g/kg PO (max 50 g): within 1h of ingestion, intact airway, ingestion adsorbed (not Li, metals, alcohols). Avoid caustics/hydrocarbons. <strong>Whole bowel irrigation:</strong> PEG for metals/iron/lithium/sustained release. <strong>Gastric lavage:</strong> rarely indicated. <strong>Ipecac:</strong> no longer recommended.');
html += S.stepBox('#10b981', '4. Enhance elimination', '<strong>Urinary alkalinization</strong> (salicylates, phenobarbital). <strong>Hemodialysis</strong> (see ISTUMBLE). <strong>Lipid emulsion</strong> for lipid-soluble drug toxicity (LA, CCB, TCA).');
html += S.stepBox('#ef4444', '5. Antidotes', 'See topic list. <strong>Contact Poison Center</strong> (US: 1-800-222-1222) early for any significant exposure.');
} else if (topic === 'acetaminophen') {
html += '<div style="padding:10px 14px;border-radius:8px;background:var(--blue-light);border:1.5px solid var(--blue);margin-bottom:10px;"><strong>Acetaminophen overdose</strong></div>';
html += '<p style="font-size:13px;">Acute toxic dose: &gt;150 mg/kg (or 7.5 g total) single ingestion. Hepatotoxic after 24h; AST/ALT peak day 3-4.</p>';
html += '<p style="font-size:13px;"><strong>Diagnosis:</strong> Draw level at 4h post-ingestion (or on arrival if &gt;4h). Plot on <strong>Rumack-Matthew nomogram</strong> (treatment line at 150 mcg/mL at 4h).</p>';
html += S.drugTable(
S.drugRow('N-acetylcysteine (NAC) — IV', '150 mg/kg over 1h → 50 mg/kg over 4h → 100 mg/kg over 16h (21h total)', 'IV', 'Total 300 mg/kg. Most effective if started &lt;8h post-ingestion.') +
S.drugRow('N-acetylcysteine — PO', '140 mg/kg load, then 70 mg/kg q4h × 17 doses', 'PO', 'Alternative to IV. Unpleasant taste — often with juice.') +
S.drugRow('Activated charcoal', '1 g/kg (max 50 g)', 'PO', 'If within 1-2h of ingestion and airway protected.')
);
if (wt) html += '<div style="padding:10px;background:var(--g50);border-radius:6px;font-size:12px;"><strong>For ' + wt + ' kg:</strong> IV loading = ' + S.d(wt, 150) + ' mg over 1h <span style="color:var(--g500);font-size:11px;">(150 mg/kg)</span>; 2nd bag = ' + S.d(wt, 50) + ' mg over 4h <span style="color:var(--g500);font-size:11px;">(50 mg/kg)</span>; 3rd bag = ' + S.d(wt, 100) + ' mg over 16h <span style="color:var(--g500);font-size:11px;">(100 mg/kg)</span>. <em>Total 300 mg/kg.</em></div>';
} else if (topic === 'opioids') {
html += '<div style="padding:10px 14px;border-radius:8px;background:var(--blue-light);border:1.5px solid var(--blue);margin-bottom:10px;"><strong>Opioid overdose</strong></div>';
html += '<p style="font-size:13px;">Triad: <strong>miosis + respiratory depression + ↓ LOC</strong>. Fentanyl / synthetics may require repeated/higher doses.</p>';
html += S.drugTable(
S.drugRow('Naloxone (initial)', '0.01-0.1 mg/kg' + (wt ? ' = ' + S.d(wt, 0.01) + '-' + S.d(wt, 0.1) + ' mg' : ''), 'IV / IM / IN / IO', 'Start low if chronic opioid use (avoid withdrawal). Titrate to respiratory effort, not consciousness.') +
S.drugRow('Naloxone (full reversal)', '2 mg IV/IM/IN', 'IV / IM / IN', 'If no chronic use and severe resp depression. Repeat q2-3 min.') +
S.drugRow('Naloxone infusion', '2/3 of effective bolus per hour', 'IV', 'For long-acting opioids (methadone, fentanyl patch, sustained release)')
);
html += '<div style="padding:10px;background:#fef3c7;border-radius:6px;font-size:12px;color:#92400e;margin-top:10px;">Observe ≥4h after last dose; longer for long-acting agents. Duration of naloxone (30-90 min) is shorter than most opioids — <strong>re-sedation is common</strong>.</div>';
} else if (topic === 'iron') {
html += '<div style="padding:10px 14px;border-radius:8px;background:var(--blue-light);border:1.5px solid var(--blue);margin-bottom:10px;"><strong>Iron overdose</strong></div>';
html += '<p style="font-size:13px;">Toxic dose: elemental Fe ≥20 mg/kg. Severe ≥60 mg/kg. <strong>Charcoal does NOT bind iron.</strong></p>';
html += '<p style="font-size:13px;"><strong>Stages:</strong> 1) GI (0-6h: vomiting, bloody diarrhea); 2) Latent (6-24h: deceptive improvement); 3) Shock/metabolic acidosis (12-24h); 4) Hepatotoxicity (2-5d); 5) Gastric scarring (2-8 wk).</p>';
html += S.drugTable(
S.drugRow('Whole bowel irrigation', 'PEG-ES 25-40 mL/kg/hr', 'NG', 'For radiopaque pills on KUB or ingested sustained-release iron.') +
S.drugRow('Deferoxamine', '15 mg/kg/hr IV infusion', 'IV', 'Max 6-8 g/day. Indications: shock, metabolic acidosis, Fe level &gt;500 mcg/dL, or severe symptoms. Urine turns "vin rosé" color.')
);
html += '<div style="padding:10px;background:#fef3c7;border-radius:6px;font-size:12px;color:#92400e;margin-top:10px;">Iron level at 4-6h. &lt;350 usually asymptomatic; 350-500 mild; &gt;500 severe. Consider KUB for radiopaque pills.</div>';
} else if (topic === 'tca') {
html += '<div style="padding:10px 14px;border-radius:8px;background:var(--blue-light);border:1.5px solid var(--blue);margin-bottom:10px;"><strong>TCA overdose</strong></div>';
html += '<p style="font-size:13px;">Anticholinergic + Na-channel blockade. <strong>Risks:</strong> seizure, VT/VF, hypotension, coma. ECG: QRS &gt;100 ms or R in aVR &gt;3 mm predicts toxicity.</p>';
html += S.drugTable(
S.drugRow('Sodium bicarbonate 8.4%', '1-2 mEq/kg IV bolus → infusion', 'IV', 'Goal pH 7.45-7.55 and narrowing QRS. Repeat bolus for QRS widening, hypotension, or arrhythmia.') +
S.drugRow('Mg sulfate', S.dStr(wt || 30, 50, 2000, ' mg'), 'IV over 10 min', 'For torsades.') +
S.drugRow('IV lipid emulsion 20%', '1.5 mL/kg bolus → 0.25 mL/kg/min × 30-60 min', 'IV', 'If refractory shock/arrest. Consult toxicology.')
);
html += '<div style="padding:10px;background:#fef3c7;border-radius:6px;font-size:12px;color:#92400e;margin-top:10px;"><strong>Avoid</strong> Class IA/IC antiarrhythmics, beta-blockers, physostigmine (risk of asystole with TCAs).</div>';
} else if (topic === 'bbccb') {
html += '<div style="padding:10px 14px;border-radius:8px;background:var(--blue-light);border:1.5px solid var(--blue);margin-bottom:10px;"><strong>β-blocker / Calcium-channel blocker overdose</strong></div>';
html += '<p style="font-size:13px;">Bradycardia + hypotension. CCBs (esp. verapamil, diltiazem, amlodipine) can be lethal in small pediatric doses.</p>';
html += S.drugTable(
S.drugRow('IV fluids', '20 mL/kg NS bolus', 'IV', 'Cautious — avoid volume overload') +
S.drugRow('Calcium chloride 10% or gluconate', 'CaCl 20 mg/kg / Ca-glu 60 mg/kg', 'IV', 'First-line for CCB. Repeat q15-20 min.') +
S.drugRow('Glucagon', '50 mcg/kg IV bolus → 50-150 mcg/kg/hr', 'IV', 'β-blocker antidote. GI side effects common.') +
S.drugRow('High-dose insulin (HIE)', 'Regular insulin 1 U/kg bolus → 0.5-2 U/kg/hr + D25% infusion', 'IV', 'Hyperinsulinemic euglycemia therapy. Monitor K+ and glucose closely.') +
S.drugRow('Vasopressors', 'Epi / norepi infusion', 'IV', 'Titrate to MAP.') +
S.drugRow('Lipid emulsion 20%', '1.5 mL/kg → 0.25 mL/kg/min', 'IV', 'For lipid-soluble CCBs (verapamil) if refractory.') +
S.drugRow('Methylene blue / ECMO', '—', '—', 'Rescue therapy — toxicology/ICU consult.')
);
} else if (topic === 'benzo') {
html += '<div style="padding:10px 14px;border-radius:8px;background:var(--blue-light);border:1.5px solid var(--blue);margin-bottom:10px;"><strong>Benzodiazepine overdose</strong></div>';
html += '<p style="font-size:13px;">Sedation + resp depression. Usually supportive — intubation rarely needed alone. <strong>Flumazenil caution.</strong></p>';
html += S.drugTable(
S.drugRow('Flumazenil', '0.01 mg/kg (max 0.2 mg) IV, may repeat q1 min to max 1 mg', 'IV', 'Only for iatrogenic reversal in a benzo-naive patient with no TCAs, seizure disorder, or chronic benzo use. <strong>Can precipitate seizures.</strong>')
);
html += '<div style="padding:10px;background:#fef3c7;border-radius:6px;font-size:12px;color:#92400e;margin-top:10px;">In real-world ingestion, <strong>supportive care (airway, monitoring)</strong> is usually safer than flumazenil.</div>';
} else if (topic === 'organo') {
html += '<div style="padding:10px 14px;border-radius:8px;background:var(--blue-light);border:1.5px solid var(--blue);margin-bottom:10px;"><strong>Organophosphate / carbamate poisoning</strong></div>';
html += '<p style="font-size:13px;">Cholinergic toxidrome: <strong>SLUDGE-M</strong> (salivation, lacrimation, urination, defecation, GI distress, emesis, miosis) + muscle fasciculation, bradycardia, bronchorrhea. Decontamination critical (PPE for providers).</p>';
html += S.drugTable(
S.drugRow('Atropine', '0.05 mg/kg IV, double q3-5 min until bronchial secretions dry', 'IV', 'Endpoint = dry lungs, not dry mouth or heart rate. Can require huge doses.') +
S.drugRow('Pralidoxime (2-PAM)', '25-50 mg/kg IV over 30 min (max 2 g) → 10-20 mg/kg/hr', 'IV', 'Only for organophosphates (not carbamates). Regenerates acetylcholinesterase.') +
S.drugRow('Diazepam / midazolam', 'Standard seizure doses', 'IV', 'For seizures or severe fasciculations.')
);
} else if (topic === 'salicylate') {
html += '<div style="padding:10px 14px;border-radius:8px;background:var(--blue-light);border:1.5px solid var(--blue);margin-bottom:10px;"><strong>Salicylate overdose</strong></div>';
html += '<p style="font-size:13px;">Mixed respiratory alkalosis + anion gap metabolic acidosis. Tinnitus, tachypnea, diaphoresis, hyperthermia. Severe: CNS depression, seizures, pulmonary edema, cerebral edema.</p>';
html += S.drugTable(
S.drugRow('Fluids', 'Aggressive resuscitation', 'IV', 'Dehydration common. Avoid fluid overload (risk of pulmonary edema).') +
S.drugRow('Sodium bicarb', '1-2 mEq/kg IV bolus → drip', 'IV', 'Urinary alkalinization (goal urine pH &gt;7.5). Prevents CNS entry.') +
S.drugRow('Glucose', 'Maintain euglycemia even if BG normal', 'IV', 'CNS hypoglycemia despite normal serum glucose.') +
S.drugRow('Hemodialysis', '—', '—', 'For severe: altered MS, pulmonary edema, renal failure, refractory acidosis, or level &gt;100 mg/dL acute / &gt;60 chronic.')
);
} else if (topic === 'etoh') {
html += '<div style="padding:10px 14px;border-radius:8px;background:var(--blue-light);border:1.5px solid var(--blue);margin-bottom:10px;"><strong>Toxic alcohols (methanol / ethylene glycol)</strong></div>';
html += '<p style="font-size:13px;">Anion gap metabolic acidosis + osmolar gap. Methanol → visual changes, blindness. Ethylene glycol → calcium oxalate crystals in urine, renal failure.</p>';
html += S.drugTable(
S.drugRow('Fomepizole', '15 mg/kg IV load → 10 mg/kg q12h × 4 → 15 mg/kg q12h', 'IV', 'First-line. Inhibits alcohol dehydrogenase.') +
S.drugRow('Ethanol (alternative)', 'Load 600 mg/kg → maintain level 100-150 mg/dL', 'IV / PO', 'If fomepizole unavailable. Monitor closely.') +
S.drugRow('Hemodialysis', '—', '—', 'For severe acidosis, end-organ damage, or high levels.') +
S.drugRow('Folate / thiamine / pyridoxine', 'Standard doses', 'IV', 'Methanol: folate. EG: thiamine + pyridoxine (divert to non-toxic metabolites).')
);
} else if (topic === 'dialyzable') {
html += '<div style="padding:10px 14px;border-radius:8px;background:var(--blue-light);border:1.5px solid var(--blue);margin-bottom:10px;"><strong>Dialyzable drugs (ISTUMBLE)</strong></div>';
html += '<p style="font-size:13px;">Commonly dialyzable in overdose:</p>';
html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:6px;font-size:13px;">';
[['I','Isopropyl alcohol, INH'],['S','Salicylates'],['T','Theophylline, Toxic alcohols'],['U','Uremia-related drugs'],['M','Methanol, Metformin, Methotrexate'],['B','Barbiturates (long-acting)'],['L','Lithium'],['E','Ethylene glycol']].forEach(function(x) {
html += '<div style="padding:8px 10px;background:var(--g50);border-radius:6px;"><strong>' + x[0] + ':</strong> ' + x[1] + '</div>';
});
html += '</div>';
html += '<div style="padding:10px;background:#fef3c7;border-radius:6px;font-size:12px;color:#92400e;margin-top:10px;"><strong>Poor candidates for HD:</strong> Large Vd (TCAs, digoxin, BBs), highly protein-bound (benzos, CCBs), lipid-soluble (opioids, phenothiazines). EXTRIP recommendations are evidence-based — consult toxicology.</div>';
}
html += S.ref('Call Poison Control early: 1-800-222-1222 (US). Refs: Goldfrank\'s Toxicologic Emergencies, EXTRIP workgroup.');
el.innerHTML = html;
}
export function init() {
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-tox-lookup' || e.target.closest('#btn-tox-lookup')) lookupTox();
});
}

View file

@ -0,0 +1,44 @@
// ============================================================
// bedside/trauma.js
// TRAUMA — primary survey, MTP, c-spine, shock signs.
// ============================================================
import { S } from './shared.js';
function showTrauma() {
var wt = parseFloat(document.getElementById('trauma-weight').value);
var el = document.getElementById('trauma-result');
var html = '';
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:0 0 6px;">Primary Survey — ABCDE</h5>';
html += S.stepBox('#ef4444', 'A — Airway + c-spine', 'Maintain airway (jaw thrust, suction). <strong>Manual inline stabilization</strong>; apply collar. Intubate if GCS ≤8, inadequate airway, or impending compromise. Avoid succinylcholine if burn/crush/prolonged immobilization.');
html += S.stepBox('#f59e0b', 'B — Breathing', 'SpO2, bilateral breath sounds, chest wall integrity. Identify tension PTX (needle decompression), open PTX (3-sided dressing), flail chest, massive hemothorax.');
html += S.stepBox('#10b981', 'C — Circulation', 'Two large-bore IVs / IO. Control hemorrhage (direct pressure, tourniquet, pelvic binder). ' + (wt ? 'NS or LR 20 mL/kg = ' + S.d(wt, 20) + ' mL bolus.' : 'NS/LR 20 mL/kg bolus.') + ' Consider blood after 40-60 mL/kg crystalloid or in Class III shock.');
html += S.stepBox('#3b82f6', 'D — Disability', 'GCS, pupils, glucose, gross motor. Consider ↑ICP (head up 30°, mannitol 0.5-1 g/kg or hypertonic saline 3% 3-5 mL/kg).');
html += S.stepBox('#8b5cf6', 'E — Exposure + environment', 'Fully expose; prevent hypothermia (warm blankets, fluids).');
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:16px 0 6px;">Massive Transfusion Protocol (MTP)</h5>';
html += S.drugTable(
S.drugRow('Ratio', '1:1:1 (pRBC : FFP : platelets)', '—', 'Activate early if ≥40 mL/kg transfused or ongoing hemorrhage. Avoid excessive crystalloid.') +
S.drugRow('Tranexamic acid (TXA)', '15 mg/kg IV (max 1 g) over 10 min → 2 mg/kg/hr × 8h', 'IV', 'Within 3 hr of injury. CRASH-2 / MATIC.') +
S.drugRow('Calcium', '20 mg/kg CaCl or 60 mg/kg Ca-gluconate IV per unit citrated blood', 'IV', 'Citrated blood chelates calcium.')
);
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:16px 0 6px;">C-spine clearance (NEXUS / CCR)</h5>';
html += '<div style="padding:10px;background:var(--g50);border-radius:6px;font-size:12px;">Pediatric c-spine decision tools are imperfect. <strong>Imaging if any:</strong> focal neurologic deficit, altered mental status, neck pain / tenderness, torticollis, substantial torso injury, high-risk mechanism (diving, MVC &gt;55 mph, fall &gt;10 ft). Plain films + CT if positive or equivocal.</div>';
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:16px 0 6px;">Pediatric shock — signs</h5>';
html += '<div style="padding:10px;background:#fee2e2;border-radius:6px;font-size:12px;color:#991b1b;">Children compensate well — <strong>hypotension is a late finding</strong>. Early signs: tachycardia, cool extremities, weak peripheral pulses, prolonged cap refill (&gt;3 sec), narrowed pulse pressure, altered mentation. Minimum SBP = 70 + (2 × age in years) for ages 1-10.</div>';
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:16px 0 6px;">Secondary survey — AMPLE + head-to-toe</h5>';
html += '<div style="padding:10px;background:var(--g50);border-radius:6px;font-size:12px;"><strong>AMPLE:</strong> Allergies, Medications, Past history, Last meal, Events of injury. Head-to-toe exam; log-roll for back; digital rectal; neurovascular checks of all extremities.</div>';
html += S.ref('ATLS 10th ed; PALS 2020; PECARN c-spine rule; CRASH-2 trial (TXA).');
el.innerHTML = html;
}
export function init() {
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-trauma-show' || e.target.closest('#btn-trauma-show')) showTrauma();
});
}

View file

@ -0,0 +1,147 @@
// ============================================================
// bedside/ventilation.js
// O2 & VENTILATION (teaching reference).
// ============================================================
import { S } from './shared.js';
function showVent() {
var wt = parseFloat(document.getElementById('vent-weight').value);
var age = parseFloat(document.getElementById('vent-age').value);
var el = document.getElementById('vent-result');
var html = '';
// ── Target SpO2 quick reference ──
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:0 0 6px;">Target SpO2</h5>';
html += '<div style="overflow-x:auto;-webkit-overflow-scrolling:touch;margin-bottom:14px;"><table style="width:100%;min-width:460px;border-collapse:collapse;font-size:12px;">';
html += '<thead><tr style="background:var(--g100);"><th style="text-align:left;padding:5px 8px;">Patient</th><th style="text-align:left;padding:5px 8px;">Target</th><th style="text-align:left;padding:5px 8px;">Notes</th></tr></thead><tbody>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Most children</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>94-98%</strong></td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Normal</td></tr>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Bronchiolitis (AAP 2014/2023)</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>≥90%</strong></td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Don\'t chase higher saturations</td></tr>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Chronic lung disease / CF</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>90-94%</strong></td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Avoid hyperoxia in CO2 retainers</td></tr>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Preterm neonate</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>90-95%</strong></td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Minimize ROP risk</td></tr>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Term neonate (min of life)</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>Per NRP ladder</strong></td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">1 min 60-65%, 10 min 85-95%</td></tr>';
html += '</tbody></table></div>';
// ── Escalation ladder ──
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 6px;">Escalation ladder (step up when target not reached / work of breathing)</h5>';
html += S.stepBox('#10b981',
'1. Nasal cannula (low-flow)',
'<strong>0.5-6 L/min</strong> &nbsp;·&nbsp; FiO2 ~24-40% (rough: room air 21% + 4% per L/min) &nbsp;·&nbsp; comfortable, no humidification needed. Good for mild hypoxia or baseline support.');
html += S.stepBox('#3b82f6',
'2. Simple face mask',
'<strong>6-10 L/min</strong> &nbsp;·&nbsp; FiO2 35-60%. Must keep flow &gt;6 L/min to flush CO2. Tolerated less well than NC.');
html += S.stepBox('#8b5cf6',
'3. Non-rebreather mask',
'<strong>10-15 L/min</strong> &nbsp;·&nbsp; FiO2 60-90%. Reservoir bag must stay inflated. For severe hypoxia with intact breathing.');
html += S.stepBox('#f59e0b',
'4. High-flow nasal cannula (HFNC)',
'<strong>' + (wt ? '1-2 L/kg/min = ' + S.d(wt, 1) + '-' + S.d(wt, 2) + ' L/min' : '1-2 L/kg/min') + '</strong> (max ~50-60 L/min adults) &nbsp;·&nbsp; heated + humidified &nbsp;·&nbsp; FiO2 30-100% titratable &nbsp;·&nbsp; generates ~2-5 cmH2O PEEP &nbsp;·&nbsp; works for bronchiolitis, early resp failure. Reassess at 1-2 h — no improvement → step up.');
html += S.stepBox('#ef4444',
'5. Non-invasive ventilation (CPAP / BiPAP)',
'<strong>CPAP</strong> 5-10 cmH2O (pure PEEP) &nbsp;·&nbsp; <strong>BiPAP</strong> IPAP 10-14 / EPAP 5 (adds pressure support) &nbsp;·&nbsp; FiO2 as needed. Needs cooperative patient, intact airway reflexes, no copious secretions. Contraindicated: coma, apnea, unstable hemodynamics, facial trauma.');
html += S.stepBox('#dc2626',
'6. Intubate + mechanical ventilation',
'When NIV fails, airway compromised, apnea, or GCS ≤8. See RSI under the Airway tab for drugs.');
// ── BVM how-to ──
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 6px;">Bag-Valve-Mask (BVM)</h5>';
html += '<div style="padding:10px 12px;background:var(--blue-light);border-radius:6px;font-size:12px;line-height:1.6;">';
html += '<strong>When:</strong> apnea, bradycardia (HR &lt;60 in neonate; inadequate breathing at any age), during resuscitation.<br>';
html += '<strong>Rate:</strong> Newborn 40-60 /min &nbsp;·&nbsp; Infant-child 20-30 /min &nbsp;·&nbsp; Adolescent 10-12 /min (1 breath q5-6 sec).<br>';
html += '<strong>Tidal volume:</strong> 6-8 mL/kg — only enough to see <strong>gentle chest rise</strong>. Avoid over-ventilation (gastric distension, ↓venous return, lung injury).<br>';
html += '<strong>Technique:</strong> head tilt / jaw thrust, proper mask seal (E-C grip or 2-thumb), squeeze 1 sec, release fully before next breath. Attach O2 at 10-15 L/min + reservoir.<br>';
html += '<strong>If not ventilating:</strong> MR SOPA — <u>M</u>ask reseal, <u>R</u>eposition airway, <u>S</u>uction mouth+nose, <u>O</u>pen mouth, <u>P</u>ressure ↑, <u>A</u>lternative airway (LMA or ETT).';
html += '</div>';
// ── Mechanical vent settings ──
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 6px;">Mechanical ventilator — starting settings</h5>';
var rate = age == null ? '' : (age < 0.1 ? '30-40' : age < 1 ? '25-35' : age < 5 ? '20-25' : age < 12 ? '16-20' : '12-16');
html += '<div style="padding:10px 12px;background:var(--g50);border-radius:6px;font-size:12px;line-height:1.65;">';
html += '<strong>Mode:</strong> Volume-control (set TV, get pressure) OR Pressure-control (set pressure, get TV). PRVC / SIMV-PS are hybrids. Pick what your unit uses.<br>';
html += '<strong>Tidal volume:</strong> <strong>' + (wt ? S.d(wt, 6) + '-' + S.d(wt, 8) + ' mL' : '6-8 mL/kg') + '</strong> <span style="color:var(--g500);font-size:11px;">(6-8 mL/kg)</span>. Use lower (4-6 mL/kg) for ARDS.<br>';
html += '<strong>Rate:</strong> ' + (rate ? rate + ' /min (for age ' + age + ' yr)' : 'Newborn 30-40 · Infant 25-35 · Child 16-20 · Adolescent 12-16') + '.<br>';
html += '<strong>PEEP:</strong> start <strong>5 cmH2O</strong> (routine). Increase to 8-12+ for refractory hypoxia (ARDS).<br>';
html += '<strong>FiO2:</strong> start 100%, wean rapidly to lowest that maintains target SpO2 (usually &lt;60%).<br>';
html += '<strong>I:E ratio:</strong> 1:2 normally. 1:3-4 for obstructive disease (asthma, bronchiolitis) to allow exhalation.<br>';
html += '<strong>Inspiratory time:</strong> 0.5 s infant &nbsp;·&nbsp; 0.7-1 s child &nbsp;·&nbsp; 1-1.2 s adolescent.<br>';
html += '<strong>Pressure limits:</strong> keep plateau &lt;30 cmH2O (ideally &lt;28) to avoid barotrauma.';
html += '</div>';
// ── Pressure-time waveform ──
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 6px;">Pressure-time waveform (what the monitor shows)</h5>';
html += '<div style="padding:10px;background:white;border:1px solid var(--g200);border-radius:8px;margin-bottom:14px;">';
html += '<svg viewBox="0 0 540 260" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:640px;display:block;margin:0 auto;">' +
// grid lines
'<line x1="60" y1="60" x2="500" y2="60" stroke="#f3f4f6"/>' +
'<line x1="60" y1="100" x2="500" y2="100" stroke="#f3f4f6"/>' +
'<line x1="60" y1="140" x2="500" y2="140" stroke="#f3f4f6"/>' +
'<line x1="60" y1="180" x2="500" y2="180" stroke="#f3f4f6"/>' +
// axes
'<line x1="60" y1="220" x2="500" y2="220" stroke="#374151" stroke-width="1.5"/>' +
'<line x1="60" y1="20" x2="60" y2="220" stroke="#374151" stroke-width="1.5"/>' +
// y-axis tick labels (cmH2O)
'<text x="55" y="64" font-size="10" text-anchor="end" fill="#6b7280">30</text>' +
'<text x="55" y="104" font-size="10" text-anchor="end" fill="#6b7280">20</text>' +
'<text x="55" y="144" font-size="10" text-anchor="end" fill="#6b7280">10</text>' +
'<text x="55" y="184" font-size="10" text-anchor="end" fill="#6b7280">5</text>' +
'<text x="55" y="224" font-size="10" text-anchor="end" fill="#6b7280">0</text>' +
// axis titles
'<text x="275" y="250" font-size="11" text-anchor="middle" fill="#374151">Time (seconds)</text>' +
'<text x="18" y="120" font-size="11" text-anchor="middle" fill="#374151" transform="rotate(-90 18 120)">Pressure (cmH₂O)</text>' +
// x-axis time markers
'<text x="60" y="238" font-size="10" text-anchor="middle" fill="#6b7280">0</text>' +
'<text x="200" y="238" font-size="10" text-anchor="middle" fill="#6b7280">2</text>' +
'<text x="340" y="238" font-size="10" text-anchor="middle" fill="#6b7280">4</text>' +
'<text x="480" y="238" font-size="10" text-anchor="middle" fill="#6b7280">6</text>' +
// PEEP reference dashed line (y=180 = 5 cmH2O)
'<line x1="60" y1="180" x2="500" y2="180" stroke="#10b981" stroke-width="1" stroke-dasharray="4,3" opacity="0.5"/>' +
// Pressure curve — 3 breaths, each 2s (Ti=0.8s, Te=1.2s); 70px/sec; PEEP=5→y=180, PIP=25→y=80, Plateau=22→y=88
'<path d="M 60 180 L 80 180 L 90 80 L 145 88 L 155 180 L 200 180 L 210 80 L 265 88 L 275 180 L 340 180 L 350 80 L 405 88 L 415 180 L 480 180" fill="none" stroke="#3b82f6" stroke-width="2.5" stroke-linejoin="round"/>' +
// PIP label
'<line x1="120" y1="50" x2="92" y2="78" stroke="#ef4444"/>' +
'<text x="125" y="48" font-size="11" font-weight="bold" fill="#ef4444">PIP</text>' +
'<text x="125" y="60" font-size="9" fill="#6b7280">peak inspiratory</text>' +
// Plateau label
'<line x1="230" y1="50" x2="145" y2="88" stroke="#a855f7"/>' +
'<text x="235" y="48" font-size="11" font-weight="bold" fill="#a855f7">Plateau</text>' +
'<text x="235" y="60" font-size="9" fill="#6b7280">reflects alveolar pressure</text>' +
// PEEP label
'<line x1="380" y1="208" x2="345" y2="182" stroke="#10b981"/>' +
'<text x="385" y="208" font-size="11" font-weight="bold" fill="#10b981">PEEP</text>' +
// Ti bracket (x=90 to 155)
'<line x1="90" y1="32" x2="155" y2="32" stroke="#f59e0b" stroke-width="1"/>' +
'<line x1="90" y1="28" x2="90" y2="36" stroke="#f59e0b"/>' +
'<line x1="155" y1="28" x2="155" y2="36" stroke="#f59e0b"/>' +
'<text x="122" y="25" font-size="10" font-weight="bold" text-anchor="middle" fill="#f59e0b">Ti</text>' +
// Te bracket (x=155 to 200)
'<line x1="155" y1="202" x2="200" y2="202" stroke="#f59e0b" stroke-width="1"/>' +
'<line x1="155" y1="198" x2="155" y2="206" stroke="#f59e0b"/>' +
'<line x1="200" y1="198" x2="200" y2="206" stroke="#f59e0b"/>' +
'<text x="177" y="215" font-size="10" font-weight="bold" text-anchor="middle" fill="#f59e0b">Te</text>' +
'</svg>';
html += '<div style="font-size:11px;color:var(--g500);margin-top:8px;line-height:1.5;"><strong style="color:#ef4444;">PIP</strong> = peak pressure during inspiration (depends on TV, airway resistance, lung compliance). <strong style="color:#a855f7;">Plateau</strong> = static pressure at end-inspiration with brief breath-hold (reflects alveolar pressure — keep &lt;30). <strong style="color:#10b981;">PEEP</strong> = baseline end-expiratory pressure keeping alveoli open. <strong style="color:#f59e0b;">Ti / Te</strong> = inspiratory / expiratory time. Rate &times; (Ti+Te) = 60 s.</div>';
html += '</div>';
// ── Troubleshooting: "if this, change that" ──
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 6px;">Adjusting for gas exchange</h5>';
html += '<div style="overflow-x:auto;-webkit-overflow-scrolling:touch;"><table style="width:100%;min-width:480px;border-collapse:collapse;font-size:12px;">';
html += '<thead><tr style="background:var(--g100);"><th style="text-align:left;padding:5px 8px;">Problem</th><th style="text-align:left;padding:5px 8px;">First lever</th><th style="text-align:left;padding:5px 8px;">Second lever</th></tr></thead><tbody>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>Low SpO2</strong> (oxygenation)</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">↑ FiO2</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">↑ PEEP (recruits collapsed alveoli)</td></tr>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>↑ PCO2</strong> (ventilation)</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">↑ Rate</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">↑ Tidal volume</td></tr>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>↓ PCO2</strong> (over-ventilating)</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">↓ Rate</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">↓ Tidal volume</td></tr>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>High peak pressure</strong></td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Check tube (kink, mucus plug), compliance, bronchospasm</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Suction, bronchodilator, lower TV</td></tr>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>Auto-PEEP</strong> (asthma, bronchiolitis)</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">↓ Rate, ↑ expiratory time</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Disconnect + bag briefly if critical</td></tr>';
html += '</tbody></table></div>';
// ── Mental model summary ──
html += '<div style="margin-top:14px;padding:10px 12px;background:#d1fae5;border-radius:6px;font-size:12px;color:#065f46;line-height:1.55;"><strong>Mental model:</strong> Oxygenation is mostly <strong>FiO2 + PEEP</strong>. Ventilation (CO2) is mostly <strong>rate + tidal volume</strong>. Match mode to disease: obstructive (asthma, bronchiolitis) → long expiratory time, permissive hypercapnia. Restrictive (ARDS) → low TV, high PEEP, permissive hypercapnia + hypoxia.</div>';
html += S.ref('Based on AAP / PALS / AARC guidance. Tailor to individual patient and institutional protocols.');
el.innerHTML = html;
}
export function init() {
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-vent-show' || e.target.closest('#btn-vent-show')) showVent();
});
}

File diff suppressed because it is too large Load diff