Game:520 Unit 2 Study Buddy: Difference between revisions

From Doc-Wiki
Jump to navigation Jump to search
Created Unit 2 Study Buddy interactive study application
 
Forcing wikitext content model
 
(10 intermediate revisions by the same user not shown)
Line 1: Line 1:
<!-- Embedded HTML Study Application -->
{{#widget:520Unit2StudyBuddy}}
<html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Unit 2 Study Buddy (Patho/Immunity)</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>Unit 2 Study Buddy</h1>
    <div class="sub">
      Unit 2 Chapters Only.
      <br/>(Immunity, Infection, Cancer, Stress)
    </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>
/* =========================
  1. 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) : "");
});
 
/* =========================
  2. HELPER FUNCTIONS
  ========================= */
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;");
}
 
/* --- ADDED MISSING FUNCTIONS (FIXED) --- */
function statusTag(id){
  const sc = state.scores[id];
  if (!sc) return { text:"Unanswered", cls:"" };
  if (sc.status === "full") return { text:"Correct", cls:"good" };
  if (sc.status === "partial") return { text:"Partial", cls:"warn" };
  return { text:"Missed", cls:"bad" };
}
 
function weightedScoreOutOf1200(){
  let earned=0, possible=0;
  for(let id in state.scores){
    earned += state.scores[id].earnedW;
    possible += state.scores[id].possibleW;
  }
  if(!possible) return 0;
  return Math.round((earned/possible)*1200);
}
 
function overallMastery(){
  let earned=0, possible=0;
  for(let id in state.scores){
    earned += state.scores[id].earnedW;
    possible += state.scores[id].possibleW;
  }
  if(!possible) return 0;
  return (earned/possible)*100;
}
 
// Render the interactive question body based on type
function renderQuestionBody(q){
  const container = document.createElement("div");
  const prev = state.answers[q.id];
 
  // MCQ / SATA
  if(q.type === "mcq" || q.type === "sata"){
    const d = document.createElement("div");
    d.className = "opts";
    q.options.forEach((opt, idx)=>{
      const card = document.createElement("div");
      card.className = "optCard";
      const isSel = prev && prev.includes(idx);
      if(isSel) card.classList.add("selected");
     
      card.innerHTML = `<div class="optMark"></div><div class="optText">${escapeHtml(opt)}</div>`;
      card.onclick = () => {
        if(q.type === "mcq"){
          // Clear others
          Array.from(d.children).forEach(c=>c.classList.remove("selected"));
          card.classList.add("selected");
        } else {
          card.classList.toggle("selected");
        }
      };
      d.appendChild(card);
    });
    container.appendChild(d);
  }
 
  // Dropdown
  else if(q.type === "dropdown"){
    const d = document.createElement("div");
    d.className = "opts";
    q.blanks.forEach((b, i)=>{
      const row = document.createElement("div");
      row.style.marginBottom = "10px";
      const selVal = prev ? prev[i] : "";
     
      let html = `<div>${escapeHtml(b.text)}</div>`;
      html += `<select data-idx="${i}" style="margin-top:6px">`;
      html += `<option value="">Select...</option>`;
      b.options.forEach(o=>{
        html += `<option value="${escapeHtml(o)}" ${o===selVal?'selected':''}>${escapeHtml(o)}</option>`;
      });
      html += `</select>`;
      row.innerHTML = html;
      d.appendChild(row);
    });
    container.appendChild(d);
  }
 
  // Matrix
  else if(q.type === "matrix"){
    const tbl = document.createElement("table");
    tbl.className = "table";
    // Header
    let thead = `<thead><tr><th>Row</th>`;
    q.rows[0].options.forEach(o => thead += `<th>${escapeHtml(o)}</th>`);
    thead += `</tr></thead>`;
    tbl.innerHTML = thead;
   
    const tbody = document.createElement("tbody");
    q.rows.forEach((r, rIdx)=>{
      const tr = document.createElement("tr");
      let td = `<td>${escapeHtml(r.row)}</td>`;
      const selVal = prev ? prev[rIdx] : null;
      r.options.forEach(opt=>{
        const checked = (selVal === opt) ? "checked" : "";
        td += `<td><input type="radio" name="mx_${rIdx}" value="${escapeHtml(opt)}" ${checked}></td>`;
      });
      tr.innerHTML = td;
      tbody.appendChild(tr);
    });
    tbl.appendChild(tbody);
    container.appendChild(tbl);
  }
 
  // Bowtie
  else if(q.type === "bowtie"){
    const wrap = document.createElement("div");
    wrap.style.display="grid";
    wrap.style.gridTemplateColumns="1fr 1fr 1fr";
    wrap.style.gap="10px";
   
    // Left
    const lDiv = document.createElement("div");
    lDiv.className = "caseBox";
    lDiv.innerHTML = `<div class="sub">${escapeHtml(q.left.label)} (Pick ${q.left.pick})</div>`;
    q.left.options.forEach(o=>{
      const isSel = prev && prev.left && prev.left.includes(o);
      const chk = document.createElement("div");
      chk.className = `optCard ${isSel?'selected':''}`;
      chk.style.marginTop="6px";
      chk.innerHTML = `<div class="optMark"></div><div class="optText" style="font-size:12px">${escapeHtml(o)}</div>`;
      chk.onclick = ()=> chk.classList.toggle("selected");
      chk.dataset.val = o;
      chk.dataset.side = "left";
      lDiv.appendChild(chk);
    });
 
    // Center
    const cDiv = document.createElement("div");
    cDiv.className = "caseBox";
    cDiv.innerHTML = `<div class="sub">${escapeHtml(q.center.label)}</div>`;
    q.center.options.forEach(o=>{
      const isSel = prev && prev.center === o;
      const chk = document.createElement("div");
      chk.className = `optCard ${isSel?'selected':''}`;
      chk.style.marginTop="6px";
      chk.innerHTML = `<div class="optMark" style="border-radius:50%"></div><div class="optText" style="font-size:12px">${escapeHtml(o)}</div>`;
      chk.onclick = ()=> {
        Array.from(cDiv.querySelectorAll(".optCard")).forEach(x=>x.classList.remove("selected"));
        chk.classList.add("selected");
      };
      chk.dataset.val = o;
      chk.dataset.side = "center";
      cDiv.appendChild(chk);
    });
 
    // Right
    const rDiv = document.createElement("div");
    rDiv.className = "caseBox";
    rDiv.innerHTML = `<div class="sub">${escapeHtml(q.right.label)} (Pick ${q.right.pick})</div>`;
    q.right.options.forEach(o=>{
      const isSel = prev && prev.right && prev.right.includes(o);
      const chk = document.createElement("div");
      chk.className = `optCard ${isSel?'selected':''}`;
      chk.style.marginTop="6px";
      chk.innerHTML = `<div class="optMark"></div><div class="optText" style="font-size:12px">${escapeHtml(o)}</div>`;
      chk.onclick = ()=> chk.classList.toggle("selected");
      chk.dataset.val = o;
      chk.dataset.side = "right";
      rDiv.appendChild(chk);
    });
 
    wrap.appendChild(lDiv);
    wrap.appendChild(cDiv);
    wrap.appendChild(rDiv);
    container.appendChild(wrap);
  }
 
  // Trends
  else if(q.type === "trends"){
    const tbl = document.createElement("table");
    tbl.className="table";
    let th = `<thead><tr>`;
    q.table.headers.forEach(h=> th+=`<th>${escapeHtml(h)}</th>`);
    th+=`</tr></thead><tbody>`;
    q.table.rows.forEach(r=>{
      th+=`<tr>`;
      r.forEach(c=> th+=`<td>${escapeHtml(c)}</td>`);
      th+=`</tr>`;
    });
    th+=`</tbody>`;
    tbl.innerHTML = th;
    container.appendChild(tbl);
 
    q.parts.forEach((p, idx)=>{
      const div = document.createElement("div");
      div.className = "caseStep";
      div.innerHTML = `<div style="margin-bottom:8px"><strong>Part ${idx+1}:</strong> ${escapeHtml(p.prompt)}</div>`;
     
      const subOpts = document.createElement("div");
      subOpts.className = "opts";
     
      if(p.type==="mcq" || p.type==="sata"){
        p.options.forEach((opt, oIdx)=>{
          const card = document.createElement("div");
          card.className = "optCard";
          // prev[idx] might be int (mcq) or array (sata)
          let isSel = false;
          if(prev && prev[idx] !== undefined){
            if(p.type==="mcq") isSel = (prev[idx] === oIdx);
            else isSel = prev[idx].includes(oIdx);
          }
          if(isSel) card.classList.add("selected");
         
          card.innerHTML = `<div class="optMark"></div><div class="optText">${escapeHtml(opt)}</div>`;
          card.onclick = () => {
            if(p.type === "mcq"){
              Array.from(subOpts.children).forEach(c=>c.classList.remove("selected"));
              card.classList.add("selected");
            } else {
              card.classList.toggle("selected");
            }
          };
          card.dataset.oidx = oIdx;
          subOpts.appendChild(card);
        });
      } else if (p.type==="dropdown"){
        p.blanks.forEach((b, bIdx)=>{
            let html = `<span>${escapeHtml(b.text)}</span> <select data-bidx="${bIdx}">`;
            html += `<option value="">...</option>`;
            const pVal = (prev && prev[idx]) ? prev[idx][bIdx] : "";
            b.options.forEach(o=> html+=`<option value="${escapeHtml(o)}" ${o===pVal?'selected':''}>${escapeHtml(o)}</option>`);
            html += `</select>`;
            const span = document.createElement("div");
            span.innerHTML = html;
            subOpts.appendChild(span);
        });
      }
      div.appendChild(subOpts);
      container.appendChild(div);
    });
  }
 
  // Case
  else if(q.type === "case"){
    q.steps.forEach((s, idx)=>{
      const div = document.createElement("div");
      div.className = "caseStep";
      div.innerHTML = `<div style="font-weight:bold;margin-bottom:6px">${escapeHtml(s.title)}</div><div class="prompt" style="margin-bottom:10px">${escapeHtml(s.prompt)}</div>`;
     
      const subOpts = document.createElement("div");
      subOpts.className = "opts";
 
      if(s.type==="mcq" || s.type==="sata"){
        s.options.forEach((opt, oIdx)=>{
          const card = document.createElement("div");
          card.className = "optCard";
          let isSel = false;
          if(prev && prev[idx] !== undefined){
            if(s.type==="mcq") isSel = (prev[idx] === oIdx);
            else isSel = prev[idx].includes(oIdx);
          }
          if(isSel) card.classList.add("selected");
         
          card.innerHTML = `<div class="optMark"></div><div class="optText">${escapeHtml(opt)}</div>`;
          card.onclick = () => {
            if(s.type === "mcq"){
              Array.from(subOpts.children).forEach(c=>c.classList.remove("selected"));
              card.classList.add("selected");
            } else {
              card.classList.toggle("selected");
            }
          };
          card.dataset.oidx = oIdx;
          subOpts.appendChild(card);
        });
      } else if (s.type==="dropdown"){
        s.blanks.forEach((b, bIdx)=>{
            let html = `<span>${escapeHtml(b.text)}</span> <select data-bidx="${bIdx}">`;
            html += `<option value="">...</option>`;
            const pVal = (prev && prev[idx]) ? prev[idx][bIdx] : "";
            b.options.forEach(o=> html+=`<option value="${escapeHtml(o)}" ${o===pVal?'selected':''}>${escapeHtml(o)}</option>`);
            html += `</select>`;
            const span = document.createElement("div");
            span.innerHTML = html;
            subOpts.appendChild(span);
        });
      }
      div.appendChild(subOpts);
      container.appendChild(div);
    });
  }
 
  return container;
}
 
// Scrape answers from DOM based on type
function getSelection(q){
  const viewer = document.getElementById("viewer");
 
  if(q.type === "mcq"){
    const sel = viewer.querySelector(".optCard.selected");
    if(!sel) return [];
    // Find index
    const all = Array.from(viewer.querySelectorAll(".optCard"));
    return [all.indexOf(sel)];
  }
  if(q.type === "sata"){
    const all = Array.from(viewer.querySelectorAll(".optCard"));
    const idxs = [];
    all.forEach((c,i)=>{ if(c.classList.contains("selected")) idxs.push(i); });
    return idxs;
  }
  if(q.type === "dropdown"){
    const selects = viewer.querySelectorAll("select");
    const res = [];
    selects.forEach(s => res.push(s.value));
    return res;
  }
  if(q.type === "matrix"){
    const res = {};
    q.rows.forEach((r, i)=>{
      const checked = viewer.querySelector(`input[name="mx_${i}"]:checked`);
      res[i] = checked ? checked.value : "";
    });
    return res;
  }
  if(q.type === "bowtie"){
    const left = [], right = [];
    let center = "";
    viewer.querySelectorAll(".optCard.selected").forEach(el=>{
      const s = el.dataset.side;
      const v = el.dataset.val;
      if(s==="left") left.push(v);
      else if(s==="right") right.push(v);
      else if(s==="center") center = v;
    });
    return { left, center, right };
  }
  if(q.type === "trends" || q.type === "case"){
    const steps = viewer.querySelectorAll(".caseStep");
    const res = [];
    steps.forEach((step, idx)=>{
      const def = (q.type==="trends" ? q.parts[idx] : q.steps[idx]);
      if(def.type==="mcq"){
        const sel = step.querySelector(".optCard.selected");
        res.push( sel ? parseInt(sel.dataset.oidx) : -1 );
      } else if(def.type==="sata"){
        const sells = step.querySelectorAll(".optCard.selected");
        const arr = [];
        sells.forEach(s=> arr.push(parseInt(s.dataset.oidx)));
        res.push(arr);
      } else if(def.type==="dropdown"){
        const selects = step.querySelectorAll("select");
        const arr = [];
        selects.forEach(s=> arr.push(s.value));
        res.push(arr);
      }
    });
    return res;
  }
  return null;
}
/* --- END ADDED FUNCTIONS --- */
 
/* =========================
  3. CONFIG
  ========================= */
const CONFIG = {
  unitName: "Unit 2",
  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
};
 
/* =========================
  4. QUESTIONS (Unit 2 ONLY)
  ========================= */
const QUESTIONS = [
  { id:1, type:"mcq", topic:"Adaptive Immunity (B vs T)", difficulty:2,
    stem:"A client recovered from chickenpox years ago and now has rapid antibody production after re-exposure. Which immune process best explains this faster response?",
    options:["Innate immunity only", "Memory B-cell response", "Complement activation only", "Neutrophil chemotaxis"],
    correct:[1],
    rationale:"Memory B cells drive a faster, stronger secondary (anamnestic) antibody response after prior exposure.",
    plain:"Your body took a 'mugshot' of the virus last time. B-cells remember faces. When the virus shows up again, the B-cells attack immediately.",
    mnemonic:"B-cells = B-one marrow = Bullets (Antibodies)."
  },
  { id:2, type:"mcq", topic:"Adaptive Immunity (Cell-mediated)", difficulty:2,
    stem:"A client has a history of recurrent viral infections despite normal antibody levels. Which immune arm is most likely impaired?",
    options:["B-cell function", "T-cell (cell-mediated) immunity", "Platelet activation", "Erythropoiesis"],
    correct:[1],
    rationale:"T cells are critical for controlling viral infections and coordinating cell-mediated responses.",
    plain:"Antibodies float in blood, but viruses hide *inside* cells. You need T-cells to go door-to-door and kill the infected cells. No T-cells = viruses run wild.",
    mnemonic:"T-cells = Tough Troops (Hand-to-hand combat)."
  },
  { id:3, type:"sata", topic:"Alterations in Immunity (Hypersensitivity)", difficulty:3,
    stem:"Which findings are most consistent with an immediate (Type I) hypersensitivity reaction? (Select all that apply)",
    options:[
      "Wheezing and urticaria minutes after exposure",
      "Hypotension after a new medication dose",
      "Joint pain weeks after strep infection",
      "Positive Coombs test with hemolysis after transfusion mismatch",
      "Facial/lip swelling shortly after a vaccine"
    ],
    correct:[0, 1, 4],
    rationale:"Type I reactions are IgE-mediated and occur rapidly: hives, bronchospasm, angioedema, hypotension/anaphylaxis.",
    plain:"This is an 'Allergy Alarm' gone wrong. The body sees a peanut or bee sting and instantly dumps histamine. Everything swells up and shuts down fast.",
    mnemonic:"Type 1 = 1mmediate (Anaphylaxis)."
  },
  { id:4, type:"mcq", topic:"Infection (Stages)", difficulty:2,
    stem:"A client reports malaise and low-grade fever two days after exposure to influenza but before classic symptoms peak. Which stage of infection is most likely?",
    options:["Incubation", "Prodromal", "Illness", "Convalescence"],
    correct:[1],
    rationale:"Prodromal stage often includes nonspecific symptoms before maximal illness.",
    plain:"You aren't full-blown sick yet, but you just feel 'off'—tired, a bit achy. The storm is coming, but hasn't hit hard yet.",
    mnemonic:"Prodromal = Pre-sick (Just feeling 'blah')."
  },
  { id:5, type:"mcq", topic:"Infection (Transmission / PPE)", difficulty:2,
    stem:"A client is suspected to have pulmonary tuberculosis. Which infection control approach is priority?",
    options:["Contact precautions only", "Droplet precautions only", "Airborne precautions with fit-tested respirator", "Standard precautions only"],
    correct:[2],
    rationale:"TB requires airborne precautions due to aerosolized droplet nuclei.",
    plain:"TB bacteria float in the air like dust motes for a long time. A regular mask isn't tight enough. You need a special N95 mask that seals to your face.",
    mnemonic:"Airborne MTV (Measles, TB, Varicella)."
  },
  { id:6, type:"sata", topic:"Infection (Antimicrobial Resistance)", difficulty:2,
    stem:"Which actions help reduce antimicrobial resistance? (Select all that apply)",
    options:[
      "Use cultures when possible before starting therapy",
      "Stop antibiotics as soon as the fever breaks (no provider input)",
      "Teach clients to complete prescribed courses when indicated",
      "Use the narrowest effective antibiotic when feasible",
      "Share leftover antibiotics with family members"
    ],
    correct:[0, 2, 3],
    rationale:"Stewardship: culture-guided therapy, appropriate duration, and narrow-spectrum choices reduce resistance.",
    plain:"If you don't kill all the bacteria (by stopping meds early) or use a 'nuke' (broad spectrum) for a small fight, the surviving bacteria learn how to fight back and become superbugs.",
    mnemonic:"Culture before Cure (Antibiotics)."
  },
  { id:7, type:"mcq", topic:"Immunizing Agents (Vaccine Types)", difficulty:2,
    stem:"Which vaccine type contains a weakened pathogen that replicates enough to produce a strong immune response?",
    options:["Inactivated", "Live attenuated", "Toxoid", "Subunit/recombinant"],
    correct:[1],
    rationale:"Live attenuated vaccines use weakened organisms that can replicate and stimulate robust immunity.",
    plain:"It's a 'defanged' version of the real virus. It's alive enough to teach the immune system a lesson, but too weak to make a healthy person sick.",
    mnemonic:"Live = Lasting immunity, but Limited for immunocompromised."
  },
  { id:8, type:"mcq", topic:"Immunizing Agents (Passive vs Active)", difficulty:2,
    stem:"A client receives rabies immune globulin after an animal bite. What type of immunity is this?",
    options:["Naturally acquired active", "Artificially acquired active", "Artificially acquired passive", "Naturally acquired passive"],
    correct:[2],
    rationale:"Immune globulin provides ready-made antibodies (passive) given by injection (artificial).",
    plain:"Active immunity is making your own dinner. Passive immunity is ordering takeout (getting antibodies someone else made). It's fast, but doesn't last.",
    mnemonic:"Passive = Passed to you (borrowed protection)."
  },
  { id:9, type:"dropdown", topic:"Immunizing Agents (Teaching)", difficulty:1,
    stem:"Complete the teaching point.",
    blanks:[
      { text:"After vaccination, the nurse should observe the client for at least ", options:["5 minutes", "15 minutes", "30 minutes", "60 minutes"], correct:"15 minutes" }
    ],
    rationale:"Post-vaccine observation helps detect early severe reactions (e.g., anaphylaxis).",
    plain:"Most dangerous reactions (like throat closing up) happen within minutes. Stick around so we can save you if your body panics.",
    mnemonic:"15 minutes saves lives."
  },
  { id:10, type:"mcq", topic:"Immunizations (Contraindications/Cautions)", difficulty:3,
    stem:"Which client statement requires the nurse to hold a live vaccine and notify the provider?",
    options:[
      "“I have seasonal allergies.”",
      "“I’m taking high-dose prednisone for an autoimmune flare.”",
      "“I had a sore arm after my last shot.”",
      "“I have mild diarrhea today.”"
    ],
    correct:[1],
    rationale:"Significant immunosuppression is a contraindication/major caution for live vaccines due to infection risk.",
    plain:"If your immune system is turned off (steroids/chemo), even a 'weak' live virus can beat you up and cause the actual disease.",
    mnemonic:"No Live for Low immunity."
  },
  { id:11, type:"mcq", topic:"Immunosuppressants (Biologics Safety)", difficulty:3,
    stem:"A client is starting a biologic immunosuppressant for an autoimmune disorder. Which pre-treatment screening is most important?",
    options:["Hearing test", "TB screening", "Vision acuity test", "Peak flow baseline"],
    correct:[1],
    rationale:"Biologic immunosuppressants can reactivate latent infections; TB screening is a key safety step.",
    plain:"Biologics turn down the immune system's surveillance. If TB is sleeping in your lungs (latent), the drug wakes it up, and it attacks.",
    mnemonic:"Biologics wake up Bugs (TB)."
  },
  { id:12, type:"sata", topic:"Immunosuppressants (Teaching)", difficulty:2,
    stem:"Teaching for a client on long-term immunosuppressants should include which points? (Select all that apply)",
    options:[
      "Report fever or new cough promptly",
      "Avoid live vaccines unless specifically approved",
      "Expect faster wound healing",
      "Practice infection prevention (hand hygiene, avoiding sick contacts)",
      "Stop the medication abruptly if you feel better"
    ],
    correct:[0, 1, 3],
    rationale:"Immunosuppression increases infection risk; teach early reporting, prevention, and vaccine safety.",
    plain:"You lost your shield. Wash your hands, stay away from sick people, and if you get a fever, call immediately—don't wait it out.",
    mnemonic:"Protect the Patient (P-P-P: Prevent, Protect, Phone provider)."
  },
  { id:13, type:"mcq", topic:"Stress & Disease (Physiologic Response)", difficulty:2,
    stem:"A client under prolonged stress has persistent insomnia and elevated blood pressure. Which mechanism best explains how chronic stress contributes to disease risk?",
    options:["Decreased sympathetic tone", "Sustained cortisol/sympathetic activation", "Increased insulin sensitivity", "Suppressed inflammatory signaling only"],
    correct:[1],
    rationale:"Chronic stress can maintain cortisol and sympathetic activation, contributing to hypertension, metabolic changes, and immune dysregulation.",
    plain:"Stress keeps the gas pedal floored (cortisol/adrenaline). Eventually, the engine overheats (high BP) and the maintenance crew (immune system) stops working.",
    mnemonic:"Cortisol = Chronic wear and tear."
  },
  { id:14, type:"sata", topic:"Stress & Disease (Nursing Priorities)", difficulty:2,
    stem:"Which nursing interventions best support a client experiencing stress-related symptoms? (Select all that apply)",
    options:[
      "Teach sleep hygiene and consistent bedtime routines",
      "Encourage caffeine to improve daytime alertness",
      "Introduce paced breathing or guided relaxation",
      "Assess for maladaptive coping (substance use, isolation)",
      "Tell the client stress is “all in their head”"
    ],
    correct:[0, 2, 3],
    rationale:"Practical coping supports (sleep hygiene, relaxation) and screening for maladaptive coping reduce harm and improve function.",
    plain:"Help them find the brake pedal. Deep breathing, sleep, and talking help reset the alarm system. Caffeine and 'tough love' just push the gas harder.",
    mnemonic:"Rest, Relax, Relate."
  },
  { id:15, type:"mcq", topic:"Cancer Biology (Oncogenes/Tumor Suppressors)", difficulty:3,
    stem:"A mutation removes a cell’s ability to stop the cell cycle when DNA damage is detected. This is most consistent with dysfunction of:",
    options:["Tumor suppressor genes", "Hemoglobin genes", "Mitochondrial rRNA", "Histamine receptors"],
    correct:[0],
    rationale:"Tumor suppressor genes normally limit proliferation and promote repair/apoptosis when damage occurs.",
    plain:"These genes are the brakes on a car. If you cut the brake lines (mutation), the cell speeds out of control and crashes (cancer).",
    mnemonic:"Suppressor = Stop sign."
  },
  { id:16, type:"mcq", topic:"Cancer Epidemiology (Risk Reduction)", difficulty:2,
    stem:"Which prevention strategy has the strongest evidence for reducing overall cancer risk at a population level?",
    options:["High-protein diet", "Tobacco cessation", "Daily antibiotics", "Avoiding all sunlight completely"],
    correct:[1],
    rationale:"Tobacco use is a major modifiable risk factor for multiple cancers; cessation reduces risk over time.",
    plain:"Smoking puts poison directly onto your cells over and over. Stopping is the single best way to stop damaging your DNA.",
    mnemonic:"Smoke = Stroke & Tumor."
  },
  { id:17, type:"mcq", topic:"Obesity (Pathophysiology)", difficulty:2,
    stem:"Which statement best describes why visceral (central) obesity is more strongly linked with metabolic complications than peripheral obesity?",
    options:[
      "Visceral fat is biologically inactive",
      "Visceral fat is associated with chronic inflammation and insulin resistance",
      "Peripheral fat produces no hormones",
      "Peripheral fat increases catecholamines continuously"
    ],
    correct:[1],
    rationale:"Visceral adiposity correlates with inflammatory cytokines, lipolysis, insulin resistance, and cardiometabolic risk.",
    plain:"Belly fat isn't just stored energy; it's an angry organ. It pumps out chemicals that cause inflammation and mess up how you handle sugar.",
    mnemonic:"Apple shape = Angry fat (Central)."
  },
  { id:18, type:"mcq", topic:"Starvation / Refeeding Syndrome", difficulty:3,
    stem:"A severely malnourished client starts nutrition support and develops weakness, arrhythmias, and confusion. Which complication is most likely?",
    options:["Ketoacidosis", "Refeeding syndrome", "Hypercalcemia", "Hemolysis"],
    correct:[1],
    rationale:"Refeeding syndrome can cause dangerous electrolyte shifts (especially phosphate) leading to cardiac/neurologic complications.",
    plain:"When a starving body gets food too fast, it grabs all the electrolytes from the blood to digest it. The heart and brain run dry and crash.",
    mnemonic:"Start Low, Go Slow (to save the flow)."
  },
 
  { id:19, type:"matrix", topic:"Immunizing Agents (Vaccine Types)", difficulty:2,
    stem:"Match the vaccine type to the best description.",
    rows:[
      { row:"Live attenuated", options:["Weakened organism; strong immune response", "Killed organism; cannot replicate", "Inactivated toxin", "Only a piece of pathogen"], correct:"Weakened organism; strong immune response" },
      { row:"Inactivated", options:["Weakened organism; strong immune response", "Killed organism; cannot replicate", "Inactivated toxin", "Only a piece of pathogen"], correct:"Killed organism; cannot replicate" },
      { row:"Toxoid", options:["Weakened organism; strong immune response", "Killed organism; cannot replicate", "Inactivated toxin", "Only a piece of pathogen"], correct:"Inactivated toxin" }
    ],
    rationale:"Vaccine types differ by what’s delivered (weakened/killed/toxin fragment/subunit) and how immunity is generated.",
    plain:"Live = Weakened zombie. Inactivated = Dead body. Toxoid = Disarmed weapon.",
    mnemonic:"Live = Alive (Attenuated). Toxoid = Toxin."
  },
  { id:20, type:"matrix", topic:"Alterations in Immunity (Defects)", difficulty:3,
    stem:"Match the immune problem to the most likely clinical pattern.",
    rows:[
      { row:"T-cell dysfunction", options:["Recurrent viral/fungal infections", "Bleeding/bruising", "Kidney stones", "Hypertension only"], correct:"Recurrent viral/fungal infections" },
      { row:"Antibody (B-cell) deficiency", options:["Recurrent bacterial sinopulmonary infections", "Seizures only", "Bone fractures only", "Hyperthyroidism"], correct:"Recurrent bacterial sinopulmonary infections" },
      { row:"Neutropenia", options:["High risk for severe bacterial/fungal infection", "Low risk for infection", "Only allergic reactions", "Only autoimmune thyroid disease"], correct:"High risk for severe bacterial/fungal infection" }
    ],
    rationale:"Different immune components protect against different pathogens; deficits create predictable infection patterns.",
    plain:"T-cells fight viruses/fungi. B-cells (antibodies) fight bacteria in lungs/sinuses. Neutrophils fight bacteria/fungi everywhere.",
    mnemonic:"T-cell = Tough viruses. B-cell = Bacteria."
  },
 
  { id:21, type:"bowtie", topic:"NGN Bowtie – Post-Vaccine Anaphylaxis", difficulty:3,
    stem:"Bowtie: Identify the risk, contributing factors, and nursing actions after an immunization.",
    center:{
      label:"Primary risk",
      options:["Anaphylaxis", "Dehydration", "Hyperglycemia", "Constipation"],
      correct:"Anaphylaxis"
    },
    left:{
      label:"Contributing cues (pick 2)", pick:2,
      options:["Wheezing/stridor", "Generalized hives", "Mild soreness at injection site", "Stable blood pressure"],
      correct:["Wheezing/stridor", "Generalized hives"]
    },
    right:{
      label:"Priority actions (pick 2)", pick:2,
      options:["Call for help and give IM epinephrine per protocol", "Place client supine and support airway/oxygen", "Send client home to rest", "Document later after the shift"],
      correct:["Call for help and give IM epinephrine per protocol", "Place client supine and support airway/oxygen"]
    },
    rationale:"Anaphylaxis is rapid and life-threatening; treat immediately with airway support and epinephrine per protocol.",
    plain:"Throat closing + Hives = Shock. Don't wait. Lay them down (blood to brain) and jab with Epi (open airway).",
    mnemonic:"Epipen for Epic reaction."
  },
  { id:22, type:"bowtie", topic:"NGN Bowtie – Neutropenic Sepsis Risk", difficulty:3,
    stem:"Bowtie: Identify the risk, contributing factors, and nursing actions for an immunocompromised client.",
    center:{
      label:"Primary risk",
      options:["Sepsis", "Hyponatremia", "Pressure injury", "Renal colic"],
      correct:"Sepsis"
    },
    left:{
      label:"Contributing cues (pick 2)", pick:2,
      options:["Fever with low ANC", "Chills and tachycardia", "Normal WBC/ANC", "No symptoms for 2 weeks"],
      correct:["Fever with low ANC", "Chills and tachycardia"]
    },
    right:{
      label:"Priority actions (pick 2)", pick:2,
      options:["Obtain cultures and start broad-spectrum antibiotics per protocol", "Initiate neutropenic precautions", "Delay antibiotics until imaging is complete", "Encourage raw fruits/vegetables immediately"],
      correct:["Obtain cultures and start broad-spectrum antibiotics per protocol", "Initiate neutropenic precautions"]
    },
    rationale:"Neutropenic fever is a medical emergency; timely cultures + antibiotics and infection prevention are key.",
    plain:"Fever with no white blood cells is a 5-alarm fire with no fire department. Start antibiotics NOW before the house burns down.",
    mnemonic:"Fever + Chemo = Emergency."
  },
 
  { id:23, type:"trends", topic:"NGN Trends – Chemo ANC/WBC", difficulty:3,
    stem:"Trends: A client receiving chemotherapy has the following labs. Interpret risk and priority action.",
    table:{
      headers:["Day", "WBC (K/µL)", "ANC (/µL)", "Temp (°F)"],
      rows:[
        ["Mon", "4.2", "1800", "98.6"],
        ["Tue", "3.1", "900", "99.1"],
        ["Wed", "2.4", "450", "100.6"],
        ["Thu", "2.0", "320", "101.2"]
      ]
    },
    parts:[
      { type:"mcq",
        prompt:"This trend most strongly indicates:",
        options:["Improving immune defense", "Rising neutropenic infection risk", "Normal post-chemo pattern with no action", "Allergic reaction only"],
        correct:[1]
      },
      { type:"sata",
        prompt:"Which actions are priority now? (Select all that apply)",
        options:["Notify provider for neutropenic fever protocol", "Initiate infection prevention precautions", "Encourage large group visitors for support", "Obtain ordered cultures/labs promptly", "Delay assessment until morning rounds"],
        correct:[0, 1, 3]
      }
    ],
    rationale:"ANC is dropping into severe neutropenia with fever: treat as neutropenic fever emergency.",
    plain:"White count is tanking and temp is rising. This is the danger zone. Stop everything and start the 'Neutropenic Fever' protocol.",
    mnemonic:"NADIR = Neutrophils Are Down, Infection Risk."
  },
  { id:24, type:"trends", topic:"NGN Trends – Obesity Metabolic Risk", difficulty:2,
    stem:"Trends: A client with central obesity is being monitored. Identify the most meaningful interpretation and next step.",
    table:{
      headers:["Visit", "Waist (in)", "Fasting Glucose", "BP"],
      rows:[
        ["1", "44", "102", "138/86"],
        ["2", "45", "110", "142/88"],
        ["3", "46", "118", "148/92"],
        ["4", "46", "126", "152/94"]
      ]
    },
    parts:[
      { type:"mcq",
        prompt:"The trend most strongly suggests:",
        options:["Improving cardiometabolic risk", "Worsening cardiometabolic risk pattern", "No clinical relevance", "Only dehydration"],
        correct:[1]
      },
      { type:"dropdown",
        prompt:"Best next nursing focus:",
        blanks:[
          { text:"Focus: ", options:["Lifestyle + risk-factor management counseling and follow-up", "Stop all activity to prevent fatigue", "Encourage skipping meals", "No follow-up needed"], correct:"Lifestyle + risk-factor management counseling and follow-up" }
        ]
      }
    ],
    rationale:"Rising waist, glucose, and BP reflect increasing metabolic risk; nursing focus is risk reduction and coordinated follow-up.",
    plain:"The belt is tighter, sugar is up, BP is up. This is 'Metabolic Syndrome' brewing. We need to fix the lifestyle before a heart attack happens.",
    mnemonic:"Metabolic Syndrome = Waist, BP, Glucose, Lipids (We B Globin')."
  },
 
  { id:25, type:"case", topic:"Unfolding Case – Immunizing Agents Clinic Visit", difficulty:2,
    stem:"A 28-year-old is at a vaccine clinic. They report a severe allergy to a prior vaccine component and are currently on high-dose steroids for an autoimmune flare.",
    steps:[
      { title:"Step 1 — Cue Recognition", type:"sata",
        prompt:"Which cues increase concern for vaccine-related harm? (Select all that apply)",
        options:["History of severe allergic reaction", "High-dose steroid therapy", "Mild anxiety about needles", "Normal vital signs", "Reports prior anaphylaxis symptoms"],
        correct:[0, 1, 4],
        rationale:"Severe allergy history and immunosuppression are major safety flags; prior anaphylaxis symptoms heighten risk.",
        plain:"If they had a bad reaction before, don't do it again. If they are on steroids, a live vaccine is risky. Stop and ask the boss.",
        mnemonic:"History predicts Future."
      },
      { title:"Step 2 — Best Action", type:"mcq",
        prompt:"What is the nurse’s best next action?",
        options:["Proceed and observe for 1 minute", "Hold vaccination and notify provider/clinic lead for guidance", "Tell the client to self-administer at home", "Give two vaccines to “get it over with”"],
        correct:[1],
        rationale:"Contraindications/cautions require escalation before administration.",
        plain:"Don't guess. If you see a red flag (allergy + steroids), STOP. Get orders.",
        mnemonic:"When in doubt, Hold it out."
      },
      { title:"Step 3 — Patient Teaching", type:"dropdown",
        prompt:"If vaccination proceeds later, the nurse should observe the client for at least:",
        blanks:[
          { text:"", options:["5 minutes", "15 minutes", "30 minutes", "2 hours"], correct:"15 minutes" }
        ],
        rationale:"Observation helps identify early severe reactions.",
        plain:"15 minutes is the standard safety window to catch instant bad reactions.",
        mnemonic:"15 minutes."
      }
    ]
  },
  { id:26, type:"case", topic:"Unfolding Case – Neutropenic Fever (Infection)", difficulty:3,
    stem:"A client on immunosuppressants presents with fever, chills, and fatigue. Labs show low ANC. The client looks ‘okay’ but reports feeling suddenly worse today.",
    steps:[
      { title:"Step 1 — Priority Interpretation", type:"mcq",
        prompt:"What is the priority interpretation?",
        options:["Likely mild viral illness; observe only", "High risk for rapid deterioration from infection", "Normal medication side effect only", "Anxiety-related symptoms"],
        correct:[1],
        rationale:"Immunosuppression + fever suggests high risk for serious infection/sepsis.",
        plain:"Even if they look okay, a fever in a chemo patient is a ticking bomb.",
        mnemonic:"Fever + Low ANC = Sepsis Risk."
      },
      { title:"Step 2 — Immediate Actions", type:"sata",
        prompt:"Which actions should the nurse anticipate/prepare to implement? (Select all that apply)",
        options:["Cultures/labs per protocol", "Early broad-spectrum antibiotics per protocol", "Delay care until confirmatory imaging", "Frequent vital signs and assessment for sepsis", "Encourage raw salad to ‘boost immunity’"],
        correct:[0, 1, 3],
        rationale:"Neutropenic fever: time-sensitive cultures + antibiotics; monitor closely for sepsis.",
        plain:"Get cultures (find the bug) and give antibiotics (kill the bug) ASAP. Monitor vital signs like a hawk.",
        mnemonic:"Culture then Kill (Antibiotics)."
      },
      { title:"Step 3 — Infection Prevention", type:"mcq",
        prompt:"Which visitor instruction is most appropriate?",
        options:["Anyone can visit if they wear perfume-free products", "Visitors with cold symptoms should not enter", "Bring fresh flowers to improve mood", "Share food from outside restaurants freely"],
        correct:[1],
        rationale:"Limit exposure to infectious sources; symptomatic visitors should not visit.",
        plain:"If a visitor sneezes, the patient could die. Keep sick people out.",
        mnemonic:"Sick visitors stay home."
      }
    ]
  },
  { id:27, type:"case", topic:"Unfolding Case – Biologic Immunosuppressant Teaching", difficulty:2,
    stem:"A client is starting a biologic for rheumatoid arthritis. They ask why they need screening and what side effects matter most.",
    steps:[
      { title:"Step 1 — Pre-treatment Safety", type:"mcq",
        prompt:"Which screening is most important before starting therapy?",
        options:["TB screening", "Color vision test", "Hearing test", "Bone density scan same day"],
        correct:[0],
        rationale:"Biologics can reactivate latent TB; screening is a key safety step.",
        plain:"Before we lower your shields, we need to make sure there isn't a sleeping enemy (TB) inside the castle.",
        mnemonic:"Screen before you Suppress."
      },
      { title:"Step 2 — High-Priority Teaching", type:"sata",
        prompt:"Which teaching points are priority? (Select all that apply)",
        options:["Report fever, cough, or sores that won’t heal", "Avoid live vaccines unless cleared", "Stop med abruptly if pain improves", "Hand hygiene and avoiding sick contacts", "Expect no infection risk change"],
        correct:[0, 1, 3],
        rationale:"Immunosuppressants increase infection risk; teach early reporting, prevention, and vaccine safety.",
        plain:"You are more likely to get sick. Wash hands, avoid sick people, and tell us if you get a fever.",
        mnemonic:"Prevention is key."
      },
      { title:"Step 3 — When to Seek Care", type:"mcq",
        prompt:"Which symptom should prompt urgent follow-up?",
        options:["Mild soreness after exercise", "New fever and shortness of breath", "Occasional dry skin", "Mild fatigue after a long day"],
        correct:[1],
        rationale:"Fever + respiratory symptoms in an immunosuppressed client can signal serious infection.",
        plain:"Fever + trouble breathing = serious infection. Go to the ER.",
        mnemonic:"Fever = Danger."
      }
    ]
  },
  { id:28, type:"case", topic:"Unfolding Case – Anorexia of Aging", difficulty:2,
    stem:"An 82-year-old reports ‘no appetite,’ early fullness, and unintentional weight loss. Family notes social isolation and multiple new medications.",
    steps:[
      { title:"Step 1 — Key Risk", type:"mcq",
        prompt:"What is the most important risk associated with anorexia of aging?",
        options:["Improved longevity", "Undernutrition leading to functional decline", "Increased muscle mass", "Decreased fall risk"],
        correct:[1],
        rationale:"Anorexia of aging can lead to undernutrition, frailty, and higher morbidity/mortality.",
        plain:"If Grandpa doesn't eat, he gets weak. Weakness leads to falls and broken hips.",
        mnemonic:"No Food = No Function."
      },
      { title:"Step 2 — Contributors", type:"sata",
        prompt:"Which factors commonly contribute? (Select all that apply)",
        options:["Diminished taste/smell", "Poor dentition", "Social isolation", "Increased hunger signals with age", "Medication effects"],
        correct:[0, 1, 2, 4],
        rationale:"Sensory decline, dentition issues, meds, and social factors commonly reduce intake in older adults.",
        plain:"He can't taste it, his teeth hurt, he's lonely, and his meds make him nauseous. No wonder he isn't eating.",
        mnemonic:"MEALS on Wheels (Meds, Emotional, Anorexia, Late life, Swallowing)."
      },
      { title:"Step 3 — Nursing Action", type:"mcq",
        prompt:"Best next nursing action?",
        options:["Advise fasting to reset appetite", "Assess nutrition, meds, and social supports; coordinate follow-up", "Tell family it’s normal and no action is needed", "Encourage only one large meal per day"],
        correct:[1],
        rationale:"Assess reversible causes and connect to nutrition/social supports to reduce harm.",
        plain:"Find the cause (teeth? meds? loneliness?) and fix it. Don't just ignore it.",
        mnemonic:"Assess and Address."
      }
    ]
  },
 
  { id:29, type:"mcq", topic:"Cancer Biology (Metastasis)", difficulty:2,
    stem:"Which finding best suggests a malignant tumor rather than a benign tumor?",
    options:["Encapsulation and slow growth", "Local invasion into surrounding tissue", "Cells closely resemble normal tissue", "No recurrence after excision"],
    correct:[1],
    rationale:"Malignancy is associated with invasion and the potential for metastasis.",
    plain:"Benign tumors stay in their lane (encapsulated). Malignant tumors break fences and invade neighbors (invasion/metastasis).",
    mnemonic:"Malignant = Means to Move."
  },
  { id:30, type:"mcq", topic:"Cancer Epidemiology (Screening Concept)", difficulty:2,
    stem:"A community program aims to reduce cancer deaths. Which approach most directly supports early detection?",
    options:["Secondary prevention (screening)", "Primary prevention only", "Tertiary rehabilitation only", "Palliative care only"],
    correct:[0],
    rationale:"Screening is secondary prevention and supports earlier detection and treatment.",
    plain:"Primary = Prevention (Sunscreen). Secondary = Screening (Mammogram). Tertiary = Treatment (Rehab).",
    mnemonic:"Secondary = Screening."
  }
];
 
/* =========================
  State + Storage
  ========================= */
const LS_KEY = "unit2_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();
}
 
/* =========================
  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() };
}
 
// Ensure payload reader exists
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 (Uses custom 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>`;
 
  // Custom fields
  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>
  `;
}
 
/* =========================
  Topic Aggregation
  ========================= */
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;
}
 
/* =========================
  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;
 
  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 2 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