Game:520 Unit 1 Study Buddy: Difference between revisions

From Doc-Wiki
Jump to navigation Jump to search
Added Category:HU NSG 520 Pathophysiology and Pharmacology
Forcing wikitext content model
 
(8 intermediate revisions by the same user not shown)
Line 1: Line 1:
<!-- Embedded HTML Study Application -->
{{#widget:520Unit1StudyBuddy}}
<html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Unit 1 ONLY</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 1 Study Buddy</h1>
    <div class="sub">
      Unit 1 concept domains (Altered Cellular/Tissue Biology + injury/death + toxins/infection/inflammation + radiation/pressure + aging/frailty + genetics/epigenetics + core math safety).
      <br/>No Unit 2 or dosage calc 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,  // <-- ONLY CHANGE: benchmark for passing
  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) – PATHO/THEORY
  ========================= */
const QUESTIONS = [
  { id:1, type:"mcq", topic:"Cellular Adaptation", difficulty:2,
    stem:"A client has chronic GERD. Endoscopy notes that the distal esophagus now has columnar-appearing epithelium. Which adaptive change best fits this finding?",
    options:["Hyperplasia","Metaplasia","Atrophy","Apoptosis"],
    correct:[1],
    rationale:"Metaplasia is a reversible replacement of one differentiated cell type with another better suited to chronic stress (e.g., acid exposure).",
    plain:"The esophagus cells changed their 'outfit' to handle the acid better. It's not cancer yet, just a swap for protection.",
    mnemonic:"Metaplasia = Morphing (changing shape/type)."
  },
  { id:2, type:"mcq", topic:"Dysplasia", difficulty:2,
    stem:"A cervical biopsy report mentions dysplasia. Which statement best describes dysplasia?",
    options:[
      "A normal, expected increase in cell size with exercise",
      "A reversible change where one mature cell type is replaced by another",
      "Disordered growth with abnormal size/shape/organization that can precede cancer",
      "A programmed cell death process that prevents inflammation"
    ],
    correct:[2],
    rationale:"Dysplasia involves disorganized, atypical cellular growth and can be a precursor to malignancy depending on severity and persistence.",
    plain:"The cells are looking weird, messy, and disorganized. This is the 'warning zone' before actual cancer.",
    mnemonic:"Dysplasia = Disordered (Bad growth)."
  },
  { id:3, type:"mcq", topic:"Ischemic/Hypoxic Injury", difficulty:2,
    stem:"During a prolonged hypoxic event, which cellular change most directly contributes to early cell swelling?",
    options:["Activation of DNA repair enzymes","Failure of ATP-dependent ion pumps","Increased collagen synthesis","Immediate lysosomal rupture"],
    correct:[1],
    rationale:"ATP depletion impairs ion pumps (especially Na+/K+ ATPase), causing Na+ and water influx and swelling.",
    plain:"No oxygen = No energy (ATP). Without energy, the bilge pump stops working, water floods the ship (cell), and it swells.",
    mnemonic:"No Air? Sodium's There (and water follows)."
  },
  { id:4, type:"mcq", topic:"Ischemia–Reperfusion Injury", difficulty:3,
    stem:"After return of blood flow to ischemic tissue, which mechanism most contributes to additional injury?",
    options:["Decreased inflammatory signaling","Reactive oxygen species generation and inflammation","Permanent restoration of membrane stability","Reduced intracellular calcium"],
    correct:[1],
    rationale:"Reperfusion can trigger ROS bursts and inflammatory cascades that extend tissue damage beyond the initial ischemia.",
    plain:"Bringing oxygen back too fast is like fanning a fire. It creates 'sparks' (free radicals) that cause fresh damage.",
    mnemonic:"Reperfusion = Rust (Oxidative stress)."
  },
  { id:5, type:"mcq", topic:"Oxidative Stress", difficulty:2,
    stem:"Which clinical explanation best connects oxidative stress to cellular injury?",
    options:[
      "Reactive species improve enzyme efficiency",
      "Reactive species can damage lipids, proteins, and DNA",
      "Oxidative stress prevents cell death in severe injury",
      "Oxidative stress only affects extracellular fluid"
    ],
    correct:[1],
    rationale:"ROS can cause lipid peroxidation, protein modification, and DNA injury, disrupting cell function and viability.",
    plain:"Free radicals are like bulls in a china shop—they smash cell walls (lipids) and tear up the instruction manual (DNA).",
    mnemonic:"ROS = Rotting Our Structures."
  },
  { id:6, type:"mcq", topic:"Chemical/Toxic Injury", difficulty:2,
    stem:"A client is brought in after exposure to a high concentration of household ammonia in a poorly ventilated space. Which immediate nursing concern most closely matches this toxin exposure pattern?",
    options:["Delayed joint inflammation only","Airway irritation/bronchospasm and impaired oxygenation","Purely neurologic injury without respiratory effects","Urinary retention due to smooth muscle hypertrophy"],
    correct:[1],
    rationale:"Many inhaled irritants cause acute airway inflammation/bronchospasm, leading to oxygenation compromise.",
    plain:"Inhaling strong chemicals burns the lungs. The airway swells shut to protect itself, blocking air.",
    mnemonic:"Ammonia = Airway (A-A)."
  },
  { id:7, type:"mcq", topic:"Infectious Injury", difficulty:2,
    stem:"A client with neutropenia develops a new fever and chills. Which pathophysiologic concept best explains why this client can deteriorate quickly?",
    options:["Enhanced adaptive immunity with rapid antibody formation","Reduced first-line cellular defenses against infection","Decreased oxidative stress during infection","Increased ability to form granulomas rapidly"],
    correct:[1],
    rationale:"Neutropenia reduces critical innate cellular defenses, increasing risk for rapid progression of infection.",
    plain:"No soldiers (neutrophils) means the enemy (bacteria) takes over fast, with no alarm bells (pus/swelling) ringing.",
    mnemonic:"Neutropenia = No Protection."
  },
  { id:8, type:"mcq", topic:"Immunologic/Inflammatory Injury", difficulty:2,
    stem:"A client develops widespread hives and wheezing minutes after receiving IV contrast. Which mechanism best explains the immediate clinical risk?",
    options:["Gradual fibrosis from chronic injury","Acute immune-mediated mediator release affecting airway and circulation","Atrophy of respiratory epithelium","Coagulative necrosis of bronchioles"],
    correct:[1],
    rationale:"Acute hypersensitivity can cause mediator release leading to bronchospasm, airway edema, and circulatory collapse.",
    plain:"The immune system overreacts instantly. It dumps chemicals that close the throat and drop blood pressure.",
    mnemonic:"Anaphylaxis = Airway & Arteries (both close/drop)."
  },
  { id:9, type:"mcq", topic:"Ionizing Radiation", difficulty:2,
    stem:"Which statement best explains why ionizing radiation can increase long-term cancer risk?",
    options:["It always causes immediate cell swelling only","It can cause DNA damage and mutations","It permanently prevents cell division in all tissues","It only affects skin cells and never internal organs"],
    correct:[1],
    rationale:"Ionizing radiation can damage DNA, increasing mutation risk and potential malignant transformation.",
    plain:"Radiation zaps the cell's instruction manual (DNA). If the manual is rewritten wrong, the cell might grow uncontrollably (cancer).",
    mnemonic:"Radiation = Rewriting DNA."
  },
  { id:10, type:"mcq", topic:"Aging/Frailty", difficulty:2,
    stem:"Which clinical pattern best supports frailty (as a syndrome) rather than normal aging alone?",
    options:["Stable chronic disease with independent ADLs","Unintentional weight loss, weakness, slowed gait, and low activity","Mild presbyopia requiring new glasses","Occasional constipation"],
    correct:[1],
    rationale:"Frailty commonly includes weight loss, weakness, low activity, exhaustion, and slowed performance, increasing vulnerability to stressors.",
    plain:"Frailty isn't just getting old; it's running out of gas. A small bump (illness) can knock them down like a sledgehammer.",
    mnemonic:"Frailty = Fragile (Handle with care)."
  },
 
  { id:11, type:"sata", topic:"Irreversible Cell Injury", difficulty:3,
    stem:"Which findings most strongly suggest irreversible cellular injury? (Select all that apply)",
    options:[
      "Loss of plasma membrane integrity",
      "Marked mitochondrial dysfunction with inability to restore ATP",
      "Mild cell swelling that resolves after oxygen is restored",
      "Nuclear fragmentation (karyorrhexis)",
      "Ribosomes detach temporarily and later reattach"
    ],
    correct:[0,1,3],
    rationale:"Irreversible injury is suggested by membrane disruption, severe mitochondrial failure, and nuclear breakdown.",
    plain:"If the shell breaks (membrane), the engine dies (mitochondria), or the brain dissolves (nucleus), the cell is gone for good.",
    mnemonic:"The 3 M's of Death: Membrane, Mitochondria, Melting Nucleus."
  },
  { id:12, type:"sata", topic:"Cellular Manifestations: Accumulations", difficulty:2,
    stem:"Which intracellular accumulations can occur with injury or metabolic derangements? (Select all that apply)",
    options:["Lipids","Glycogen","Proteins","Calcium","Oxygen molecules stored in lysosomes"],
    correct:[0,1,2,3],
    rationale:"Common abnormal accumulations include lipids, glycogen, proteins, and calcium; oxygen is not stored as a lysosomal “accumulation.”",
    plain:"Sick cells act like hoarders. They pile up fat, sugar, proteins, or calcium because they can't clean house effectively.",
    mnemonic:"Cells Hoard Junk (Ca, Fat, Protein)."
  },
  { id:13, type:"sata", topic:"Necrosis vs Apoptosis", difficulty:2,
    stem:"Which findings align more with necrosis than apoptosis? (Select all that apply)",
    options:[
      "Cell swelling and membrane rupture",
      "Inflammation in surrounding tissue",
      "Orderly cell shrinkage with minimal inflammation",
      "Leakage of intracellular enzymes into blood",
      "Formation of membrane-bound apoptotic bodies"
    ],
    correct:[0,1,3],
    rationale:"Necrosis involves swelling, membrane rupture, enzyme leakage, and inflammatory response.",
    plain:"Necrosis is a messy murder scene—cells explode and cause inflammation. Apoptosis is a tidy, planned exit.",
    mnemonic:"Necrosis = Nasty/Messy. Apoptosis = Polite/Planned."
  },
  { id:14, type:"sata", topic:"Environmental Toxins: Nursing Risk Reduction", difficulty:2,
    stem:"A nurse is teaching about reducing exposure to common environmental toxins at home. Which recommendations are appropriate? (Select all that apply)",
    options:[
      "Ensure adequate ventilation when using cleaning chemicals",
      "Store chemicals in original containers and out of children’s reach",
      "Mix bleach and ammonia for stronger cleaning",
      "Use gloves/eye protection when indicated on labels",
      "Ignore label warnings if symptoms don’t appear immediately"
    ],
    correct:[0,1,3],
    rationale:"Ventilation, safe storage, and PPE reduce risk; mixing chemicals can create toxic gases; label warnings matter even without immediate symptoms.",
    plain:"Don't mix chemicals (it makes poison gas), wear gear, and open a window. Safety 101.",
    mnemonic:"Vent, Vest (PPE), and Verify (labels)."
  },
  { id:15, type:"sata", topic:"Genetics & Epigenetics", difficulty:3,
    stem:"Which statements about epigenetics are accurate? (Select all that apply)",
    options:[
      "Epigenetics can alter gene expression without changing the DNA sequence",
      "DNA methylation and histone modification are epigenetic mechanisms",
      "Epigenetic changes are fixed and cannot shift across life",
      "miRNAs can influence gene expression regulation",
      "Epigenetics is unrelated to disease risk"
    ],
    correct:[0,1,3],
    rationale:"Epigenetic regulation can change over time and influences development and disease risk via multiple mechanisms.",
    plain:"The DNA book stays the same, but epigenetics uses sticky notes to say 'Skip this page' or 'Read this loudly.'",
    mnemonic:"Epi = Above (Above the genes)."
  },
  { id:16, type:"sata", topic:"Math Safety Foundations", difficulty:2,
    stem:"Which habits reduce calculation-related medication errors? (Select all that apply)",
    options:[
      "Convert units to a single system before solving",
      "Estimate whether your final answer makes sense clinically",
      "Skip unit labels to save time",
      "Recheck decimal placement before finalizing",
      "Use a second check for high-alert meds when available"
    ],
    correct:[0,1,3,4],
    rationale:"Unit consistency, estimation, decimal safety, and double-checks reduce error risk.",
    plain:"Math errors kill. Check your units, watch your decimals, and ask 'Does this dose make sense?'",
    mnemonic:"Stop and Smell the Roses (Reasonableness, Order, Safety)."
  },
 
  { id:17, type:"dropdown", topic:"Cellular Adaptation", difficulty:2,
    stem:"Choose the best term for each situation.",
    blanks:[
      { text:"Decreased muscle bulk after prolonged bedrest: ", options:["Atrophy","Hypertrophy","Hyperplasia","Metaplasia"], correct:"Atrophy" },
      { text:"Increased glandular tissue in pregnancy to prepare for lactation: ", options:["Hyperplasia","Atrophy","Necrosis","Calcification"], correct:"Hyperplasia" }
    ],
    rationale:"Atrophy = decreased size; hyperplasia = increased number of cells (common in hormonally stimulated tissues).",
    plain:"Use it or lose it (Atrophy). Pregnant body needs MORE machinery, so it builds more cells (Hyperplasia).",
    mnemonic:"A-trophy (Absent trophy = small). Hyper-plasia (Plus cells)."
  },
  { id:18, type:"dropdown", topic:"Ischemia/Hypoxia", difficulty:2,
    stem:"Complete the statement.",
    blanks:[
      { text:"Early hypoxic cell injury often starts with ATP depletion leading to failure of ", options:["DNA replication","ATP-dependent ion pumps","Collagen synthesis","Antibody formation"], correct:"ATP-dependent ion pumps" }
    ],
    rationale:"ATP depletion impairs ion gradients, producing swelling and dysfunction.",
    plain:"No fuel (ATP) -> The pump stops -> Water floods the boat.",
    mnemonic:"No ATP = Pump Fail = Swell."
  },
  { id:19, type:"dropdown", topic:"Epigenetic Mechanisms", difficulty:3,
    stem:"Match mechanism to description.",
    blanks:[
      { text:"Adding methyl groups to DNA to affect expression: ", options:["DNA methylation","Coagulative necrosis","Ion channel upregulation","Fibrosis"], correct:"DNA methylation" },
      { text:"Small RNAs that can suppress translation: ", options:["miRNAs","Neutrophils","Keratin","Urate"], correct:"miRNAs" }
    ],
    rationale:"DNA methylation and miRNAs are classic gene-expression regulators without changing DNA sequence.",
    plain:"Methylation = Muting the gene. miRNA = Micro-managers blocking the message.",
    mnemonic:"Methylation Mutes."
  },
  { id:20, type:"dropdown", topic:"Math Foundations", difficulty:2,
    stem:"Select the correct option.",
    blanks:[
      { text:"A ratio compares ", options:["Two quantities","Only time","Only temperature","Only oxygen"], correct:"Two quantities" },
      { text:"A proportion means two ratios are ", options:["Unrelated","Equal","Always smaller","Always larger"], correct:"Equal" }
    ],
    rationale:"Ratios compare quantities; proportions set two ratios equal (common in dosage setups).",
    plain:"Ratio is This vs. That. Proportion is saying 'This match is equal to That match.'",
    mnemonic:"Proportion = Perfectly Equal."
  },
 
  { id:21, type:"case", topic:"Unfolding Case: Hypoxia → Injury", difficulty:3,
    stem:"A 67-year-old with COPD arrives with increasing dyspnea. VS: RR 30, HR 118, SpO2 86% RA, using accessory muscles. ABG pending.",
    steps:[
      { title:"Step 1 — Cues", type:"sata",
        prompt:"Which cues suggest high risk for hypoxic cellular injury? (Select all that apply)",
        options:["SpO2 86% on room air","RR 30 with accessory use","HR 118","New mild ankle edema only","Dyspnea with fatigue"],
        correct:[0,1,2,4],
        rationale:"Severe hypoxemia and signs of increased work of breathing with tachycardia and fatigue indicate oxygen delivery mismatch.",
        plain:"Look for signs of struggle: Fast breathing, fast heart, low oxygen. The engine is running hot but getting no air.",
        mnemonic:"Air Hunger = Cell Danger."
      },
      { title:"Step 2 — Analysis", type:"mcq",
        prompt:"Which cellular change is most likely early if hypoxia persists?",
        options:["ATP depletion leading to ion pump failure and swelling","Immediate nuclear fragmentation","Instant fibrosis of alveoli","Urate crystal deposition in lungs"],
        correct:[0],
        rationale:"Early reversible injury often begins with ATP depletion and ion pump dysfunction.",
        plain:"If the air cuts off, the first thing to go is the battery power (ATP). Then the cell swells.",
        mnemonic:"Hypoxia -> No ATP -> Swell."
      },
      { title:"Step 3 — Action", type:"dropdown",
        prompt:"Choose the best immediate nursing priority.",
        blanks:[
          { text:"Priority: ", options:["Apply oxygen and reassess response","Delay interventions until ABG returns","Encourage oral fluids","Provide sedative first"], correct:"Apply oxygen and reassess response" }
        ],
        rationale:"Immediate oxygenation support and reassessment are key to preventing progression to irreversible injury.",
        plain:"Don't wait for lab results when the patient is suffocating. Give O2 now.",
        mnemonic:"Airway First."
      }
    ]
  },
  { id:22, type:"case", topic:"Unfolding Case: Reperfusion Red Flags", difficulty:3,
    stem:"A client undergoes revascularization for acute limb ischemia. Post-op baseline pulses were present. Two hours later: escalating pain, tight swelling, numbness.",
    steps:[
      { title:"Step 1 — Cues", type:"sata",
        prompt:"Which cues raise concern for emergent complication? (Select all that apply)",
        options:["Escalating pain out of proportion","Tense swelling","New numbness/tingling","Pulses weaker than baseline","Comfortable pain control with stable sensation"],
        correct:[0,1,2,3],
        rationale:"Pain out of proportion, neuro changes, swelling, and decreasing pulses suggest compartment syndrome/neurovascular compromise risk.",
        plain:"Pain that meds won't touch + tight skin + numbness = BAD. The limb is under pressure.",
        mnemonic:"The 5 P's: Pain, Pallor, Pulselessness, Paresthesia, Paralysis."
      },
      { title:"Step 2 — Analysis", type:"mcq",
        prompt:"Which mechanism can worsen injury after blood flow is restored?",
        options:["Reactive oxygen species generation and inflammation","Complete shutdown of inflammatory mediators","Permanent normalization of calcium balance","Immediate collagen remodeling preventing edema"],
        correct:[0],
        rationale:"Reperfusion can trigger ROS/inflammation that extends damage.",
        plain:"The return of blood flow caused a 'flash fire' of inflammation (ROS), making the swelling worse.",
        mnemonic:"Reperfusion = Re-injury risk."
      },
      { title:"Step 3 — Action", type:"dropdown",
        prompt:"Best nursing response now:",
        blanks:[
          { text:"Response: ", options:[
            "Document and recheck in 1 hour",
            "Notify provider urgently and perform frequent neurovascular reassessments",
            "Encourage ambulation to improve circulation",
            "Apply heat and elevate above heart aggressively"
          ], correct:"Notify provider urgently and perform frequent neurovascular reassessments" }
        ],
        rationale:"This is time-sensitive. Escalate and reassess neurovascular status frequently.",
        plain:"This is an emergency (Compartment Syndrome). Call the provider immediately.",
        mnemonic:"Time is Tissue."
      }
    ]
  },
  { id:23, type:"case", topic:"Unfolding Case: Hypersensitivity Injury", difficulty:2,
    stem:"A client starts a new antibiotic. On day 6 they report fever, diffuse rash, and joint aches. No shortness of breath currently.",
    steps:[
      { title:"Step 1 — Cues", type:"mcq",
        prompt:"This pattern is most consistent with:",
        options:["Immunologic (hypersensitivity) injury","Pure ischemic injury","Ionizing radiation injury","Mechanical pressure injury"],
        correct:[0],
        rationale:"Fever, rash, and arthralgias after a new med suggests immune-mediated drug reaction.",
        plain:"New drug + rash + fever = The immune system hates the drug.",
        mnemonic:"Rash + Fever = Drug Reaction?"
      },
      { title:"Step 2 — Analysis", type:"sata",
        prompt:"Which assessments help identify escalation risk? (Select all that apply)",
        options:["Airway symptoms (wheezing, throat tightness)","Mucosal involvement (oral lesions)","Vital signs trends","Cap refill of toes only","Extent/progression of rash"],
        correct:[0,1,2,4],
        rationale:"Airway/mucosal involvement and worsening vitals/rash progression suggest higher severity.",
        plain:"Check the ABCs. Is the throat closing? Is the skin peeling (mucosa)? Is the rash spreading fast?",
        mnemonic:"Watch the Airway & Mucosa."
      },
      { title:"Step 3 — Action", type:"dropdown",
        prompt:"Choose the best next step.",
        blanks:[
          { text:"Next step: ", options:[
            "Continue medication to confirm it’s the cause",
            "Hold the suspected agent per protocol and notify provider",
            "Tell the client to stop all fluids",
            "Ignore unless blisters appear"
          ], correct:"Hold the suspected agent per protocol and notify provider" }
        ],
        rationale:"Stop/hold suspected trigger and escalate to prevent progression.",
        plain:"Stop the drug immediately. Don't add fuel to the fire.",
        mnemonic:"Stop the Trigger."
      }
    ]
  },
  { id:24, type:"case", topic:"Unfolding Case: Aging/Frailty Risk", difficulty:2,
    stem:"An 82-year-old lives alone and reports recent unintentional weight loss and fatigue. They walk slower and avoid stairs now. No acute illness today.",
    steps:[
      { title:"Step 1 — Cues", type:"sata",
        prompt:"Which cues support frailty risk? (Select all that apply)",
        options:["Unintentional weight loss","Slowed gait","Fatigue/exhaustion","Avoiding activity due to weakness","Occasional reading glasses use"],
        correct:[0,1,2,3],
        rationale:"Frailty patterns include weight loss, weakness, exhaustion, slow gait, and low activity.",
        plain:"The engine is sputtering: losing weight, moving slow, no energy. This isn't just 'old age'.",
        mnemonic:"Fried's Phenotype (Weight, Exhaustion, Grip, Speed, Activity)."
      },
      { title:"Step 2 — Analysis", type:"mcq",
        prompt:"Why does frailty matter clinically?",
        options:[
          "It only affects mood, not outcomes",
          "It increases vulnerability to stressors (infection, surgery, falls)",
          "It guarantees cancer within 1 year",
          "It is identical to normal aging"
        ],
        correct:[1],
        rationale:"Frailty increases vulnerability and worsens outcomes after physiologic stress.",
        plain:"A frail person is like a house of cards. One small gust of wind (illness/fall) can knock the whole thing down.",
        mnemonic:"Frailty = Zero Reserve."
      },
      { title:"Step 3 — Action", type:"dropdown",
        prompt:"Best nursing planning focus?",
        blanks:[
          { text:"Focus: ", options:[
            "Ignore because vitals are stable",
            "Screen nutrition, strength/mobility, and safety needs; coordinate supports",
            "Only provide medication education",
            "Encourage maximal exertion without rest"
          ], correct:"Screen nutrition, strength/mobility, and safety needs; coordinate supports" }
        ],
        rationale:"Frailty care emphasizes risk reduction, functional support, and coordinated interventions.",
        plain:"Build them up. Get nutrition on board, check safety, and get help at home.",
        mnemonic:"Support the Foundation."
      }
    ]
  },
 
  { id:25, type:"matrix", topic:"Adaptation vs Death: Pattern ID", difficulty:2,
    stem:"Match each scenario to the best concept.",
    rows:[
      { row:"Chronic airway irritation leads to more protective epithelial type", options:["Metaplasia","Hypertrophy","Atrophy","Necrosis"], correct:"Metaplasia" },
      { row:"Increased number of endometrial glands due to hormonal stimulation", options:["Hyperplasia","Apoptosis","Calcification","Atrophy"], correct:"Hyperplasia" },
      { row:"Programmed cell removal during normal tissue turnover", options:["Apoptosis","Necrosis","Urate accumulation","Fibrosis"], correct:"Apoptosis" },
      { row:"Tissue death with enzyme leakage and inflammation", options:["Necrosis","Hyperplasia","Metaplasia","Autophagy"], correct:"Necrosis" }
    ],
    rationale:"Matrix checks recognition of hallmark patterns rather than memorized examples.",
    plain:"Metaplasia = Swap. Hyperplasia = More cells. Apoptosis = Clean death. Necrosis = Messy death.",
    mnemonic:"Meta (Change). Hyper (More). Apo (Pop/Gone). Necro (Dead/Rot)."
  },
  { id:26, type:"matrix", topic:"Cell Injury Mechanisms", difficulty:3,
    stem:"Match mechanism to best description.",
    rows:[
      { row:"ATP depletion", options:["Ion pump failure and swelling","Immediate antibody production","Collagen cross-linking","Urate precipitation"], correct:"Ion pump failure and swelling" },
      { row:"Reactive oxygen species", options:["Lipid/protein/DNA injury","Guaranteed tissue regeneration","Only helpful signaling","Prevents inflammation"], correct:"Lipid/protein/DNA injury" },
      { row:"Ischemia–reperfusion", options:["ROS + inflammatory extension of injury","Instant full recovery of tissue","Only affects skin","Stops calcium entry completely"], correct:"ROS + inflammatory extension of injury" }
    ],
    rationale:"Mechanism matching strengthens cue→patho→outcome reasoning.",
    plain:"No ATP? Swelling. Free radicals? Structural damage. Reperfusion? Second wave of injury.",
    mnemonic:"ATP = Pump. ROS = Damage. Reperfusion = Inflammation."
  },
 
  { id:27, type:"bowtie", topic:"NGN Bowtie: Hypoxic Injury", difficulty:3,
    stem:"Bowtie: Identify the likely problem, contributing factors, and priority actions.",
    center:{ label:"Most likely problem", options:["Hypoxic/ischemic cellular injury","Autoimmune flare","Primary endocrine disorder","Urate arthropathy"], correct:"Hypoxic/ischemic cellular injury" },
    left:{ label:"Contributing factors (pick 2)", pick:2, options:[
      "Low oxygen saturation with increased work of breathing",
      "Reduced perfusion/hypotension",
      "Recent vaccination",
      "Increased sunlight exposure"
    ], correct:["Low oxygen saturation with increased work of breathing","Reduced perfusion/hypotension"] },
    right:{ label:"Priority actions (pick 2)", pick:2, options:[
      "Support oxygenation/perfusion and reassess response",
      "Monitor for deterioration and escalate early",
      "Delay assessment to avoid anxiety",
      "Encourage fluid restriction as first-line for hypoxia"
    ], correct:["Support oxygenation/perfusion and reassess response","Monitor for deterioration and escalate early"] },
    rationale:"Correct center + cause/action pairing reflects NGN integrated reasoning under pressure.",
    plain:"Problem: Cells starving for air. Causes: Low blood flow or low lung function. Fix: Give air and fix flow.",
    mnemonic:"Hypoxia Kills Cells."
  },
  { id:28, type:"bowtie", topic:"NGN Bowtie: Hypersensitivity Reaction", difficulty:2,
    stem:"Bowtie: Identify the likely problem, contributing factors, and priority actions.",
    center:{ label:"Most likely problem", options:["Immunologic (hypersensitivity) injury","Reperfusion injury","Radiation injury","Pressure injury"], correct:"Immunologic (hypersensitivity) injury" },
    left:{ label:"Contributing factors (pick 2)", pick:2, options:[
      "New medication exposure",
      "Rash with fever/joint symptoms",
      "Weeks of immobilization",
      "High altitude exposure only"
    ], correct:["New medication exposure","Rash with fever/joint symptoms"] },
    right:{ label:"Priority actions (pick 2)", pick:2, options:[
      "Hold suspected trigger per protocol and notify provider",
      "Assess airway/breathing and severity; monitor vitals",
      "Encourage repeat dosing to confirm",
      "Ignore unless pain is present"
    ], correct:["Hold suspected trigger per protocol and notify provider","Assess airway/breathing and severity; monitor vitals"] },
    rationale:"Drug hypersensitivity requires rapid severity assessment and escalation to prevent progression.",
    plain:"Problem: Bad reaction to drug. Signs: Rash/Fever. Action: STOP the drug and watch breathing.",
    mnemonic:"See Rash? Dash (to stop the med)."
  },
 
  { id:29, type:"trends", topic:"NGN Trends: Infection Deterioration", difficulty:3,
    stem:"Trends: A client is being monitored for infection. Identify when escalation is most indicated.",
    table:{
      headers:["Time","Temp (°F)","HR","BP","RR","SpO2"],
      rows:[
        ["0h", "99.3", "94",  "122/76", "18", "96%"],
        ["2h", "100.6","108", "112/70", "20", "95%"],
        ["4h", "101.8","118", "100/64", "24", "93%"],
        ["6h", "102.4","128", "90/56",  "28", "92%"]
      ]
    },
    parts:[
      { type:"mcq",
        prompt:"At which time point does the trend most strongly indicate deterioration requiring escalation?",
        options:["0h","2h","4h","6h"],
        correct:[3]
      },
      { type:"sata",
        prompt:"Which changes are most concerning? (Select all that apply)",
        options:["Rising temperature","Increasing HR","Dropping BP","Increasing RR","Improving oxygenation"],
        correct:[0,1,2,3]
      }
    ],
    rationale:"Worsening fever with tachycardia, hypotension, tachypnea, and falling SpO2 suggests escalating systemic compromise.",
    plain:"Everything is going the wrong way. Heart is racing, BP is crashing, fever is spiking. This is sepsis territory.",
    mnemonic:"Sepsis = Hypotension + Tachycardia."
  },
  { id:30, type:"trends", topic:"NGN Trends: Neurovascular Compromise", difficulty:3,
    stem:"Trends: A client after limb revascularization is reassessed over time.",
    table:{
      headers:["Time","Pain (0–10)","Pulses","Cap Refill","Sensation","Swelling"],
      rows:[
        ["PACU", "2", "Present", "<2s", "Intact", "Mild"],
        ["1h",  "4", "Present", "2–3s","Tingling","Moderate"],
        ["2h",  "6", "Weaker",  "3–4s","Numbness","Increasing"],
        ["3h",  "8", "Faint",  ">4s", "Reduced", "Tense"]
      ]
    },
    parts:[
      { type:"mcq",
        prompt:"Which trend change is the strongest trigger to escalate immediately?",
        options:["Pain 4 at 1h","Moderate swelling at 1h","Faint pulses with delayed cap refill at 3h","Mild swelling in PACU"],
        correct:[2]
      },
      { type:"dropdown",
        prompt:"Choose the best immediate nursing response.",
        blanks:[
          { text:"Best response: ", options:[
            "Continue routine checks",
            "Notify provider urgently and perform frequent neurovascular reassessments",
            "Encourage walking to relieve pain",
            "Apply heat and reassess later"
          ], correct:"Notify provider urgently and perform frequent neurovascular reassessments" }
        ]
      }
    ],
    rationale:"Progressive neurovascular compromise after reperfusion is time-sensitive and requires urgent escalation.",
    plain:"The pulse is disappearing and the pain is skyrocketing. The limb is dying. Scream for help.",
    mnemonic:"Lost Pulse = Lost Limb."
  }
];
 
/* =========================
  State + Storage (Moved to top 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();
}
 
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)
  ========================= */
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>`;
 
  const nursingLogic = q.rationale || "Rationale not available.";
  const plainLogic = q.plain || "Plain explanation not available.";
  const mnemonic = q.mnemonic || "No mnemonic for this topic.";
 
  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]]
[[Category:HU NSG 520 Pathophysiology and Pharmacology]]

Latest revision as of 22:27, 17 January 2026