Game:520 Unit 1 Dosage Calc Study Buddy: Difference between revisions

From Doc-Wiki
Jump to navigation Jump to search
m Docmoates moved page Game:Unit 1 Dosage Calc Study Buddy to Game:520 Unit 1 Dosage Calc Study Buddy: Adding 520 prefix to game pages
Forcing wikitext content model
 
(9 intermediate revisions by the same user not shown)
Line 1: Line 1:
<!-- Embedded HTML Study Application -->
{{#widget:520Unit1DosageCalc}}
<html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Unit 1 Dosage Calc</title>
<style>
  :root{
    --bg:#0b1220; --panel:#0f1a2e; --card:#111f3a; --muted:#9fb0d0; --text:#e9f0ff;
    --accent:#7aa2ff; --good:#3ddc97; --warn:#ffd166; --bad:#ff5c7a; --line:#223458;
  }
  *{box-sizing:border-box}
  body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;color:var(--text);
    background:radial-gradient(1000px 600px at 15% 0%, #14224a 0%, rgba(20,34,74,0) 60%),
              radial-gradient(900px 500px at 100% 20%, #1a2a59 0%, rgba(26,42,89,0) 55%),
              linear-gradient(180deg,#070b14, #0b1220 35%, #070b14)}
  .app{display:grid;grid-template-columns:350px 1fr;min-height:100vh}
  aside{padding:18px;border-right:1px solid var(--line);background:rgba(15,26,46,.65);backdrop-filter: blur(8px)}
  main{padding:18px 18px 32px}
  h1{margin:4px 0 8px;font-size:18px}
  h2{margin:0;font-size:16px}
  .sub{color:var(--muted);font-size:12px;line-height:1.35}
  .row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
  .btn{border:1px solid var(--line);background:rgba(17,31,58,.9);color:var(--text);padding:10px 12px;border-radius:12px;cursor:pointer}
  .btn:hover{border-color:#365089}
  .btn.primary{background:rgba(122,162,255,.18);border-color:rgba(122,162,255,.45)}
  .btn.danger{background:rgba(255,92,122,.12);border-color:rgba(255,92,122,.35)}
  .btn:disabled{opacity:.5;cursor:not-allowed}
  .statgrid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px}
  .stat{border:1px solid var(--line);background:rgba(17,31,58,.7);border-radius:14px;padding:10px}
  .stat .k{font-size:11px;color:var(--muted)}
  .stat .v{font-size:18px;margin-top:4px}
  .progressWrap{margin-top:14px;border:1px solid var(--line);border-radius:14px;padding:12px;background:rgba(17,31,58,.65)}
  .bar{height:10px;border-radius:999px;background:#1b2a49;overflow:hidden;border:1px solid #223458}
  .bar > div{height:100%;width:0;background:linear-gradient(90deg,var(--accent),#9bb8ff);transition:.2s}
  .small{font-size:11px;color:var(--muted);margin-top:6px;display:flex;justify-content:space-between;gap:10px}
  .filters{margin-top:14px;display:grid;grid-template-columns:1fr 1fr;gap:10px}
  select,input[type="search"]{width:100%;padding:10px 10px;border-radius:12px;border:1px solid var(--line);background:rgba(17,31,58,.7);color:var(--text);outline:none}
  .list{margin-top:12px;max-height:34vh;overflow:auto;padding-right:6px}
  .qitem{border:1px solid var(--line);border-radius:12px;padding:10px;margin-bottom:10px;background:rgba(17,31,58,.6);cursor:pointer}
  .qitem:hover{border-color:#365089}
  .qitem .top{display:flex;justify-content:space-between;gap:10px}
  .qitem .t{font-size:12px;color:var(--muted);margin-top:6px;line-height:1.25}
  .tag{font-size:11px;color:var(--muted);border:1px solid var(--line);padding:4px 8px;border-radius:999px;white-space:nowrap}
  .tag.good{border-color:rgba(61,220,151,.5);color:rgba(61,220,151,.95)}
  .tag.bad{border-color:rgba(255,92,122,.5);color:rgba(255,92,122,.95)}
  .tag.warn{border-color:rgba(255,209,102,.5);color:rgba(255,209,102,.95)}
  .card{border:1px solid var(--line);border-radius:16px;padding:16px;background:rgba(17,31,58,.65)}
  .header{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap}
  .meta{display:flex;gap:8px;flex-wrap:wrap}
  .badge{font-size:12px;padding:6px 10px;border-radius:999px;border:1px solid var(--line);background:rgba(15,26,46,.55)}
  .prompt{margin:10px 0 0;color:var(--text);line-height:1.45}
  .hint{margin:10px 0 0;color:var(--muted);font-size:12px}
  .opts{margin-top:12px;display:grid;gap:10px}
 
  .optCard{
    border:1px solid var(--line);
    border-radius:12px;
    padding:12px;
    background:rgba(15,26,46,.5);
    cursor:pointer;
    display:flex; gap:10px; align-items:flex-start;
    user-select:none;
  }
  .optCard:hover{border-color:#365089}
  .optMark{
    width:18px;height:18px;border-radius:6px;
    border:1px solid #365089;background:rgba(122,162,255,.06);
    flex:0 0 auto;margin-top:2px;
  }
  .optCard.selected .optMark{
    background:rgba(122,162,255,.35);
    border-color:rgba(122,162,255,.8);
    box-shadow:0 0 0 2px rgba(122,162,255,.12) inset;
  }
  .optText{flex:1}
 
  .actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:12px}
  .revealBox{margin-top:12px;border:1px dashed #365089;border-radius:14px;padding:12px;background:rgba(122,162,255,.06)}
  .revealBox .rtitle{font-size:12px;color:var(--muted);margin-bottom:8px}
  .split{display:grid;grid-template-columns:1.2fr .8fr;gap:12px}
  .miniTable{width:100%;border-collapse:collapse;margin-top:10px}
  .miniTable th,.miniTable td{padding:10px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}
  .kpiRow{display:flex;justify-content:space-between;gap:10px;align-items:center;margin-top:10px}
  .miniList{margin:10px 0 0;padding:0;list-style:none}
  .miniList li{padding:8px 10px;border:1px solid var(--line);border-radius:12px;background:rgba(15,26,46,.45);margin-bottom:8px}
  .miniList small{color:var(--muted)}
  .chip{display:inline-block;margin-left:8px;font-size:11px;padding:3px 8px;border-radius:999px;border:1px solid var(--line);color:var(--muted)}
  .chip.bad{border-color:rgba(255,92,122,.45);color:rgba(255,92,122,.95)}
  .chip.warn{border-color:rgba(255,209,102,.45);color:rgba(255,209,102,.95)}
  .caseBox{border:1px solid var(--line);border-radius:14px;padding:12px;background:rgba(15,26,46,.4);margin-top:12px}
  .caseStep{border-top:1px solid var(--line);margin-top:12px;padding-top:12px}
  .caseStep:first-child{border-top:none;margin-top:0;padding-top:0}
  textarea{width:100%;min-height:72px;resize:vertical;padding:10px;border-radius:12px;border:1px solid var(--line);background:rgba(15,26,46,.55);color:var(--text);outline:none}
  .table{width:100%;border-collapse:collapse;margin-top:10px}
  .table th,.table td{padding:10px;border-bottom:1px solid var(--line);text-align:left}
  .err{margin-top:12px;border:1px solid rgba(255,92,122,.35);background:rgba(255,92,122,.08);padding:10px;border-radius:12px;color:#ffd0d9;font-size:12px;display:none}
 
  .logicGrid{display:grid;gap:10px;margin-top:10px}
  .logicBox{border:1px solid var(--line);background:rgba(15,26,46,.45);border-radius:12px;padding:10px}
  .logicBox .ttl{font-size:12px;color:var(--muted);margin-bottom:6px}
  .logicBox .txt{line-height:1.35}
  @media (max-width: 980px){.app{grid-template-columns:1fr} aside{border-right:none;border-bottom:1px solid var(--line)} .split{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="app">
  <aside>
    <h1>Dosage Calculation Study Bot</h1>
    <div class="sub">
      Unit 1 Dosage Calculation ONLY
      <br/>No Patho/Pharm content.
    </div>
 
    <div class="progressWrap">
      <div class="bar"><div id="barFill"></div></div>
      <div class="small">
        <span id="progText">Progress: 0/30</span>
        <span id="pctText">0%</span>
      </div>
 
      <div class="kpiRow">
        <div class="small" style="margin:0"><span>Topics to revisit (partial/missed)</span><span id="reviewCount">0</span></div>
      </div>
      <ul class="miniList" id="reviewTopics"></ul>
    </div>
 
    <div class="statgrid">
      <div class="stat"><div class="k">Weighted Score</div><div class="v" id="scoreV">0</div></div>
      <div class="stat"><div class="k">Readiness</div><div class="v" id="bandV">—</div></div>
    </div>
 
    <div class="filters">
      <select id="filterMode">
        <option value="all">All</option>
        <option value="missed">Missed (below 70%)</option>
        <option value="partial">Partial (70–99%)</option>
        <option value="flagged">Flagged</option>
        <option value="unanswered">Unanswered</option>
      </select>
      <select id="topicFilter"></select>
    </div>
 
    <div style="margin-top:10px">
      <input id="searchBox" type="search" placeholder="Search prompt / topic…" />
    </div>
 
    <div class="row" style="margin-top:12px">
      <button class="btn" id="prevBtn">Prev</button>
      <button class="btn primary" id="nextBtn">Next</button>
      <button class="btn" id="jumpReportBtn">Report</button>
      <button class="btn danger" id="resetBtn">Reset</button>
    </div>
 
    <div class="list" id="qList"></div>
  </aside>
 
  <main>
    <div class="card" id="viewer"></div>
    <div class="err" id="errBox"></div>
 
    <div class="card" id="reportCard" style="margin-top:16px">
      <div class="header">
        <div>
          <h2>End Report</h2>
          <div class="sub">Weighted performance + a clean list of topics to revisit (partial or missed).</div>
        </div>
        <div class="meta">
          <span class="badge" id="repWeighted">Weighted: 0</span>
          <span class="badge" id="repPct">Percent: 0%</span>
          <span class="badge" id="repBand">Band: —</span>
        </div>
      </div>
 
      <div class="split" style="margin-top:12px">
        <div class="caseBox">
          <div style="display:flex;justify-content:space-between;gap:10px;align-items:center;flex-wrap:wrap">
            <div>
              <div style="font-size:13px;color:var(--muted)">Concept mastery (by topic)</div>
              <div class="sub">Percent is weighted by item format + difficulty.</div>
            </div>
            <button class="btn" id="downloadBtn">Download session JSON</button>
          </div>
          <table class="miniTable" id="topicTable">
            <thead><tr><th>Topic</th><th>Mastery</th><th>Status</th></tr></thead>
            <tbody></tbody>
          </table>
        </div>
 
        <div class="caseBox">
          <div style="font-size:13px;color:var(--muted)">Topics to revisit (list)</div>
          <div class="sub">Anything partial or missed appears here, sorted by frequency/priority.</div>
          <ul class="miniList" id="revisitList"></ul>
        </div>
      </div>
    </div>
  </main>
</div>
 
<script>
/* =========================
  Global error capture
  ========================= */
window.addEventListener("error", (e)=>{
  const box = document.getElementById("errBox");
  box.style.display = "block";
  box.textContent = "Script error: " + (e.message || "Unknown") + (e.filename ? (" @ " + e.filename) : "");
});
 
/* =========================
  CONFIG (editable)
  ========================= */
const CONFIG = {
  unitName: "Unit 1",
  passingHesiScore: 875,
  readinessBands: [
    { name: "Needs Work", minPct: 0 },
    { name: "Borderline", minPct: 70 },
    { name: "Ready", minPct: 85 }
  ],
  difficultyWeights: { 1: 1, 2: 2, 3: 3 },
  formatMultipliers: {
    mcq: 1,
    sata: 1,
    dropdown: 1,
    case: 2,
    matrix: 2,
    bowtie: 3,
    trends: 3
  },
  maxItemWeight: 6
};
 
/* =========================
  QUESTIONS (Unit 1 ONLY)
  ========================= */
const QUESTIONS = [
  // =========================
  // MCQ (foundation + common HESI/NCLEX styles)
  // =========================
  {
    id: 1, type: "mcq", topic: "Dose by Supply", difficulty: 2,
    stem: "Order: Morphine 4 mg IV now. Available: 10 mg/5 mL. How many mL will the nurse administer?",
    options: ["1 mL", "2 mL", "4 mL", "5 mL"],
    correct: [1],
    rationale: "Dimensional analysis: (4 mg ÷ 10 mg) × 5 mL = 2 mL.",
    plain: "You need 4 mg. Your vial has 10 mg in every 5 mL. Since 4 is less than half of 10, you need less than half the liquid (2 mL).",
    mnemonic: "D / H x Q (Desired / Have x Quantity)"
  },
  {
    id: 2, type: "mcq", topic: "Tablet Calculation", difficulty: 1,
    stem: "Order: Amoxicillin 500 mg PO now. Available: 250 mg/tablet. How many tablets will the nurse give?",
    options: ["1", "2", "3", "4"],
    correct: [1],
    rationale: "500 ÷ 250 = 2 tablets.",
    plain: "You need 500 total. Each pill is 250. 250 + 250 = 500.",
    mnemonic: "Want / Got (What you want / What you got)"
  },
  {
    id: 3, type: "mcq", topic: "IV Pump Rate (mL/hr)", difficulty: 1,
    stem: "An IV order reads: 1000 mL to infuse over 8 hours. What is the pump rate?",
    options: ["100 mL/hr", "125 mL/hr", "150 mL/hr", "175 mL/hr"],
    correct: [1],
    rationale: "1000 mL ÷ 8 hr = 125 mL/hr.",
    plain: "The pump only speaks 'mL per hour'. Simply take the total bag size and divide by total hours.",
    mnemonic: "Pump Rate = Total Volume / Total Time (Hours)"
  },
  {
    id: 4, type: "mcq", topic: "gtt/min (Gravity)", difficulty: 2,
    stem: "Infuse 500 mL over 4 hours. Tubing drop factor: 15 gtt/mL. What is the flow rate in gtt/min?",
    options: ["20 gtt/min", "25 gtt/min", "31 gtt/min", "40 gtt/min"],
    correct: [2],
    rationale: "mL/min = 500 ÷ 240 = 2.083; gtt/min = 2.083 × 15 = 31.25 ≈ 31.",
    plain: "Gravity drips are calculated in MINUTES. First convert 4 hours to 240 minutes. Then do (Vol x DF) / Minutes.",
    mnemonic: "The Video Doctor Films Minutes (Total Vol x Drop Factor / Minutes)"
  },
  {
    id: 5, type: "mcq", topic: "Weight-Based Dose (kg)", difficulty: 2,
    stem: "Client weighs 154 lb. Order: enoxaparin 1 mg/kg. Available: 100 mg/mL. How many mL will the nurse administer?",
    options: ["0.35 mL", "0.50 mL", "0.70 mL", "1.4 mL"],
    correct: [2],
    rationale: "154 lb ÷ 2.2 = 70 kg. Dose = 70 mg. Volume = 70 mg ÷ (100 mg/mL) = 0.70 mL.",
    plain: "Step 1: Pounds to Kg (make it smaller). Step 2: Multiply Kg by dose. Step 3: Divide by concentration.",
    mnemonic: "2.2 is the key (Lbs / 2.2 = Kg)"
  },
  {
    id: 6, type: "mcq", topic: "Safe Dose Range (Peds)", difficulty: 3,
    stem: "A child weighs 22 lb. Safe dose is 2–4 mg/kg/day. Provider orders 50 mg/day. What is the nurse’s priority action?",
    options: [
      "Administer as ordered",
      "Clarify the order before giving",
      "Split the dose into two doses",
      "Give with food to reduce GI upset"
    ],
    correct: [1],
    rationale: "22 lb ÷ 2.2 = 10 kg. Safe range = 20–40 mg/day. Ordered 50 mg/day exceeds safe range.",
    plain: "The baby is 10kg. The max safe dose is 4 mg x 10kg = 40mg. The doctor ordered 50mg. That's an overdose.",
    mnemonic: "Safety First: Calculate the range BEFORE checking the order."
  },
 
  // =========================
  // SATA (MCMA partial credit style)
  // =========================
  {
    id: 7, type: "sata", topic: "Error Prevention", difficulty: 2,
    stem: "Which actions reduce dosage calculation errors? (Select all that apply)",
    options: [
      "Convert units to one system before solving",
      "Estimate whether the final answer is reasonable",
      "Skip unit labels to save time",
      "Double-check decimal placement",
      "Use an independent double-check for high-alert meds"
    ],
    correct: [0, 1, 3, 4],
    rationale: "Unit consistency, reasonableness checks, decimal safety, and double-checks reduce medication errors.",
    plain: "Make sure apples match apples (units), and ask 'Does this answer make sense?' before giving it.",
    mnemonic: "The 6 Rights + 'Does it make sense?'"
  },
  {
    id: 8, type: "sata", topic: "High-Alert Meds Safety", difficulty: 2,
    stem: "Which medications are commonly treated as high-alert and require extra safeguards? (Select all that apply)",
    options: ["Insulin", "Heparin", "Potassium chloride IV", "Acetaminophen", "Warfarin"],
    correct: [0, 1, 2, 4],
    rationale: "Insulin, heparin, IV KCl, and warfarin are commonly high-alert due to serious harm risk if misdosed.",
    plain: "Think: Which drugs can kill a patient instantly if the math is wrong? Blood thinners, insulin, and IV potassium are top offenders.",
    mnemonic: "PINCH (Potassium, Insulin, Narcotics, Chemo, Heparin)"
  },
  {
    id: 9, type: "sata", topic: "Unit Conversions", difficulty: 2,
    stem: "Which conversions are correct? (Select all that apply)",
    options: [
      "1 g = 1000 mg",
      "1 mg = 1000 mcg",
      "1 L = 100 mL",
      "2.2 lb = 1 kg",
      "1 tsp = 10 mL"
    ],
    correct: [0, 1, 3],
    rationale: "Correct: 1 g=1000 mg; 1 mg=1000 mcg; 2.2 lb=1 kg. 1 L=1000 mL; 1 tsp=5 mL.",
    plain: "Remember your 1000s rule. Grams to milligrams is 1000. Milligrams to micrograms is 1000. But teaspoons are only 5.",
    mnemonic: "King Henry Died By Drinking Chocolate Milk (Kilo, Hecto, Deka, Base, Deci, Centi, Milli)"
  },
 
  // =========================
  // Dropdowns
  // =========================
  {
    id: 10, type: "dropdown", topic: "Dose by Supply", difficulty: 1,
    stem: "Complete the calculation.",
    blanks: [
      { text: "Order: furosemide 40 mg IV. Available: 20 mg/2 mL. Give: ", options: ["2 mL", "4 mL", "6 mL", "8 mL"], correct: "4 mL" }
    ],
    rationale: "20 mg in 2 mL → 10 mg/mL. Need 40 mg → 4 mL.",
    plain: "If 2mL holds 20mg, then 1mL holds 10mg. You need 40mg, so you need 4mL.",
    mnemonic: "Double the dose = Double the volume (if concentration stays same)"
  },
  {
    id: 11, type: "dropdown", topic: "IV Pump Rate (mL/hr)", difficulty: 1,
    stem: "Complete the calculation.",
    blanks: [
      { text: "Infuse 250 mL over 2 hours. Pump rate: ", options: ["100", "125", "150", "175"], correct: "125" }
    ],
    rationale: "250 ÷ 2 = 125 mL/hr.",
    plain: "The pump needs to know 'how much per ONE hour'. Divide total (250) by hours (2).",
    mnemonic: "Total Vol / Total Hours"
  },
  {
    id: 12, type: "dropdown", topic: "Reconstitution/Concentration", difficulty: 2,
    stem: "A vial is reconstituted to a total volume of 10 mL and contains 1 g total drug. Select the final concentration.",
    blanks: [
      { text: "Concentration (mg/mL): ", options: ["10", "50", "100", "200"], correct: "100" }
    ],
    rationale: "1 g = 1000 mg. 1000 mg ÷ 10 mL = 100 mg/mL.",
    plain: "Always convert grams to mg first. 1 gram is huge (1000mg). Spread 1000mg across 10mL = 100 per mL.",
    mnemonic: "1 g = 1 paperclip = 1000 tiny grains of sand (mg)"
  },
 
  // =========================
  // MATRIX / GRID (2)
  // =========================
  {
    id: 13, type: "matrix", topic: "Method Selection", difficulty: 1,
    stem: "Match each scenario to the best calculation approach.",
    rows: [
      { row: "IV gravity rate ordered in gtt/min", options: ["mL/hr pump", "gtt/min formula", "tablet calculation"], correct: "gtt/min formula" },
      { row: "Oral tablets available as mg/tablet", options: ["tablet calculation", "gtt/min formula", "titration protocol"], correct: "tablet calculation" },
      { row: "Weight-based medication order (mg/kg)", options: ["weight-based dosing", "tablet calculation", "unitless ratio only"], correct: "weight-based dosing" }
    ],
    rationale: "Picking the right method prevents setup errors before the math even starts.",
    plain: "Don't overcomplicate it. If it asks for drops, use the drop formula. If it involves weight, start with kg.",
    mnemonic: "Match the formula to the 'Ask' (Unit)"
  },
  {
    id: 14, type: "matrix", topic: "Dose Forms", difficulty: 2,
    stem: "Match medication form to what you calculate directly.",
    rows: [
      { row: "Liquid PO", options: ["mL to administer", "gtt/min only", "tablets only"], correct: "mL to administer" },
      { row: "IV pump infusion", options: ["mL/hr", "tablets", "mcg/min without conversion"], correct: "mL/hr" },
      { row: "Tablets/capsules", options: ["number of tablets", "gtt/min only", "mL/hr only"], correct: "number of tablets" }
    ],
    rationale: "Dose form tells you what the final ‘answer unit’ must be.",
    plain: "Pumps speak mL/hr. Syringes speak mL. Pill cups speak tablets. Know your output unit.",
    mnemonic: "Pump = Hour. Gravity = Minute. Syringe = mL."
  },
 
  // =========================
  // BOWTIE (2)
  // =========================
  {
    id: 15, type: "bowtie", topic: "NGN Bowtie – Overdose Risk", difficulty: 3,
    stem: "Bowtie: Identify the risk, contributing factors, and nursing actions.",
    center: {
      label: "Primary risk",
      options: ["Medication overdose", "Medication underdose", "Allergic reaction", "Therapeutic effect only"],
      correct: "Medication overdose"
    },
    left: {
      label: "Contributing factors (pick 2)", pick: 2,
      options: ["Decimal misplacement", "Incorrect unit conversion", "Patient education provided", "Medication reconciliation completed"],
      correct: ["Decimal misplacement", "Incorrect unit conversion"]
    },
    right: {
      label: "Priority actions (pick 2)", pick: 2,
      options: ["Recalculate with units and estimate reasonableness", "Use independent double-check when indicated", "Administer quickly to avoid delay", "Skip labels to reduce clutter"],
      correct: ["Recalculate with units and estimate reasonableness", "Use independent double-check when indicated"]
    },
    rationale: "Most critical dosing errors come from decimals and unit conversions; recalculation + double-checks prevent harm.",
    plain: "A misplaced decimal can turn a dose of 1.0 into 10.0. That's 10x the dose. Always estimate: 'Does this look right?'",
    mnemonic: "Leading Zero = Hero (0.5). Trailing Zero = No (5.0 -> 5)."
  },
  {
    id: 16, type: "bowtie", topic: "NGN Bowtie – Heparin Safety", difficulty: 3,
    stem: "Bowtie: Identify the risk, contributing factors, and nursing actions.",
    center: {
      label: "Primary risk",
      options: ["Bleeding complication", "Dehydration", "Hypoglycemia", "Pain escalation only"],
      correct: "Bleeding complication"
    },
    left: {
      label: "Contributing factors (pick 2)", pick: 2,
      options: ["Dose miscalculation", "No baseline labs reviewed", "Client prefers morning meds", "IV site is patent"],
      correct: ["Dose miscalculation", "No baseline labs reviewed"]
    },
    right: {
      label: "Priority actions (pick 2)", pick: 2,
      options: ["Verify weight-based order and concentration", "Review relevant labs per protocol", "Increase rate without verification", "Document later to save time"],
      correct: ["Verify weight-based order and concentration", "Review relevant labs per protocol"]
    },
    rationale: "Heparin errors can cause bleeding. Verify math + concentration and check protocol labs.",
    plain: "Heparin is high stakes. Wrong math = bleeding out. Always check the PTT/labs and weight first.",
    mnemonic: "Heparin = Hemorrhage Risk (H & H)"
  },
 
  // =========================
  // TRENDS (2)
  // =========================
  {
    id: 17, type: "trends", topic: "NGN Trends – Insulin Correction", difficulty: 2,
    stem: "Trends: A client is on a sliding-scale insulin protocol. Identify the best action based on trend + protocol.",
    table: {
      headers: ["Time", "BG (mg/dL)", "Symptoms"],
      rows: [
        ["0800", "310", "Thirsty"],
        ["1200", "260", "No symptoms"],
        ["1600", "220", "No symptoms"],
        ["2000", "180", "No symptoms"]
      ]
    },
    parts: [
      {
        type: "mcq",
        prompt: "The trend indicates the client is moving toward:",
        options: ["Worsening hyperglycemia", "Improving glycemic control", "Hypoglycemia risk", "No meaningful change"],
        correct: [1]
      },
      {
        type: "sata",
        prompt: "Which nursing checks remain priority while insulin is being adjusted? (Select all that apply)",
        options: ["Verify meal timing relative to insulin", "Monitor for hypoglycemia symptoms", "Hold all BG checks if improving", "Confirm insulin type/dose drawn up", "Encourage skipping meals"],
        correct: [0, 1, 3]
      }
    ],
    rationale: "BG is trending down (improving). Still verify timing, dose/type, and monitor for hypoglycemia.",
    plain: "Numbers are going down, which is good. But don't get lazy—insulin is dangerous. Confirm the dose and feed the patient.",
    mnemonic: "Cold & Clammy = Need some Candy. Hot & Dry = Sugar High."
  },
  {
    id: 18, type: "trends", topic: "NGN Trends – IV Fluids + Output", difficulty: 2,
    stem: "Trends: A client is ordered IV fluids. Use the trend to identify the best interpretation and action.",
    table: {
      headers: ["Hour", "IV Rate (mL/hr)", "Urine Output (mL/hr)", "BP"],
      rows: [
        ["0–1", "125", "15", "92/58"],
        ["1–2", "125", "20", "94/60"],
        ["2–3", "125", "28", "98/62"],
        ["3–4", "125", "35", "104/66"]
      ]
    },
    parts: [
      {
        type: "mcq",
        prompt: "The trend most strongly suggests:",
        options: ["Worsening hypoperfusion", "Improving perfusion response", "Fluid overload", "Medication error"],
        correct: [1]
      },
      {
        type: "dropdown",
        prompt: "Best next nursing action:",
        blanks: [
          { text: "Action: ", options: ["Stop fluids immediately", "Continue per order and reassess", "Increase rate without order", "Remove IV"], correct: "Continue per order and reassess" }
        ]
      }
    ],
    rationale: "BP and UO are improving at the current ordered rate; continue and reassess per protocol.",
    plain: "Kidneys are waking up (output > 30 is the goal) and BP is rising. The fluids are working. Don't stop now.",
    mnemonic: "Urine Output Goal > 30 mL/hr"
  },
 
  // =========================
  // CASE STUDIES (4) – unfolding (3 steps each)
  // =========================
  {
    id: 19, type: "case", topic: "Unfolding Case – Pediatric Acetaminophen", difficulty: 2,
    stem: "A child weighs 18 kg. Provider orders acetaminophen 15 mg/kg/dose PO. Available: 160 mg/5 mL.",
    steps: [
      {
        title: "Step 1 — Calculate Dose",
        type: "mcq",
        prompt: "What dose (mg) should the nurse give?",
        options: ["180 mg", "240 mg", "270 mg", "300 mg"],
        correct: [2],
        rationale: "18 kg × 15 mg/kg = 270 mg.",
        plain: "18 kg times 15 mg each = 270 total mg needed.",
        mnemonic: "Kg x Dose/Kg = Total Dose"
      },
      {
        title: "Step 2 — Convert to mL",
        type: "mcq",
        prompt: "How many mL will the nurse administer?",
        options: ["5 mL", "6 mL", "7.5 mL", "8.5 mL"],
        correct: [2],
        rationale: "160 mg/5 mL = 32 mg/mL. 270 mg ÷ 32 = 8.4375 mL (not listed). Using ratio: (270/160)*5 = 8.4375.",
        plain: "You need 270. Concentration is 32mg/mL. 270 / 32 is roughly 8.4. Closest safe option is 8.5 mL.",
        mnemonic: "D/H x V"
      },
      {
        title: "Step 3 — Safety Priority",
        type: "mcq",
        prompt: "Which check is most important before giving this dose?",
        options: ["Last dose timing and total daily limit", "Ask the parent to administer at home", "Skip weight verification if charted", "Give an extra dose for faster relief"],
        correct: [0],
        rationale: "Acetaminophen toxicity risk requires timing + total daily dose limit checks.",
        plain: "Tylenol hurts the liver if given too much/often. Check when the last dose was!",
        mnemonic: "Liver Lover? Watch the Tylenol total."
      }
    ]
  },
  {
    id: 20, type: "case", topic: "Unfolding Case – Heparin Infusion", difficulty: 3,
    stem: "Heparin infusion order: 18 units/kg/hr. Client weight: 80 kg. Bag: 25,000 units in 500 mL.",
    steps: [
      {
        title: "Step 1 — Units per hour",
        type: "mcq",
        prompt: "How many units/hr should the client receive?",
        options: ["960 units/hr", "1200 units/hr", "1440 units/hr", "1800 units/hr"],
        correct: [2],
        rationale: "80 × 18 = 1440 units/hr.",
        plain: "Weight (80) x Rate (18) = 1440 units needed per hour.",
        mnemonic: "Kg x Units/Kg = Total Units"
      },
      {
        title: "Step 2 — Convert to mL/hr",
        type: "mcq",
        prompt: "What rate in mL/hr should the pump be set to?",
        options: ["14.4 mL/hr", "28.8 mL/hr", "36 mL/hr", "57.6 mL/hr"],
        correct: [1],
        rationale: "Concentration: 25,000 ÷ 500 = 50 units/mL. Rate: 1440 ÷ 50 = 28.8 mL/hr.",
        plain: "Bag concentration is 50 units in every mL. You need 1440 units. 1440 / 50 = 28.8.",
        mnemonic: "Total Units / Concentration = Rate"
      },
      {
        title: "Step 3 — Safety Check",
        type: "sata",
        prompt: "Which actions are priority for safe heparin administration? (Select all that apply)",
        options: ["Confirm concentration on bag", "Use independent double-check if policy requires", "Increase rate if patient reports pain", "Monitor for bleeding", "Skip protocol labs if stable"],
        correct: [0, 1, 3],
        rationale: "Verify concentration, double-check, and monitor bleeding per protocol. Pain doesn’t justify rate changes; labs are not optional.",
        plain: "Heparin is a high-alert med. Double check everything. Watch for blood.",
        mnemonic: "High Alert = Double Check"
      }
    ]
  },
  {
    id: 21, type: "case", topic: "Unfolding Case – IV Antibiotic Volume", difficulty: 2,
    stem: "Order: ceftriaxone 1 g IV. Vial: 1 g. After reconstitution, concentration is 100 mg/mL. IV push max volume per policy is 10 mL per syringe.",
    steps: [
      {
        title: "Step 1 — Convert Dose",
        type: "mcq",
        prompt: "1 g equals how many mg?",
        options: ["10 mg", "100 mg", "1000 mg", "10,000 mg"],
        correct: [2],
        rationale: "1 g = 1000 mg.",
        plain: "Grams are big. Milligrams are small. 1 big gram = 1000 small mg.",
        mnemonic: "G to mg = x1000"
      },
      {
        title: "Step 2 — Calculate Volume",
        type: "mcq",
        prompt: "How many mL contains 1 g at 100 mg/mL?",
        options: ["5 mL", "10 mL", "15 mL", "20 mL"],
        correct: [1],
        rationale: "1000 mg ÷ 100 mg/mL = 10 mL.",
        plain: "You have 1000mg total. Each mL fits 100mg. You need 10 mL to hold it all.",
        mnemonic: "Total / Concentration = Volume"
      },
      {
        title: "Step 3 — Safety Decision",
        type: "mcq",
        prompt: "Given the policy limit of 10 mL per syringe, the nurse should:",
        options: ["Proceed with 10 mL in one syringe", "Split into two syringes automatically", "Hold medication without notifying anyone", "Give IM instead"],
        correct: [0],
        rationale: "10 mL meets the max per syringe; proceed per policy and administration guidelines.",
        plain: "Policy says max is 10mL. You have exactly 10mL. You are safe to proceed.",
        mnemonic: "Know your max limits."
      }
    ]
  },
  {
    id: 22, type: "case", topic: "Unfolding Case – Gravity Drip Check", difficulty: 2,
    stem: "Order: 1000 mL over 10 hours. Tubing: 20 gtt/mL. The nurse is using gravity tubing (no pump).",
    steps: [
      {
        title: "Step 1 — mL/min",
        type: "mcq",
        prompt: "What is the rate in mL/min?",
        options: ["1.0", "1.5", "1.67", "2.0"],
        correct: [2],
        rationale: "10 hr = 600 min. 1000 ÷ 600 = 1.67 mL/min.",
        plain: "Convert 10 hours to 600 minutes. 1000 mL / 600 min = 1.67 mL/min.",
        mnemonic: "Hours x 60 = Minutes"
      },
      {
        title: "Step 2 — gtt/min",
        type: "mcq",
        prompt: "What is the flow rate in gtt/min?",
        options: ["20", "28", "33", "40"],
        correct: [2],
        rationale: "1.67 × 20 = 33.4 ≈ 33 gtt/min.",
        plain: "Take mL/min (1.67) and multiply by the drop factor (20). ~33 drops per minute.",
        mnemonic: "mL/min x DF = gtt/min"
      },
      {
        title: "Step 3 — Safety Habit",
        type: "mcq",
        prompt: "Best practice after setting the gtt/min is to:",
        options: ["Recheck in 15 minutes and with any position change", "Assume it stays accurate all shift", "Turn the roller clamp fully open", "Silence alarms to prevent interruptions"],
        correct: [0],
        rationale: "Gravity flow rates drift with position and venous pressure; recheck is a safety standard.",
        plain: "Gravity drips aren't robots. If the patient moves their arm, the rate changes. Recheck often.",
        mnemonic: "Gravity is unreliable. Trust but verify."
      }
    ]
  },
 
  // =========================
  // More MCQ / SATA / Dropdown to reach 30 total
  // =========================
  {
    id: 23, type: "mcq", topic: "mcg ↔ mg Conversion", difficulty: 2,
    stem: "Order: levothyroxine 75 mcg PO. Available: 0.1 mg tablets. How many tablets will the nurse administer?",
    options: ["0.5 tablet", "0.75 tablet", "1 tablet", "1.5 tablets"],
    correct: [1],
    rationale: "0.1 mg = 100 mcg. Need 75 mcg → 75/100 = 0.75 tablet.",
    plain: "First, convert mg to mcg. 0.1 mg is 100 mcg. You need 75. 75 is three-quarters of 100.",
    mnemonic: "0.1 mg = 100 mcg"
  },
  {
    id: 24, type: "mcq", topic: "mEq Calculation (Basic)", difficulty: 3,
    stem: "Order: potassium chloride 20 mEq IV. Available: 10 mEq/5 mL. How many mL will the nurse administer?",
    options: ["5 mL", "10 mL", "15 mL", "20 mL"],
    correct: [1],
    rationale: "10 mEq in 5 mL → 2 mEq/mL. Need 20 mEq → 10 mL.",
    plain: "You need 20 mEq. You have 10 mEq in every 5mL scoop. You need two scoops. 5 + 5 = 10 mL.",
    mnemonic: "D/H x Q"
  },
  {
    id: 25, type: "sata", topic: "Rounding Rules & Policy", difficulty: 2,
    stem: "Which statements about rounding are safest for NCLEX/HESI-style dosing? (Select all that apply)",
    options: [
      "Follow facility policy for rounding (especially pediatrics/IV)",
      "Round only at the end of the problem",
      "Round aggressively early to save time",
      "If options don’t match, recheck math and units first",
      "Document your rounding method in high-risk situations"
    ],
    correct: [0, 1, 3, 4],
    rationale: "Don’t round early; confirm units; follow policy; document when needed. If options don’t match, it’s a red flag to recheck.",
    plain: "Rounding early introduces error. Keep the long decimals in your calculator until the very final step.",
    mnemonic: "Round at the End, not the Trend"
  },
  {
    id: 26, type: "dropdown", topic: "Ratio/Proportion Setup", difficulty: 1,
    stem: "Complete the statement.",
    blanks: [
      { text: "If 5 mL contains 250 mg, then 10 mL contains: ", options: ["250 mg", "500 mg", "750 mg", "1000 mg"], correct: "500 mg" }
    ],
    rationale: "Doubling the volume doubles the dose: 250 → 500 mg.",
    plain: "If you double the liquid (5 to 10), you double the drug (250 to 500).",
    mnemonic: "Direct Proportion: Double one, double the other."
  },
  {
    id: 27, type: "mcq", topic: "IVPB Rate (mL/hr)", difficulty: 1,
    stem: "An IVPB antibiotic is 100 mL to infuse over 30 minutes. What pump rate is needed?",
    options: ["100 mL/hr", "150 mL/hr", "200 mL/hr", "250 mL/hr"],
    correct: [2],
    rationale: "30 minutes = 0.5 hr. 100 ÷ 0.5 = 200 mL/hr.",
    plain: "You need 100 mL in half an hour. That means in a full hour, you'd need double that amount (200).",
    mnemonic: "Half hour run? Double the rate."
  },
  {
    id: 28, type: "mcq", topic: "Dose by Supply (Liquid)", difficulty: 2,
    stem: "Order: diphenhydramine 25 mg PO. Available: 12.5 mg/5 mL. How many mL will the nurse administer?",
    options: ["5 mL", "10 mL", "12 mL", "20 mL"],
    correct: [1],
    rationale: "25 mg is double 12.5 mg, so volume doubles: 5 mL → 10 mL.",
    plain: "You need 25. You have 12.5. 12.5 is half of 25. So you need two doses. 5mL x 2 = 10mL.",
    mnemonic: "D/H x V"
  },
  {
    id: 29, type: "sata", topic: "Clinical Reasonableness Check", difficulty: 2,
    stem: "A nurse calculates a dose volume of 25 mL for an IV push medication. Which actions are appropriate? (Select all that apply)",
    options: [
      "Recheck the concentration and units",
      "Confirm route and whether dilution/IVPB is required",
      "Administer anyway if patient is in pain",
      "Consult policy/charge nurse if volume seems unsafe for IV push",
      "Assess if the ordered dose exceeds safe range"
    ],
    correct: [0, 1, 3, 4],
    rationale: "Large IV push volumes are a red flag. Verify units/concentration, route/policy, and safe range before giving.",
    plain: "Imagine pushing 25 mL into an IV line by hand. That's a huge syringe. Red flag! Stop and recheck.",
    mnemonic: "Big Volume IV Push? Stop & Shush (Check it)"
  },
  {
    id: 30, type: "mcq", topic: "Time to Infuse", difficulty: 2,
    stem: "An IV is running at 75 mL/hr. How long will it take to infuse 450 mL?",
    options: ["4 hours", "5 hours", "6 hours", "7 hours"],
    correct: [2],
    rationale: "Time = volume ÷ rate = 450 ÷ 75 = 6 hours.",
    plain: "You have 450 total. You use 75 every hour. How many 75s fit into 450? (6).",
    mnemonic: "Total / Rate = Time"
  }
];
 
/* =========================
  State + Storage (MOVED UP FOR SAFETY)
  ========================= */
const LS_KEY = "unit1_ngn_hesi_session_v5";
let state = loadState() || {
  currentId: 1,
  startedAt: Date.now(),
  flagged: {},
  answers: {},
  scores: {},
  topicAgg: {}
};
 
function saveState(){ localStorage.setItem(LS_KEY, JSON.stringify(state)); }
function loadState(){ try{ return JSON.parse(localStorage.getItem(LS_KEY)); }catch(e){ return null; } }
function resetState(){
  localStorage.removeItem(LS_KEY);
  state = {
    currentId: 1,
    startedAt: Date.now(),
    flagged: {},
    answers: {},
    scores: {},
    topicAgg: {}
  };
  renderAll();
}
 
// DEFINING 'el' HERE SO IT EXISTS BEFORE ANY RENDER FUNCTION USES IT
const el = (id)=>document.getElementById(id);
const clamp=(n,min,max)=>Math.max(min,Math.min(max,n));
const uniq=(arr)=>Array.from(new Set(arr));
 
function escapeHtml(s){
  return String(s)
    .replaceAll("&","&amp;")
    .replaceAll("<","&lt;")
    .replaceAll(">","&gt;")
    .replaceAll('"',"&quot;")
    .replaceAll("'","&#039;");
}
 
/* =========================
  Scoring
  ========================= */
function itemWeight(q){
  const d = CONFIG.difficultyWeights[q.difficulty] ?? 1;
  const m = CONFIG.formatMultipliers[q.type] ?? 1;
  return Math.min(CONFIG.maxItemWeight, d * m);
}
function computeStatus(pct){
  if (pct >= 0.999) return "full";
  if (pct >= 0.70) return "partial";
  return "missed";
}
function mcqScore(selectedIdx, correctIdx){
  const earned = (selectedIdx.length===1 && correctIdx.includes(selectedIdx[0])) ? 1 : 0;
  return { earned, possible: 1 };
}
function sataScore(selectedIdx, correctIdx){
  const correctSet = new Set(correctIdx);
  let raw = 0;
  for (const idx of selectedIdx){
    raw += correctSet.has(idx) ? 1 : -1;
  }
  raw = Math.max(0, raw);
  const possible = correctIdx.length;
  const earned = Math.min(possible, raw);
  return { earned, possible };
}
function dropdownScore(values, blanks){
  let earned = 0;
  for (let i=0;i<blanks.length;i++){
    if ((values[i] ?? "") === blanks[i].correct) earned += 1;
  }
  return { earned, possible: blanks.length };
}
function matrixScore(map, rows){
  let earned = 0;
  for (let i=0;i<rows.length;i++){
    if ((map[i] ?? "") === rows[i].correct) earned += 1;
  }
  return { earned, possible: rows.length };
}
function bowtieScore(payload, q){
  let earned = 0;
  const possible = 1 + q.left.pick + q.right.pick;
 
  if ((payload.center ?? "") === q.center.correct) earned += 1;
 
  const leftCorrect = new Set(q.left.correct);
  let leftEarned = 0;
  for (const v of (payload.left ?? [])){
    leftEarned += leftCorrect.has(v) ? 1 : -1;
  }
  earned += clamp(leftEarned, 0, q.left.pick);
 
  const rightCorrect = new Set(q.right.correct);
  let rightEarned = 0;
  for (const v of (payload.right ?? [])){
    rightEarned += rightCorrect.has(v) ? 1 : -1;
  }
  earned += clamp(rightEarned, 0, q.right.pick);
 
  return { earned, possible };
}
function trendsScore(payload, q){
  let earned = 0, possible = 0;
  q.parts.forEach((p, idx) => {
    if (p.type === "mcq"){
      const s = mcqScore([payload[idx]], p.correct);
      earned += s.earned; possible += s.possible;
    } else if (p.type === "sata"){
      const s = sataScore(payload[idx] ?? [], p.correct);
      earned += s.earned; possible += s.possible;
    } else if (p.type === "dropdown"){
      const s = dropdownScore(payload[idx] ?? [], p.blanks);
      earned += s.earned; possible += s.possible;
    }
  });
  return { earned, possible };
}
function caseScore(payload, q){
  let earned = 0, possible = 0;
  q.steps.forEach((s, idx) => {
    if (s.type === "mcq"){
      const r = mcqScore([payload[idx]], s.correct);
      earned += r.earned; possible += r.possible;
    } else if (s.type === "sata"){
      const r = sataScore(payload[idx] ?? [], s.correct);
      earned += r.earned; possible += r.possible;
    } else if (s.type === "dropdown"){
      const r = dropdownScore(payload[idx] ?? [], s.blanks);
      earned += r.earned; possible += r.possible;
    }
  });
  return { earned, possible };
}
function scoreQuestion(q, payload){
  let earned=0, possible=0;
 
  if (q.type==="mcq"){
    ({earned, possible} = mcqScore(payload || [], q.correct));
  } else if (q.type==="sata"){
    ({earned, possible} = sataScore(payload || [], q.correct));
  } else if (q.type==="dropdown"){
    ({earned, possible} = dropdownScore(payload || [], q.blanks));
  } else if (q.type==="matrix"){
    ({earned, possible} = matrixScore(payload || {}, q.rows));
  } else if (q.type==="bowtie"){
    ({earned, possible} = bowtieScore(payload || {}, q));
  } else if (q.type==="trends"){
    ({earned, possible} = trendsScore(payload || [], q));
  } else if (q.type==="case"){
    ({earned, possible} = caseScore(payload || [], q));
  }
 
  const pct = possible ? (earned/possible) : 0;
  const status = computeStatus(pct);
 
  const w = itemWeight(q);
  const earnedW = pct * w;
  const possibleW = w;
 
  return { earned, possible, pct, status, earnedW, possibleW, at: Date.now() };
}
 
/* =========================
  Topic aggregation + revisit tracker
  ========================= */
function bumpTopic(topic, earnedW, possibleW, status){
  if (!state.topicAgg[topic]) state.topicAgg[topic] = { earned:0, possible:0, missedCount:0, partialCount:0 };
  state.topicAgg[topic].earned += earnedW;
  state.topicAgg[topic].possible += possibleW;
  if (status === "missed") state.topicAgg[topic].missedCount += 1;
  if (status === "partial") state.topicAgg[topic].partialCount += 1;
}
function rebuildTopicAgg(){
  state.topicAgg = {};
  for (const q of QUESTIONS){
    const sc = state.scores[q.id];
    if (!sc) continue;
    bumpTopic(q.topic, sc.earnedW, sc.possibleW, sc.status);
  }
}
function buildRevisitList(){
  rebuildTopicAgg();
  const out = [];
  for (const [topic, agg] of Object.entries(state.topicAgg)){
    const mastery = agg.possible ? (agg.earned/agg.possible)*100 : 0;
    const count = agg.missedCount + agg.partialCount;
    if (count <= 0) continue;
    const worst = agg.missedCount > 0 ? "missed" : "partial";
    out.push({ topic, mastery, count, worst });
  }
  out.sort((a,b)=> (a.worst===b.worst ? 0 : (a.worst==="missed" ? -1 : 1)) || (b.count-a.count) || (a.mastery-b.mastery));
  return out;
}
 
/* =========================
  Summary metrics
  ========================= */
function overallMastery(){
  let e=0,p=0;
  for (const q of QUESTIONS){
    const sc = state.scores[q.id];
    if (!sc) continue;
    e += sc.earnedW; p += sc.possibleW;
  }
  return p ? (e/p)*100 : 0;
}
function weightedScoreOutOf1200(){
  const pct = overallMastery();
  const score = Math.round(300 + (pct/100) * 900);
  return clamp(score, 300, 1200);
}
function statusTag(qid){
  const sc = state.scores[qid];
  if (!sc) return { text:"Unanswered", cls:"" };
  if (sc.status === "full") return { text:"Full", cls:"good" };
  if (sc.status === "partial") return { text:"Partial", cls:"warn" };
  return { text:"Missed", cls:"bad" };
}
 
/* =========================
  Clickable option engine
  ========================= */
function getSelection(q){ return state.answers[q.id]; }
function setSelection(q, payload){ state.answers[q.id] = payload; saveState(); }
function toggleSingle(q, idx){ setSelection(q, [idx]); }
function toggleMulti(q, idx){
  const cur = getSelection(q) || [];
  const set = new Set(cur);
  if (set.has(idx)) set.delete(idx); else set.add(idx);
  setSelection(q, [...set].sort((a,b)=>a-b));
}
 
/* =========================
  Render helpers
  ========================= */
function renderOptionCards(q, isMulti){
  const wrap = document.createElement("div");
  wrap.className = "opts";
  const selected = getSelection(q) || [];
  q.options.forEach((opt, idx)=>{
    const card = document.createElement("div");
    const active = isMulti ? selected.includes(idx) : (selected[0]===idx);
    card.className = "optCard" + (active ? " selected" : "");
    card.tabIndex = 0;
    card.setAttribute("role","button");
    card.setAttribute("aria-pressed", active ? "true" : "false");
    card.innerHTML = `<div class="optMark"></div><div class="optText">${escapeHtml(opt)}</div>`;
    card.onclick = ()=>{
      if (isMulti) toggleMulti(q, idx);
      else toggleSingle(q, idx);
      renderAll(true);
    };
    card.onkeydown = (e)=>{
      if (e.key==="Enter" || e.key===" "){
        e.preventDefault();
        card.click();
      }
    };
    wrap.appendChild(card);
  });
  return wrap;
}
 
function renderQuestionBody(q){
  const wrap = document.createElement("div");
  const existing = getSelection(q);
 
  if (q.type === "mcq"){
    wrap.appendChild(renderOptionCards(q, false));
  }
 
  if (q.type === "sata"){
    wrap.appendChild(renderOptionCards(q, true));
  }
 
  if (q.type === "dropdown"){
    const box = document.createElement("div");
    box.className = "caseBox";
    const vals = existing || [];
    q.blanks.forEach((b, i)=>{
      const sel = document.createElement("select");
      sel.dataset.blankIndex = i;
      sel.innerHTML = `<option value="">Select…</option>` + b.options.map(o=>`<option value="${escapeHtml(o)}">${escapeHtml(o)}</option>`).join("");
      sel.value = vals[i] || "";
      sel.onchange = ()=>{
        const cur = (getSelection(q) || []).slice();
        cur[i] = sel.value || "";
        setSelection(q, cur);
      };
      const line = document.createElement("div");
      line.style.marginTop = "8px";
      line.innerHTML = `<div style="color:var(--muted);font-size:12px">${escapeHtml(b.text)}</div>`;
      line.appendChild(sel);
      box.appendChild(line);
    });
    wrap.appendChild(box);
  }
 
  if (q.type === "matrix"){
    const box = document.createElement("div");
    box.className = "caseBox";
    const table = document.createElement("table");
    table.className = "table";
    table.innerHTML = `<thead><tr><th>Row</th><th>Pick one</th></tr></thead>`;
    const tb = document.createElement("tbody");
 
    const map = existing || {};
    q.rows.forEach((r, i)=>{
      const tr = document.createElement("tr");
      const td1 = document.createElement("td");
      td1.textContent = r.row;
 
      const td2 = document.createElement("td");
      const sel = document.createElement("select");
      sel.innerHTML = `<option value="">Select…</option>` + r.options.map(o=>`<option value="${escapeHtml(o)}">${escapeHtml(o)}</option>`).join("");
      sel.value = map[i] || "";
      sel.onchange = ()=>{
        const cur = Object.assign({}, (getSelection(q) || {}));
        cur[i] = sel.value || "";
        setSelection(q, cur);
      };
      td2.appendChild(sel);
 
      tr.appendChild(td1); tr.appendChild(td2);
      tb.appendChild(tr);
    });
 
    table.appendChild(tb);
    box.appendChild(table);
    wrap.appendChild(box);
  }
 
  if (q.type === "bowtie"){
    const box = document.createElement("div");
    box.className = "caseBox";
    const payload = existing || { center:"", left:[], right:[] };
 
    const center = document.createElement("div");
    center.innerHTML = `<div style="color:var(--muted);font-size:12px;margin-bottom:6px">${escapeHtml(q.center.label)}</div>`;
    const cSel = document.createElement("select");
    cSel.innerHTML = `<option value="">Select…</option>` + q.center.options.map(o=>`<option value="${escapeHtml(o)}">${escapeHtml(o)}</option>`).join("");
    cSel.value = payload.center || "";
    cSel.onchange = ()=>{
      setSelection(q, { ...payload, center: cSel.value || "" });
    };
    center.appendChild(cSel);
 
    const mkPickList = (side, cfg)=>{
      const sec = document.createElement("div");
      sec.className = "caseStep";
      sec.innerHTML = `<div style="color:var(--muted);font-size:12px;margin-bottom:6px">${escapeHtml(cfg.label)} (pick ${cfg.pick})</div>`;
      cfg.options.forEach((o)=>{
        const card = document.createElement("div");
        const selected = (payload[side] || []).includes(o);
        card.className = "optCard" + (selected ? " selected" : "");
        card.innerHTML = `<div class="optMark"></div><div class="optText">${escapeHtml(o)}</div>`;
        card.onclick = ()=>{
          const cur = new Set(payload[side] || []);
          if (cur.has(o)) cur.delete(o);
          else cur.add(o);
          const arr = [...cur];
          if (arr.length > cfg.pick) return;
          const next = { ...payload, [side]: arr };
          setSelection(q, next);
          renderAll(true);
        };
        sec.appendChild(card);
      });
      return sec;
    };
 
    box.appendChild(center);
    box.appendChild(mkPickList("left", q.left));
    box.appendChild(mkPickList("right", q.right));
    wrap.appendChild(box);
  }
 
  if (q.type === "trends"){
    const box = document.createElement("div");
    box.className = "caseBox";
 
    const t = document.createElement("table");
    t.className = "table";
    const head = `<tr>${q.table.headers.map(h=>`<th>${escapeHtml(h)}</th>`).join("")}</tr>`;
    const rows = q.table.rows.map(r=>`<tr>${r.map(c=>`<td>${escapeHtml(String(c))}</td>`).join("")}</tr>`).join("");
    t.innerHTML = `<thead>${head}</thead><tbody>${rows}</tbody>`;
    box.appendChild(t);
 
    const payload = existing || [];
 
    q.parts.forEach((p, idx)=>{
      const sec = document.createElement("div");
      sec.className = "caseStep";
      sec.innerHTML = `<div style="font-weight:600;margin-bottom:8px">${escapeHtml(p.prompt)}</div>`;
 
      if (p.type==="mcq"){
        const chosen = payload[idx] ?? null;
        p.options.forEach((opt, oi)=>{
          const card = document.createElement("div");
          const selected = chosen === oi;
          card.className = "optCard" + (selected ? " selected" : "");
          card.innerHTML = `<div class="optMark"></div><div class="optText">${escapeHtml(opt)}</div>`;
          card.onclick = ()=>{
            const cur = (getSelection(q) || []).slice();
            cur[idx] = oi;
            setSelection(q, cur);
            renderAll(true);
          };
          sec.appendChild(card);
        });
      } else if (p.type==="sata"){
        const chosen = payload[idx] || [];
        p.options.forEach((opt, oi)=>{
          const selected = chosen.includes(oi);
          const card = document.createElement("div");
          card.className = "optCard" + (selected ? " selected" : "");
          card.innerHTML = `<div class="optMark"></div><div class="optText">${escapeHtml(opt)}</div>`;
          card.onclick = ()=>{
            const cur = (getSelection(q) || []).slice();
            const set = new Set(cur[idx] || []);
            if (set.has(oi)) set.delete(oi); else set.add(oi);
            cur[idx] = [...set].sort((a,b)=>a-b);
            setSelection(q, cur);
            renderAll(true);
          };
          sec.appendChild(card);
        });
      } else if (p.type==="dropdown"){
        const vals = payload[idx] || [];
        p.blanks.forEach((b, bi)=>{
          const sel = document.createElement("select");
          sel.innerHTML = `<option value="">Select…</option>` + b.options.map(o=>`<option value="${escapeHtml(o)}">${escapeHtml(o)}</option>`).join("");
          sel.value = vals[bi] || "";
          sel.onchange = ()=>{
            const cur = (getSelection(q) || []).slice();
            const part = (cur[idx] || []).slice();
            part[bi] = sel.value || "";
            cur[idx] = part;
            setSelection(q, cur);
          };
          const line = document.createElement("div");
          line.style.marginTop = "8px";
          line.innerHTML = `<div style="color:var(--muted);font-size:12px">${escapeHtml(b.text)}</div>`;
          line.appendChild(sel);
          sec.appendChild(line);
        });
      }
 
      box.appendChild(sec);
    });
 
    wrap.appendChild(box);
  }
 
  if (q.type === "case"){
    const box = document.createElement("div");
    box.className = "caseBox";
    box.innerHTML = `<div style="color:var(--muted);font-size:12px;margin-bottom:8px">Unfolding case (3 steps)</div>`;
    const payload = existing || [];
 
    q.steps.forEach((s, idx)=>{
      const sec = document.createElement("div");
      sec.className = "caseStep";
      sec.innerHTML = `<div style="font-weight:700;margin-bottom:6px">${escapeHtml(s.title)}</div>
                      <div style="margin-bottom:10px">${escapeHtml(s.prompt)}</div>`;
 
      if (s.type==="mcq"){
        const chosen = payload[idx] ?? null;
        s.options.forEach((opt, oi)=>{
          const card = document.createElement("div");
          const selected = chosen === oi;
          card.className = "optCard" + (selected ? " selected" : "");
          card.innerHTML = `<div class="optMark"></div><div class="optText">${escapeHtml(opt)}</div>`;
          card.onclick = ()=>{
            const cur = (getSelection(q) || []).slice();
            cur[idx] = oi;
            setSelection(q, cur);
            renderAll(true);
          };
          sec.appendChild(card);
        });
      } else if (s.type==="sata"){
        const chosen = payload[idx] || [];
        s.options.forEach((opt, oi)=>{
          const selected = chosen.includes(oi);
          const card = document.createElement("div");
          card.className = "optCard" + (selected ? " selected" : "");
          card.innerHTML = `<div class="optMark"></div><div class="optText">${escapeHtml(opt)}</div>`;
          card.onclick = ()=>{
            const cur = (getSelection(q) || []).slice();
            const set = new Set(cur[idx] || []);
            if (set.has(oi)) set.delete(oi); else set.add(oi);
            cur[idx] = [...set].sort((a,b)=>a-b);
            setSelection(q, cur);
            renderAll(true);
          };
          sec.appendChild(card);
        });
      } else if (s.type==="dropdown"){
        const vals = payload[idx] || [];
        s.blanks.forEach((b, bi)=>{
          const sel = document.createElement("select");
          sel.innerHTML = `<option value="">Select…</option>` + b.options.map(o=>`<option value="${escapeHtml(o)}">${escapeHtml(o)}</option>`).join("");
          sel.value = vals[bi] || "";
          sel.onchange = ()=>{
            const cur = (getSelection(q) || []).slice();
            const step = (cur[idx] || []).slice();
            step[bi] = sel.value || "";
            cur[idx] = step;
            setSelection(q, cur);
          };
          const line = document.createElement("div");
          line.style.marginTop = "8px";
          line.innerHTML = `<div style="color:var(--muted);font-size:12px">${escapeHtml(b.text)}</div>`;
          line.appendChild(sel);
          sec.appendChild(line);
        });
      }
 
      box.appendChild(sec);
    });
 
    wrap.appendChild(box);
  }
 
  return wrap;
}
 
/* =========================
  Read payloads for scoring
  ========================= */
function readAnswerPayload(q){
  const payload = getSelection(q);
 
  if (q.type==="mcq") return payload || [];
  if (q.type==="sata") return payload || [];
  if (q.type==="dropdown") return payload || [];
  if (q.type==="matrix") return payload || {};
  if (q.type==="bowtie") return payload || { center:"", left:[], right:[] };
  if (q.type==="trends") return payload || [];
  if (q.type==="case") return payload || [];
  return payload;
}
 
/* =========================
  Reveal content (UPDATED TO USE DISTINCT FIELDS)
  ========================= */
function renderReveal(q){
  const sc = state.scores[q.id];
  const scoreLine = sc
    ? `<div class="sub">Score: ${Math.round(sc.pct*100)}% • Status: <strong>${escapeHtml(sc.status)}</strong></div>`
    : `<div class="sub">Submit first to compute score.</div>`;
 
  // UPDATED: Now uses direct fields for distinct logic
  const nursingLogic = q.rationale || "Rationale not provided.";
  const plainLogic = q.plain || "Plain speak explanation not provided.";
  const mnemonic = q.mnemonic || "No specific mnemonic for this item.";
 
  const keyBlock = (()=>{
    if (!sc) return "";
    if (q.type==="mcq" || q.type==="sata"){
      const correctText = q.correct.map(i=>q.options[i]).map(escapeHtml);
      return `<div style="margin-top:10px"><strong>Correct answer:</strong> ${correctText.join(q.type==="sata" ? " • " : "")}</div>`;
    }
    if (q.type==="dropdown"){
      const key = q.blanks.map(b=>`${escapeHtml(b.text)} <strong>${escapeHtml(b.correct)}</strong>`).join("<br/>");
      return `<div style="margin-top:10px"><strong>Correct answer:</strong><br/>${key}</div>`;
    }
    if (q.type==="matrix"){
      const key = q.rows.map(r=>`${escapeHtml(r.row)} → <strong>${escapeHtml(r.correct)}</strong>`).join("<br/>");
      return `<div style="margin-top:10px"><strong>Correct answer:</strong><br/>${key}</div>`;
    }
    if (q.type==="bowtie"){
      return `<div style="margin-top:10px"><strong>Correct answer:</strong></div>
        <div><small class="sub">${escapeHtml(q.center.label)}:</small> <strong>${escapeHtml(q.center.correct)}</strong></div>
        <div style="margin-top:6px"><small class="sub">${escapeHtml(q.left.label)}:</small> <strong>${escapeHtml(q.left.correct.join(" • "))}</strong></div>
        <div style="margin-top:6px"><small class="sub">${escapeHtml(q.right.label)}:</small> <strong>${escapeHtml(q.right.correct.join(" • "))}</strong></div>`;
    }
    if (q.type==="trends"){
      const keyParts = q.parts.map((p, idx)=>{
        if (p.type==="mcq") return `Part ${idx+1}: <strong>${escapeHtml(p.options[p.correct[0]])}</strong>`;
        if (p.type==="sata") return `Part ${idx+1}: <strong>${escapeHtml(p.correct.map(i=>p.options[i]).join(" • "))}</strong>`;
        if (p.type==="dropdown") return `Part ${idx+1}: <strong>${escapeHtml(p.blanks.map(b=>b.correct).join(" • "))}</strong>`;
        return `Part ${idx+1}: <strong>—</strong>`;
      }).join("<br/>");
      return `<div style="margin-top:10px"><strong>Correct answer:</strong><br/>${keyParts}</div>`;
    }
    if (q.type==="case"){
      const keySteps = q.steps.map((s, idx)=>{
        if (s.type==="mcq") return `${escapeHtml(s.title)} → <strong>${escapeHtml(s.options[s.correct[0]])}</strong>`;
        if (s.type==="sata") return `${escapeHtml(s.title)} → <strong>${escapeHtml(s.correct.map(i=>s.options[i]).join(" • "))}</strong>`;
        if (s.type==="dropdown") return `${escapeHtml(s.title)} → <strong>${escapeHtml(s.blanks.map(b=>b.correct).join(" • "))}</strong>`;
        return `${escapeHtml(s.title)} → <strong>—</strong>`;
      }).join("<br/>");
      return `<div style="margin-top:10px"><strong>Correct answer:</strong><br/>${keySteps}</div>`;
    }
    return "";
  })();
 
  return `
    ${scoreLine}
    ${keyBlock}
    <div class="logicGrid">
      <div class="logicBox">
        <div class="ttl">Logic (nursing terminology)</div>
        <div class="txt">${escapeHtml(nursingLogic)}</div>
      </div>
      <div class="logicBox">
        <div class="ttl">Logic (plain talk)</div>
        <div class="txt">${escapeHtml(plainLogic)}</div>
      </div>
      <div class="logicBox">
        <div class="ttl">Mnemonic / memory hook</div>
        <div class="txt">${escapeHtml(mnemonic)}</div>
      </div>
    </div>
  `;
}
 
/* =========================
  Render sidebar + progress + report + viewer
  ========================= */
function renderSidebar(){
  const topics = uniq(QUESTIONS.map(q=>q.topic)).sort((a,b)=>a.localeCompare(b));
  const tf = el("topicFilter");
  tf.innerHTML = `<option value="all">All topics</option>` + topics.map(t=>`<option value="${escapeHtml(t)}">${escapeHtml(t)}</option>`).join("");
 
  const list = el("qList");
  const mode = el("filterMode").value;
  const topicSel = el("topicFilter").value;
  const search = (el("searchBox").value || "").trim().toLowerCase();
 
  list.innerHTML = "";
  for (const q of QUESTIONS){
    const sc = state.scores[q.id];
    const flagged = !!state.flagged[q.id];
    const tag = statusTag(q.id);
 
    if (topicSel !== "all" && q.topic !== topicSel) continue;
    if (search && !(q.stem.toLowerCase().includes(search) || q.topic.toLowerCase().includes(search))) continue;
 
    if (mode === "flagged" && !flagged) continue;
    if (mode === "unanswered" && sc) continue;
    if (mode === "missed" && (!sc || sc.status !== "missed")) continue;
    if (mode === "partial" && (!sc || sc.status !== "partial")) continue;
 
    const div = document.createElement("div");
    div.className = "qitem";
    div.onclick = ()=>{ state.currentId = q.id; saveState(); renderAll(true); };
 
    const flagChip = flagged ? `<span class="tag warn">Flag</span>` : "";
    div.innerHTML = `
      <div class="top">
        <div><strong>Q${q.id}</strong> <span class="tag">${escapeHtml(q.type.toUpperCase())}</span></div>
        <div style="display:flex;gap:8px;align-items:center;justify-content:flex-end">
          ${flagChip}
          <span class="tag ${tag.cls}">${tag.text}</span>
        </div>
      </div>
      <div class="t">${escapeHtml(q.topic)} • Diff ${q.difficulty}</div>
    `;
    list.appendChild(div);
  }
}
 
function renderProgress(){
  const answered = Object.keys(state.scores).length;
  const total = QUESTIONS.length;
  const pct = Math.round((answered/total)*100);
  el("barFill").style.width = `${pct}%`;
  el("progText").textContent = `Progress: ${answered}/${total}`;
  el("pctText").textContent = `${pct}%`;
 
  const score1200 = weightedScoreOutOf1200();
  el("scoreV").textContent = score1200;
 
  // Benchmark: passing HESI score is 875
  el("bandV").textContent = (score1200 >= CONFIG.passingHesiScore) ? `PASS (≥${CONFIG.passingHesiScore})` : `NOT YET (<${CONFIG.passingHesiScore})`;
 
  const revisit = buildRevisitList();
  el("reviewCount").textContent = revisit.length;
  const ul = el("reviewTopics");
  ul.innerHTML = "";
  revisit.slice(0,8).forEach(r=>{
    const li = document.createElement("li");
    li.innerHTML = `<strong>${escapeHtml(r.topic)}</strong>
      <span class="chip ${r.worst==='missed'?'bad':'warn'}">${r.worst}</span>
      <div><small>${r.count} item(s) impacted • mastery ${Math.round(r.mastery)}%</small></div>`;
    ul.appendChild(li);
  });
  if (revisit.length > 8){
    const li = document.createElement("li");
    li.innerHTML = `<small>+ ${revisit.length-8} more… (see End Report)</small>`;
    ul.appendChild(li);
  }
 
  const overallPct = overallMastery();
  el("repWeighted").textContent = `Weighted: ${score1200}`;
  el("repPct").textContent = `Percent: ${Math.round(overallPct)}%`;
  el("repBand").textContent = `Benchmark: ${score1200 >= CONFIG.passingHesiScore ? "PASS" : "NOT YET"} (875)`;
}
 
function renderReport(){
  rebuildTopicAgg();
  const topics = uniq(QUESTIONS.map(q=>q.topic)).sort((a,b)=>a.localeCompare(b));
  const tbody = el("topicTable").querySelector("tbody");
  tbody.innerHTML = "";
  topics.forEach(t=>{
    const agg = state.topicAgg[t];
    const mastery = agg && agg.possible ? (agg.earned/agg.possible)*100 : 0;
    const status = mastery >= 85 ? "Strong" : (mastery >= 70 ? "Developing" : (agg ? "Needs Work" : "Not started"));
    const tr = document.createElement("tr");
    tr.innerHTML = `
      <td>${escapeHtml(t)}</td>
      <td>${Math.round(mastery)}%</td>
      <td>${escapeHtml(status)}</td>
    `;
    tbody.appendChild(tr);
  });
 
  const rev = buildRevisitList();
  const ul = el("revisitList");
  ul.innerHTML = "";
  if (rev.length === 0){
    const li = document.createElement("li");
    li.innerHTML = `<strong>No revisit topics yet.</strong><div><small>Answer items and this list will populate automatically.</small></div>`;
    ul.appendChild(li);
  } else {
    rev.forEach(r=>{
      const li = document.createElement("li");
      li.innerHTML = `<strong>${escapeHtml(r.topic)}</strong>
        <span class="chip ${r.worst==='missed'?'bad':'warn'}">${r.worst}</span>
        <div><small>${r.count} item(s) impacted • mastery ${Math.round(r.mastery)}%</small></div>`;
      ul.appendChild(li);
    });
  }
}
 
function renderViewer(){
  const q = QUESTIONS.find(x=>x.id===state.currentId) || QUESTIONS[0];
  if (!q) return;
 
  const flagged = !!state.flagged[q.id];
  const tag = statusTag(q.id);
  const w = itemWeight(q);
 
  const viewer = el("viewer");
  viewer.innerHTML = `
    <div class="header">
      <div>
        <h2>Q${q.id} of ${QUESTIONS.length}</h2>
        <div class="prompt">${escapeHtml(q.stem)}</div>
        <div class="hint">Pick your answer, submit, and the logic will auto-reveal.</div>
      </div>
      <div class="meta">
        <span class="badge">${escapeHtml(q.type.toUpperCase())}</span>
        <span class="badge">Diff ${q.difficulty}</span>
        <span class="badge">Weight ${w}</span>
        <span class="badge">${escapeHtml(q.topic)}</span>
        <span class="badge">${escapeHtml(tag.text)}</span>
      </div>
    </div>
 
    <div id="qBody"></div>
 
    <div class="actions">
      <button class="btn" id="flagBtn">${flagged ? "Unflag" : "Flag"}</button>
      <button class="btn primary" id="submitBtn">Submit</button>
      <button class="btn" id="clearBtn">Clear response</button>
    </div>
 
    <div class="revealBox" id="revealBox" style="display:none">
      <div class="rtitle">Answer + Logic (3 ways)</div>
      <div id="revealContent"></div>
    </div>
  `;
 
  const body = viewer.querySelector("#qBody");
  body.appendChild(renderQuestionBody(q));
 
  viewer.querySelector("#flagBtn").onclick = ()=>{
    state.flagged[q.id] = !state.flagged[q.id];
    saveState(); renderAll(true);
  };
 
  viewer.querySelector("#clearBtn").onclick = ()=>{
    delete state.answers[q.id];
    delete state.scores[q.id];
    rebuildTopicAgg();
    saveState(); renderAll(true);
  };
 
  viewer.querySelector("#submitBtn").onclick = ()=>{
    const payload = readAnswerPayload(q);
    state.answers[q.id] = payload;
 
    const scored = scoreQuestion(q, payload);
    state.scores[q.id] = scored;
 
    rebuildTopicAgg();
    saveState();
    renderAll(true);
 
    const rb = el("viewer").querySelector("#revealBox");
    rb.style.display = "block";
    el("viewer").querySelector("#revealContent").innerHTML = renderReveal(q);
  };
 
  if (state.scores[q.id]){
    const rb = viewer.querySelector("#revealBox");
    rb.style.display = "block";
    viewer.querySelector("#revealContent").innerHTML = renderReveal(q);
  }
}
 
function next(){
  const idx = QUESTIONS.findIndex(q=>q.id===state.currentId);
  if (idx < QUESTIONS.length-1){
    state.currentId = QUESTIONS[idx+1].id;
    saveState(); renderAll(true);
  }
}
function prev(){
  const idx = QUESTIONS.findIndex(q=>q.id===state.currentId);
  if (idx > 0){
    state.currentId = QUESTIONS[idx-1].id;
    saveState(); renderAll(true);
  }
}
 
function renderAll(keepScroll=false){
  renderSidebar();
  renderProgress();
  renderViewer();
  renderReport();
  if (!keepScroll){
    window.scrollTo({top:0, behavior:"auto"});
  }
}
 
/* =========================
  Events
  ========================= */
el("filterMode").addEventListener("change", ()=>renderSidebar());
el("topicFilter").addEventListener("change", ()=>renderSidebar());
el("searchBox").addEventListener("input", ()=>renderSidebar());
 
el("nextBtn").addEventListener("click", next);
el("prevBtn").addEventListener("click", prev);
el("resetBtn").addEventListener("click", ()=>{
  if (confirm("Reset this Unit 1 session? This clears answers and progress.")) resetState();
});
el("jumpReportBtn").addEventListener("click", ()=>{
  el("reportCard").scrollIntoView({behavior:"smooth", block:"start"});
});
el("downloadBtn").addEventListener("click", ()=>{
  const payload = { unit: CONFIG.unitName, exportedAt: new Date().toISOString(), state };
  const blob = new Blob([JSON.stringify(payload,null,2)], {type:"application/json"});
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "unit1_session.json";
  a.click();
  URL.revokeObjectURL(url);
});
 
/* =========================
  Init
  ========================= */
(function init(){
  rebuildTopicAgg();
  renderAll();
})();
</script>
</body>
</html>
</html>


[[Category:HU NSG 520 Pathophysiology and Pharmacology]]
[[Category:Herzing University/Games]]
[[Category:Herzing University/Games]]

Latest revision as of 22:27, 17 January 2026