Here is a comprehensive guide to 10 common heart diseases with their characteristic ECG changes and illustrative ECG images.

Reading File
Now let me run parallel searches for ECG findings across multiple conditions simultaneously.
Finding Sources
Finding Sources
Finding Sources
Finding Sources
Finding Sources
Now let me read the key ECG content blocks from Guyton/Harrison/Tintinalli in parallel:
Reading File
Reading File
Reading File
Excellent content found. Now let me read the ECG images and additional content for more conditions:
Reading File
Finding Sources
Finding Sources
Reading File
Finding Sources
Finding Sources
Now let me fetch the key ECG images in parallel:
Loading Image
Loading Image
Loading Image
Loading Image
Loading Image
Loading Image
Excellent ECG images retrieved. Now let me get more images - atrial flutter, ventricular fibrillation, and WPW:
Reading File
Reading File
Loading Image
Loading Image
Finding Sources
Reading File
Reading File
Reading File
Reading File
Loading Image
Loading Image
I now have all the key images and content needed. Let me compile the comprehensive guide.---

ECG Guide: 10 Common Heart Diseases

A comprehensive reference drawn from Harrison's Principles of Internal Medicine 22E, Guyton & Hall Textbook of Medical Physiology, Ganong's Review of Medical Physiology, and Tintinalli's Emergency Medicine.

1. Acute Myocardial Infarction (STEMI / NSTEMI)

Pathophysiology

Occlusion of a coronary artery causes three simultaneous electrical disturbances in infarcted cells: rapid repolarization (accelerated K+ channel opening), decline in resting membrane potential (loss of intracellular K+), and delayed depolarization. All three produce a current of injury directed outward from the affected zone.

Characteristic ECG Changes

PhaseFindings
Hyperacute (minutes)Tall, peaked "hyperacute" T waves
Early acute (hours)ST elevation in leads overlying infarct; reciprocal ST depression in opposite leads
Evolving (hours-days)T-wave inversions; Q waves develop
Established (days-weeks)Pathological Q waves; ST normalizes
NSTEMIST depression or T-wave changes without Q waves; no ST elevation
Localization by leads:
  • Anterior (LAD): ST elevation V1-V6, leads I, aVL
  • Inferior (RCA): ST elevation II, III, aVF
  • Posterior (LCx): reciprocal ST depression V1-V3 (dominant R with ST depression)
  • Right ventricular: ST elevation in right-sided leads (V3R, V4R)
"The hallmark of acute myocardial infarction is elevation of the ST segments in the leads overlying the area of infarction. Leads on the opposite side of the heart show ST segment depression." - Ganong's Review of Medical Physiology

ECG Image - Serial Evolution of Anterior Infarction

Serial ECG patterns in anterior MI: A=Normal, B=Early ST elevation, C=Q waves with ST elevation, D=Deeper Q waves, E=T-wave normalization
Serial 12-lead ECG patterns in anterior myocardial infarction. A: Normal. B: Early pattern - ST elevation in I, aVL, V3-V5. C: Q waves appear with ST elevation. D: Deep Q waves, deep T inversions in V3-V5. E: Partial normalization. (Ganong's Review of Medical Physiology)

ECG Image - Anterior Wall Ischemia / T-wave Changes

Precordial leads V1-V6 showing severe anterior wall ischemia with prominent T-wave inversions in V3-V6
Severe anterior wall ischemia showing prominent T-wave inversions in the precordial leads (V1-V6). (Harrison's Principles of Internal Medicine 22E)

2. Atrial Fibrillation (AF)

Pathophysiology

Multiple chaotic re-entrant wavelets circulate throughout the atrial muscle mass simultaneously. Atrial enlargement (from valve disease, ventricular failure, or hypertension) predisposes by lengthening conduction pathways and slowing impulse conduction. Impulses reach the AV node irregularly, producing the signature irregularly irregular ventricular response.

Characteristic ECG Changes

  • No discernible P waves - replaced by fine, irregular fibrillatory baseline (f-waves, 350-600/min)
  • Irregularly irregular RR intervals - the hallmark finding
  • Normal QRS complexes (unless aberrant conduction or bundle branch block present)
  • Ventricular rate typically 90-170 bpm (uncontrolled)
  • Baseline may show low-amplitude, high-frequency oscillations instead of distinct P waves
"The ECG hallmarks of atrial fibrillation include the absence of discernible P waves and an irregularly irregular ventricular rhythm." - Tintinalli's Emergency Medicine

ECG Image

Atrial fibrillation ECG in lead II showing absent P waves and irregular QRS complexes
Atrial fibrillation (Lead II). No P waves are visible; QRS complexes are present at irregular intervals. (Guyton & Hall Textbook of Medical Physiology)

3. Atrial Flutter

Pathophysiology

A single large re-entrant circuit travels continuously around the atrial muscle mass (most commonly a counterclockwise circuit around the tricuspid annulus in typical flutter). The atria contract at 250-350 beats/min; the AV node cannot conduct all impulses, so a 2:1, 3:1, or 4:1 block typically results.

Characteristic ECG Changes

  • "Sawtooth" flutter waves (F-waves) - regular, uniform P-like deflections at 250-350/min, best seen in leads II, III, aVF, V1
  • No isoelectric baseline between flutter waves
  • Regular or regularly irregular ventricular rate depending on AV conduction ratio
  • Most common: 2:1 block → ventricular rate ~150 bpm
  • QRS complexes are normal in width (unless aberrant conduction)

ECG Image

Atrial flutter ECG showing classic sawtooth flutter waves with 2:1 block
Atrial flutter at 250 beats/min with 2:1 AV block producing a ventricular rate of 125 beats/min (Lead II). Classic sawtooth pattern visible. (Guyton & Hall Textbook of Medical Physiology)

4. Ventricular Fibrillation (VF)

Pathophysiology

Multiple micro-reentrant circuits simultaneously traverse the ventricular myocardium. Impulse division at refractory areas multiplies wavefronts, shortens refractory periods, and slows conduction - creating a self-sustaining, hemodynamically lethal state. No coordinated ventricular contraction occurs; VF is immediately fatal without defibrillation.

Characteristic ECG Changes

  • Completely chaotic, irregular waveforms - no identifiable P waves, QRS complexes, or T waves
  • Coarse VF (early): high-amplitude, rapid, irregular oscillations (~0.5 mV)
  • Fine VF (later, after 20-30 seconds): low-amplitude, irregular oscillations (~0.2 mV) - harder to distinguish from asystole
  • No repetitive pattern - waveform morphology varies continuously
  • Rate effectively indeterminate (30-500 ventricular "activations"/min)
"In ventricular fibrillation, the ECG is bizarre and ordinarily shows no tendency toward a regular rhythm of any type." - Guyton & Hall Textbook of Medical Physiology

ECG Image

Ventricular fibrillation ECG showing chaotic, irregular low-amplitude oscillations with no organized waveforms
Ventricular fibrillation (Lead II). Chaotic, completely irregular undulations with no identifiable P waves, QRS complexes, or T waves. (Guyton & Hall Textbook of Medical Physiology)

5. First-Degree Atrioventricular (AV) Block

Pathophysiology

Slowed conduction through the AV node (or rarely the His-Purkinje system) delays, but does not block, atrial impulses reaching the ventricles. Every P wave still conducts to the ventricles. Causes include vagal tone, inferior MI, myocarditis, digitalis toxicity, and electrolyte disturbances.

Characteristic ECG Changes

  • Prolonged PR interval > 200 ms (0.20 s) in adults - the defining feature
  • Every P wave is followed by a QRS complex (1:1 conduction)
  • Normal P wave morphology
  • Normal QRS duration (unless co-existing bundle branch block)
  • Regular rhythm

ECG Image

First-degree heart block ECG showing prolonged PR interval of 0.38 seconds
First-degree heart block. PR interval = 0.38 seconds (normal < 0.20 s). All P waves conduct to ventricles. (Ganong's Review of Medical Physiology)

6. Second-Degree AV Block

Pathophysiology

Intermittent failure of AV conduction means some P waves are not followed by QRS complexes. Two main types:
  • Mobitz Type I (Wenckebach): Progressive AV nodal fatigue causes lengthening PR intervals until one P wave is blocked, then the cycle resets. Usually benign; AV node level lesion.
  • Mobitz Type II: Sudden, non-preceded failure of conduction, fixed PR interval. Often infranodal (His-Purkinje); higher risk of complete block.
  • 2:1 block: Every other P wave is blocked; cannot distinguish Mobitz I from II.

Characteristic ECG Changes

Mobitz Type I (Wenckebach):
  • Progressively lengthening PR interval
  • Eventually a P wave is not followed by a QRS (dropped beat)
  • RR intervals shorten before the dropped beat; the cycle repeats
Mobitz Type II:
  • Fixed PR interval (normal or prolonged)
  • Sudden non-conducted P wave (no preceding PR prolongation)
  • Often wide QRS (bundle branch block pattern)

ECG Images

Second-degree heart block Wenckebach showing progressively lengthening PR with dropped QRS
Second-degree AV block - Wenckebach (Mobitz Type I). Three P waves visible; PR progressively lengthens until the third P wave fails to conduct (dropped QRS). (Ganong's Review of Medical Physiology)
Second-degree 2:1 heart block showing every other P wave fails to conduct
Second-degree AV block - 2:1 pattern (aVF lead). Multiple P waves (labeled "P") visible; only every other P wave is followed by a QRS complex. (Ganong's Review of Medical Physiology)

7. Third-Degree (Complete) AV Block

Pathophysiology

Complete interruption of conduction between atria and ventricles. Atria and ventricles beat independently from separate pacemakers. The ventricular escape rhythm arises from a subsidiary pacemaker (junctional at 40-60 bpm, or ventricular at 20-40 bpm). Causes include septal MI, surgical damage to the bundle of His, Lyme disease, and myocarditis. Results in Stokes-Adams syndrome (syncope from cerebral ischemia).

Characteristic ECG Changes

  • Complete AV dissociation - P waves and QRS complexes are entirely independent
  • Regular P-P intervals (atrial rate, typically 60-100 bpm)
  • Regular, but slower, RR intervals (ventricular rate: junctional 40-60 bpm; ventricular 15-40 bpm)
  • P waves "march through" QRS complexes with no relationship
  • QRS may be narrow (junctional escape) or wide/aberrant (ventricular escape)
"The ventricular rate averages 35 beats/min, but in individual cases can be as low as 15 beats/min. The resultant cerebral ischemia causes dizziness and fainting (Stokes-Adams syndrome)." - Ganong's Review of Medical Physiology

ECG Image

Complete heart block ECG showing P waves and QRS complexes firing independently
Complete (third-degree) heart block (aVF lead). Multiple P waves labeled "P" march through at a rate of 107/min; QRS complexes labeled "QRS" occur independently at a ventricular rate of 43/min. (Ganong's Review of Medical Physiology)

8. Wolff-Parkinson-White (WPW) Syndrome

Pathophysiology

An accessory atrioventricular pathway (Bundle of Kent) bypasses the AV node, conducting impulses from atria to ventricles faster than the normal pathway. This pre-excites part of the ventricle before the main impulse arrives via the AV node, producing the characteristic delta wave. Patients are prone to re-entrant tachycardias (AVRT) and, if AF develops, potentially fatal rapid ventricular conduction via the accessory pathway.

Characteristic ECG Changes (in sinus rhythm)

  • Short PR interval (< 120 ms / 0.12 s) - AV node delay bypassed
  • Delta wave - slurred, slow upstroke at onset of QRS (pre-excitation of ventricle)
  • Wide QRS complex (> 120 ms) - fusion of normal conduction and accessory pathway conduction
  • Secondary ST-T changes - discordant to QRS direction
  • Paroxysmal tachycardias:
    • Orthodromic AVRT: narrow-complex tachycardia (conduction antegrade via AV node, retrograde via accessory path)
    • Antidromic AVRT: wide-complex tachycardia (conduction antegrade via accessory path)

ECG Image

WPW syndrome 12-lead ECG showing short PR interval, delta wave, and widened QRS with schematic of accessory pathway conduction
Wolff-Parkinson-White syndrome. A: 12-lead ECG showing short PR interval, delta waves, and wide QRS complexes. B: Isolated complex showing delta wave slur at QRS onset. C: Normal-looking complex for comparison. D: Schematic showing dual pathway conduction via AV node (AVN) and accessory pathway (AP). (Tintinalli's Emergency Medicine)

9. Left Ventricular Hypertrophy (LVH)

Pathophysiology

Chronic pressure overload (hypertension, aortic stenosis) or volume overload (regurgitation, cardiomyopathy) causes increased muscle mass of the left ventricle. Greater myocardial mass generates larger electrical forces, producing high-voltage QRS complexes. Altered repolarization produces the "strain" pattern ST-T changes.

Characteristic ECG Changes

Voltage criteria (any one of):
  • S in V1 + R in V5 or V6 ≥ 35 mm (Sokolow-Lyon)
  • R in aVL ≥ 20 mm (women) or ≥ 28 mm (men)
  • R in aVL + S in V3 > 20 mm (women) or > 28 mm (men) (Cornell)
Associated findings:
  • Left axis deviation
  • Left atrial enlargement (bifid P wave, P duration > 120 ms in II; or biphasic P in V1 with negative terminal component)
  • "Strain" pattern - ST depression with asymmetric T-wave inversion in leads I, aVL, V5-V6 (lateral leads)
  • QRS widening (may progress to LBBB)
  • Cornell voltage criteria increase specificity when combined with left atrial abnormality
"LVH often progresses to incomplete or complete left bundle branch block. ECG evidence for LVH is a major noninvasive marker of increased risk of cardiovascular morbidity and mortality, including sudden cardiac death." - Harrison's Principles of Internal Medicine 22E

10. Bundle Branch Block

Pathophysiology

Intrinsic impairment of conduction in either the right (RBBB) or left (LBBB) bundle branch forces ventricular activation to proceed cell-to-cell through the unaffected ventricle, then slowly spreads to the blocked side. This abnormal activation sequence widens the QRS and produces characteristic terminal forces and repolarization changes.

Right Bundle Branch Block (RBBB)

The terminal QRS vector is oriented to the right and anteriorly because the right ventricle depolarizes last.
ECG changes:
  • QRS ≥ 120 ms (complete RBBB)
  • rSR' (or rsR') in V1 - "M" or "rabbit ears" pattern
  • Wide S wave in leads I and V6 (terminal activation moving rightward)
  • ST depression and T-wave inversion in V1-V3 (secondary repolarization changes)
  • Causes: right heart strain, pulmonary embolism, congenital heart disease, anterior MI

Left Bundle Branch Block (LBBB)

Disrupts both early septal depolarization and delayed left ventricular activation.
ECG changes:
  • QRS ≥ 120 ms (complete LBBB)
  • Broad monophasic R wave in leads I, aVL, V5-V6 (entirely positive QRS)
  • Wide QS or rS complex in V1 (predominantly negative QRS)
  • No septal Q waves in leads I, V5-V6 (septal activation reversed)
  • Discordant ST-T changes (ST and T in opposite direction to main QRS)
  • New LBBB in the setting of chest pain is treated as STEMI equivalent (Sgarbossa criteria)
  • Causes: hypertension, coronary artery disease, dilated cardiomyopathy, aortic stenosis
"With right bundle branch block, the terminal QRS vector is oriented to the right and anteriorly (rSR' in V1 and qRS in V6). Left bundle branch block generates wide, predominantly negative (QS) complexes in lead V1 and entirely positive (R) complexes in V6." - Harrison's Principles of Internal Medicine 22E

Quick Reference Summary Table

ConditionKey ECG FeatureRateRhythmP WavesQRS
STEMIST elevation + Q wavesVariableRegularNormalNormal or wide
NSTEMIST depression / T-wave inversionsVariableRegularNormalNormal
Atrial FibrillationNo P waves; f-waves90-170Irregularly irregularAbsentNormal
Atrial FlutterSawtooth F-waves75-150Regular (or fixed ratio)Flutter wavesNormal
Ventricular FibrillationChaotic oscillationsIndeterminateNo rhythmNoneNone
1° AV BlockPR > 200 msNormalRegularNormalNormal
2° AV Block (Wenckebach)Progressive PR lengthening → dropped beatSlowIrregularNormalNormal
3° AV BlockP-QRS dissociationAtrial normal; ventricular 15-60Regular (both pacemakers independent)Normal (independent)Wide or narrow
WPW SyndromeShort PR + delta wave + wide QRSNormal (sinus)Regular (sinus)NormalWide (pre-excitation)
LVHHigh voltage + strain patternNormalRegularBifid (LAE)Tall, may be wide
RBBBrSR' in V1; wide S in I, V6NormalRegularNormal≥ 120 ms
LBBBBroad R in V6; QS in V1NormalRegularNormal≥ 120 ms

*Sources: Guyton & Hall Textbook of Medical Physiology (Elsevier); Ganong's Review of Medical Physiology, 26th Ed. (McGraw-Hill); Harrison's Principles of Internal Medicine, 22nd Ed. (McGraw-Hill, 2025); Tintinalli's Emergency Medicine, 9th Ed. (McGraw-Hill)*This guide covers 12 ECG entities across 10 disease categories, with direct textbook ECG images retrieved from Harrison's, Guyton & Hall, Ganong's, and Tintinalli's. Here's a quick breakdown of what's included:
  1. Acute MI (STEMI/NSTEMI) - ST evolution, hyperacute T waves, Q waves, localization by territory
  2. Atrial Fibrillation - Absent P waves, irregularly irregular RR
  3. Atrial Flutter - Classic sawtooth F-waves, 2:1 block pattern
  4. Ventricular Fibrillation - Chaotic oscillations, coarse vs. fine VF
  5. 1° AV Block - PR > 200 ms (illustrated with PR = 0.38 s image)
  6. 2° AV Block - Wenckebach (progressive PR) and 2:1 block (both illustrated)
  7. 3° (Complete) AV Block - P-QRS dissociation (illustrated with atrial rate 107, ventricular rate 43)
  8. WPW Syndrome - Short PR + delta wave + wide QRS (full 12-lead image included)
  9. Left Ventricular Hypertrophy - Voltage criteria (Sokolow-Lyon, Cornell), strain pattern
  10. Bundle Branch Block (RBBB & LBBB) - rSR' vs. broad R/QS pattern
The serial MI evolution image (A through E panels showing normal → hyperacute → Q waves → deep inversion → recovery) is particularly useful for understanding the time course of ischemic ECG changes.

Generate a PDF reference card with ECG patterns and diagnostic criteria

Reading File
Running Command

ls -la /home/daytona/workspace/ 2>/dev/null || echo "workspace empty or missing"

Writing File

~/ecg-reference/build_ecg_pdf.py

"""
ECG Reference Card PDF Generator
Uses ReportLab for professional multi-page PDF layout.
"""

from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
    HRFlowable, PageBreak, KeepTogether
)
from reportlab.platypus.flowables import Flowable
from reportlab.graphics.shapes import Drawing, Rect, Line, String, Path
from reportlab.graphics import renderPDF
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import io
import urllib.request
import os

# ── Color palette ──────────────────────────────────────────────────────────────
NAVY       = colors.HexColor("#0D2B4E")
CRIMSON    = colors.HexColor("#C0392B")
TEAL       = colors.HexColor("#1A6E7A")
GOLD       = colors.HexColor("#D4A017")
LIGHT_BLUE = colors.HexColor("#EAF4FB")
LIGHT_GRAY = colors.HexColor("#F5F5F5")
MID_GRAY   = colors.HexColor("#CCCCCC")
DARK_GRAY  = colors.HexColor("#444444")
WHITE      = colors.white
ECG_RED    = colors.HexColor("#CC2222")
ECG_BLUE   = colors.HexColor("#1A3A8A")
GRID_COLOR = colors.HexColor("#FFCCCC")

OUTPUT = "/home/daytona/workspace/ecg-reference/ECG_Reference_Card.pdf"
W, H   = A4  # 595.27 x 841.89 pts

# ── Styles ────────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()

def S(name, **kw):
    return ParagraphStyle(name, **kw)

TITLE_STYLE  = S("Title",  fontName="Helvetica-Bold", fontSize=22, textColor=WHITE,
                  alignment=TA_CENTER, spaceAfter=4)
SUB_STYLE    = S("Sub",    fontName="Helvetica", fontSize=10, textColor=colors.HexColor("#D0E8F5"),
                  alignment=TA_CENTER, spaceAfter=2)
HDR_STYLE    = S("Hdr",    fontName="Helvetica-Bold", fontSize=12, textColor=WHITE,
                  alignment=TA_LEFT, spaceBefore=0, spaceAfter=0, leftPadding=6)
BODY_STYLE   = S("Body",   fontName="Helvetica", fontSize=8, textColor=DARK_GRAY,
                  leading=11, spaceAfter=2)
SMALL_STYLE  = S("Small",  fontName="Helvetica", fontSize=7, textColor=DARK_GRAY,
                  leading=10)
BOLD_STYLE   = S("Bold",   fontName="Helvetica-Bold", fontSize=8, textColor=NAVY,
                  leading=11)
CAPTION      = S("Caption",fontName="Helvetica-Oblique", fontSize=6.5,
                  textColor=colors.HexColor("#666666"), alignment=TA_CENTER, spaceAfter=2)
FOOT_STYLE   = S("Foot",   fontName="Helvetica", fontSize=6.5, textColor=MID_GRAY,
                  alignment=TA_CENTER)
CRIT_STYLE   = S("Crit",   fontName="Helvetica-Bold", fontSize=7.5,
                  textColor=CRIMSON, leading=10)
NOTE_STYLE   = S("Note",   fontName="Helvetica-Oblique", fontSize=7,
                  textColor=TEAL, leading=10)

# ── ECG waveform drawing helpers ───────────────────────────────────────────────
def ecg_grid(d, x, y, w, h, rows=4, cols=5):
    """Draw ECG paper grid."""
    d.add(Rect(x, y, w, h, fillColor=colors.HexColor("#FFF8F8"),
               strokeColor=colors.HexColor("#FFBBBB"), strokeWidth=0.3))
    # minor grid
    small = w / (cols * 5)
    for i in range(cols * 5 + 1):
        lw = 0.4 if i % 5 == 0 else 0.15
        lc = colors.HexColor("#FF9999") if i % 5 == 0 else colors.HexColor("#FFCCCC")
        d.add(Line(x + i*small, y, x + i*small, y + h, strokeColor=lc, strokeWidth=lw))
    row_h = h / (rows * 5)
    for i in range(rows * 5 + 1):
        lw = 0.4 if i % 5 == 0 else 0.15
        lc = colors.HexColor("#FF9999") if i % 5 == 0 else colors.HexColor("#FFCCCC")
        d.add(Line(x, y + i*row_h, x + w, y + i*row_h, strokeColor=lc, strokeWidth=lw))

def draw_ecg_line(d, pts, color=ECG_RED, lw=1.2):
    """Draw a polyline from a list of (x,y) tuples."""
    if len(pts) < 2:
        return
    p = Path(strokeColor=color, strokeWidth=lw, fillColor=None)
    p.moveTo(pts[0][0], pts[0][1])
    for px, py in pts[1:]:
        p.lineTo(px, py)
    d.add(p)

def normal_qrs(cx, cy, scale=1.0, color=ECG_RED):
    """Return points for a single normal PQRST complex centered near (cx,cy)."""
    s = scale
    pts = [
        (cx - 22*s, cy),
        (cx - 18*s, cy + 3*s),   # P start
        (cx - 14*s, cy + 6*s),   # P peak
        (cx - 10*s, cy + 3*s),   # P end
        (cx - 6*s,  cy),          # PR segment
        (cx - 3*s,  cy - 2*s),   # Q
        (cx,        cy + 20*s),  # R peak
        (cx + 3*s,  cy - 4*s),   # S
        (cx + 6*s,  cy),          # ST start
        (cx + 12*s, cy + 4*s),   # T start
        (cx + 17*s, cy + 7*s),   # T peak
        (cx + 22*s, cy + 3*s),   # T end
        (cx + 28*s, cy),
    ]
    return pts

# ── ECG Diagram functions (each returns a Drawing) ────────────────────────────
def make_normal_ecg(w=160, h=50):
    """Normal sinus rhythm strip."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.40
    pts = []
    for i in range(3):
        cx = 20 + i * 48
        pts += normal_qrs(cx, baseline, scale=0.80)
        if i < 2:
            pts.append((cx + 30, baseline))
    draw_ecg_line(d, pts, ECG_RED)
    return d

def make_afib_ecg(w=160, h=50):
    """Atrial fibrillation - irregular QRS, no P waves, fibrillatory baseline."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.42
    import math, random
    random.seed(42)
    # fibrillatory baseline
    fib_pts = []
    for x in range(int(w)):
        noise = math.sin(x * 0.9) * 1.5 + math.sin(x * 1.7) * 1.0 + math.sin(x * 3.1) * 0.7
        fib_pts.append((x, baseline + noise))
    draw_ecg_line(d, fib_pts, colors.HexColor("#FF6666"), lw=0.7)
    # irregular QRS - variable spacing
    qrs_x = [22, 46, 73, 94, 118, 140]
    for cx in qrs_x:
        qrs = [
            (cx - 2, baseline),
            (cx - 1, baseline - 2),
            (cx,     baseline + 16),
            (cx + 1, baseline - 5),
            (cx + 3, baseline),
        ]
        draw_ecg_line(d, qrs, ECG_BLUE, lw=1.4)
    d.add(String(4, h - 8, "II", fillColor=NAVY, fontSize=7,
                 fontName="Helvetica-Bold"))
    return d

def make_aflutter_ecg(w=160, h=50):
    """Atrial flutter - sawtooth F-waves, 2:1 block."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.45
    # sawtooth
    saw = []
    x = 4
    while x < w - 10:
        saw += [(x, baseline), (x + 6, baseline + 8), (x + 12, baseline)]
        x += 12
    draw_ecg_line(d, saw, colors.HexColor("#DD4444"), lw=0.9)
    # QRS every 2nd flutter wave
    for cx in [28, 52, 76, 100, 124, 148]:
        qrs = [
            (cx - 2, baseline),
            (cx,     baseline + 18),
            (cx + 2, baseline - 4),
            (cx + 4, baseline),
            (cx + 8, baseline + 5),
            (cx + 12, baseline),
        ]
        draw_ecg_line(d, qrs, ECG_BLUE, lw=1.4)
    d.add(String(4, h - 8, "II", fillColor=NAVY, fontSize=7,
                 fontName="Helvetica-Bold"))
    return d

def make_vfib_ecg(w=160, h=50):
    """Ventricular fibrillation - chaotic oscillations."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    import math, random
    random.seed(7)
    baseline = h * 0.5
    pts = []
    for x in range(int(w)):
        chaos = (math.sin(x * 0.3) * 9 + math.sin(x * 0.7 + 1) * 7
                 + math.sin(x * 1.3 + 2) * 5 + math.sin(x * 2.1) * 3
                 + (random.random() - 0.5) * 4)
        pts.append((x, baseline + chaos))
    draw_ecg_line(d, pts, ECG_RED, lw=1.0)
    d.add(String(4, h - 8, "II", fillColor=NAVY, fontSize=7,
                 fontName="Helvetica-Bold"))
    return d

def make_vtach_ecg(w=160, h=50):
    """Ventricular tachycardia - wide, rapid, regular QRS."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.45
    import math
    pts = [(0, baseline)]
    period = 22
    for i in range(7):
        cx = 8 + i * period
        pts += [
            (cx,      baseline),
            (cx + 3,  baseline + 16),
            (cx + 6,  baseline + 16),
            (cx + 9,  baseline - 10),
            (cx + 12, baseline - 10),
            (cx + 15, baseline + 5),
            (cx + 18, baseline + 5),
            (cx + 20, baseline),
        ]
    draw_ecg_line(d, pts, ECG_BLUE, lw=1.3)
    d.add(String(4, h - 8, "II", fillColor=NAVY, fontSize=7,
                 fontName="Helvetica-Bold"))
    return d

def make_stemi_ecg(w=160, h=60):
    """STEMI - ST elevation pattern."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.38
    st_elev = 12  # ST elevation in pts
    pts = []
    for i in range(3):
        cx = 18 + i * 50
        pts += [
            (cx - 14, baseline),
            (cx - 10, baseline + 5),  # P
            (cx - 6,  baseline + 2),
            (cx - 3,  baseline - 2),  # Q
            (cx,      baseline + 22), # R tall
            (cx + 3,  baseline + st_elev),  # S (elevated)
            (cx + 8,  baseline + st_elev + 3),  # ST elevated
            (cx + 13, baseline + st_elev + 1),
            (cx + 17, baseline - 3),  # T inversion
            (cx + 22, baseline),
        ]
        if i < 2:
            pts.append((cx + 28, baseline))
    draw_ecg_line(d, pts, ECG_RED, lw=1.3)
    # label ST elevation arrow
    d.add(Line(18, baseline + st_elev, 18, baseline,
               strokeColor=colors.HexColor("#006600"), strokeWidth=1.0,
               strokeDashArray=[2, 2]))
    d.add(String(20, baseline + st_elev/2, "↑ST", fillColor=colors.HexColor("#006600"),
                 fontSize=6, fontName="Helvetica-Bold"))
    d.add(String(4, h - 8, "V4", fillColor=NAVY, fontSize=7, fontName="Helvetica-Bold"))
    return d

def make_first_degree_block_ecg(w=160, h=50):
    """First-degree AV block - long PR interval."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.40
    # Long PR = wider gap between P and QRS
    for i in range(3):
        cx = 22 + i * 50
        p_start = cx - 24
        pts = [
            (p_start,      baseline),
            (p_start + 4,  baseline + 5),   # P peak
            (p_start + 8,  baseline),        # P end
            # long PR segment
            (cx - 6,       baseline),
            (cx - 3,       baseline - 2),   # Q
            (cx,           baseline + 20),  # R
            (cx + 3,       baseline - 4),   # S
            (cx + 6,       baseline),
            (cx + 11,      baseline + 6),   # T
            (cx + 15,      baseline),
        ]
        draw_ecg_line(d, pts, ECG_RED, lw=1.2)
        # PR bracket
        if i == 1:
            d.add(Line(p_start, baseline - 8, cx, baseline - 8,
                       strokeColor=TEAL, strokeWidth=0.8))
            d.add(String(p_start + 4, baseline - 15, "PR > 200ms",
                         fillColor=TEAL, fontSize=6, fontName="Helvetica-Bold"))
    d.add(String(4, h - 8, "II", fillColor=NAVY, fontSize=7, fontName="Helvetica-Bold"))
    return d

def make_complete_block_ecg(w=160, h=60):
    """Third-degree (complete) AV block - P-QRS dissociation."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.45
    # P waves at ~100 bpm (fast)
    p_xs = [10, 22, 34, 46, 58, 70, 82, 94, 106, 118, 130, 142, 154]
    for px in p_xs:
        draw_ecg_line(d, [(px, baseline), (px+3, baseline+6),
                          (px+6, baseline)], colors.HexColor("#AA3300"), lw=0.9)
    # QRS at ~40 bpm (slow) - wide
    qrs_xs = [18, 58, 98, 138]
    for cx in qrs_xs:
        draw_ecg_line(d, [
            (cx, baseline), (cx+2, baseline - 3),
            (cx+4, baseline + 14), (cx+6, baseline - 5),
            (cx+10, baseline), (cx+14, baseline + 4), (cx+18, baseline)
        ], ECG_BLUE, lw=1.4)
    # labels
    d.add(String(4, h - 8, "aVF", fillColor=NAVY, fontSize=7, fontName="Helvetica-Bold"))
    d.add(String(72, h - 8, "Atrial: 107/min  Ventricular: 43/min",
                 fillColor=DARK_GRAY, fontSize=5.5, fontName="Helvetica-Oblique"))
    return d

def make_wenckebach_ecg(w=160, h=55):
    """Second-degree AV block Mobitz I (Wenckebach)."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.45
    # 3 beats then dropped: PR progressively longer
    pr_lengths = [8, 13, 18]  # increasing PR
    cx_starts  = [14, 50, 92]
    for idx, (cx, pr) in enumerate(zip(cx_starts, pr_lengths)):
        # P wave
        draw_ecg_line(d, [(cx, baseline), (cx+3, baseline+7), (cx+6, baseline)],
                      colors.HexColor("#AA3300"), lw=1.0)
        # QRS after PR gap
        qcx = cx + pr
        draw_ecg_line(d, [
            (qcx, baseline), (qcx+1, baseline-2),
            (qcx+2, baseline+16), (qcx+3, baseline-4),
            (qcx+5, baseline), (qcx+9, baseline+6), (qcx+13, baseline)
        ], ECG_BLUE, lw=1.3)
        # PR label
        d.add(String(cx+1, baseline - 14, f"PR={pr*5}ms",
                     fillColor=TEAL, fontSize=5.5, fontName="Helvetica-Bold"))
    # 4th P wave - no QRS (dropped)
    d.add(String(134, baseline + 10, "P", fillColor=colors.HexColor("#AA3300"),
                 fontSize=8, fontName="Helvetica-Bold"))
    draw_ecg_line(d, [(136, baseline), (139, baseline+7), (142, baseline)],
                  colors.HexColor("#AA3300"), lw=1.0)
    d.add(String(140, baseline - 14, "BLOCKED",
                 fillColor=CRIMSON, fontSize=5.5, fontName="Helvetica-Bold"))
    d.add(String(4, h - 8, "II", fillColor=NAVY, fontSize=7, fontName="Helvetica-Bold"))
    return d

def make_wpw_ecg(w=160, h=55):
    """WPW - short PR, delta wave, wide QRS."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.40
    for i in range(3):
        cx = 20 + i * 50
        pts = [
            (cx - 18, baseline),
            (cx - 14, baseline + 5),   # P
            (cx - 10, baseline + 2),   # short PR
            # delta wave (slow slur)
            (cx - 7,  baseline),
            (cx - 2,  baseline + 8),   # delta wave slur
            (cx,      baseline + 22),  # R peak
            (cx + 4,  baseline - 4),   # S
            (cx + 8,  baseline),
            (cx + 12, baseline + 5),   # T
            (cx + 16, baseline),
        ]
        draw_ecg_line(d, pts, ECG_RED, lw=1.3)
        # delta wave annotation
        if i == 1:
            d.add(String(cx - 8, baseline - 14, "δ wave",
                         fillColor=GOLD, fontSize=6, fontName="Helvetica-Bold"))
            d.add(Line(cx - 7, baseline - 1, cx - 1, baseline + 7,
                       strokeColor=GOLD, strokeWidth=0.8))
    d.add(String(4, h - 8, "I", fillColor=NAVY, fontSize=7, fontName="Helvetica-Bold"))
    return d

def make_lvh_ecg(w=160, h=65):
    """LVH - tall R in V5, deep S in V1, strain pattern."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.55
    # V1 - deep S
    cx1 = 28
    pts_v1 = [
        (cx1 - 12, baseline),
        (cx1 - 8,  baseline + 4),   # small r
        (cx1 - 4,  baseline),
        (cx1,      baseline - 22),  # deep S
        (cx1 + 4,  baseline),
        (cx1 + 9,  baseline - 3),   # T negative
        (cx1 + 14, baseline),
    ]
    draw_ecg_line(d, pts_v1, ECG_RED, lw=1.3)
    d.add(String(cx1 - 14, 6, "V1", fillColor=NAVY, fontSize=7, fontName="Helvetica-Bold"))
    # V5 - tall R, strain
    cx5 = 110
    pts_v5 = [
        (cx5 - 12, baseline),
        (cx5 - 8,  baseline + 4),
        (cx5 - 4,  baseline),
        (cx5 - 2,  baseline - 2),   # small Q
        (cx5,      baseline + 40),  # TALL R
        (cx5 + 3,  baseline - 3),
        (cx5 + 6,  baseline - 5),   # ST depression (strain)
        (cx5 + 12, baseline - 12),  # T inversion (strain)
        (cx5 + 16, baseline - 3),
        (cx5 + 20, baseline),
    ]
    draw_ecg_line(d, pts_v5, ECG_RED, lw=1.3)
    d.add(String(cx5 - 8, 6, "V5", fillColor=NAVY, fontSize=7, fontName="Helvetica-Bold"))
    # measurement arrows
    d.add(Line(cx5 + 1, baseline, cx5 + 1, baseline + 40,
               strokeColor=TEAL, strokeWidth=0.7, strokeDashArray=[2, 2]))
    d.add(String(cx5 + 4, baseline + 18, "R>25mm",
                 fillColor=TEAL, fontSize=5.5, fontName="Helvetica-Bold"))
    # strain label
    d.add(String(cx5 + 8, baseline - 16, "Strain",
                 fillColor=CRIMSON, fontSize=5.5, fontName="Helvetica-Bold"))
    return d

def make_lbbb_ecg(w=160, h=55):
    """LBBB - broad monophasic R in V6, QS in V1."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.48
    # V1 - QS pattern
    cx1 = 28
    pts_v1 = [
        (cx1 - 10, baseline),
        (cx1,      baseline - 20),  # deep QS
        (cx1 + 10, baseline),
        (cx1 + 14, baseline + 5),   # T positive (discordant)
        (cx1 + 18, baseline),
    ]
    draw_ecg_line(d, pts_v1, ECG_RED, lw=1.3)
    d.add(String(cx1 - 14, 6, "V1\n(QS)", fillColor=NAVY, fontSize=6.5,
                 fontName="Helvetica-Bold"))
    # V6 - broad positive R
    cx6 = 116
    pts_v6 = [
        (cx6 - 14, baseline),
        # broad, notched R (no Q)
        (cx6 - 8,  baseline + 2),
        (cx6 - 4,  baseline + 14),
        (cx6,      baseline + 22),  # R peak notched
        (cx6 + 4,  baseline + 18),
        (cx6 + 8,  baseline + 22),  # second notch (M-pattern)
        (cx6 + 12, baseline + 2),
        (cx6 + 14, baseline),
        (cx6 + 16, baseline - 6),  # ST depression
        (cx6 + 20, baseline - 10), # T inversion
        (cx6 + 24, baseline),
    ]
    draw_ecg_line(d, pts_v6, ECG_RED, lw=1.3)
    d.add(String(cx6 - 8, 6, "V6\n(RR')", fillColor=NAVY, fontSize=6.5,
                 fontName="Helvetica-Bold"))
    # QRS width annotation
    d.add(Line(cx6 - 14, baseline - 16, cx6 + 14, baseline - 16,
               strokeColor=TEAL, strokeWidth=0.7))
    d.add(String(cx6 - 8, baseline - 24, "QRS ≥ 120ms",
                 fillColor=TEAL, fontSize=5.5, fontName="Helvetica-Bold"))
    return d

def make_rbbb_ecg(w=160, h=55):
    """RBBB - rSR' in V1, wide S in V6."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.45
    # V1 - rSR' pattern
    cx1 = 32
    pts_v1 = [
        (cx1 - 14, baseline),
        (cx1 - 8,  baseline + 10),  # small r
        (cx1 - 4,  baseline + 2),
        (cx1,      baseline - 4),   # S
        (cx1 + 4,  baseline + 18),  # R' tall
        (cx1 + 10, baseline + 2),
        (cx1 + 14, baseline - 6),   # T inversion
        (cx1 + 18, baseline),
    ]
    draw_ecg_line(d, pts_v1, ECG_RED, lw=1.3)
    d.add(String(cx1 - 16, 6, "V1\n(rSR')", fillColor=NAVY, fontSize=6.5,
                 fontName="Helvetica-Bold"))
    # V6 - wide S wave
    cx6 = 118
    pts_v6 = [
        (cx6 - 14, baseline),
        (cx6 - 10, baseline + 4),   # q
        (cx6 - 7,  baseline + 2),
        (cx6 - 4,  baseline),
        (cx6,      baseline + 18),  # R
        (cx6 + 4,  baseline),
        (cx6 + 8,  baseline - 12),  # wide, deep S
        (cx6 + 14, baseline - 4),
        (cx6 + 18, baseline),
        (cx6 + 20, baseline + 5),   # T
        (cx6 + 24, baseline),
    ]
    draw_ecg_line(d, pts_v6, ECG_RED, lw=1.3)
    d.add(String(cx6 - 6, 6, "V6\n(qRS)", fillColor=NAVY, fontSize=6.5,
                 fontName="Helvetica-Bold"))
    d.add(String(cx6 + 4, baseline - 20, "Wide S",
                 fillColor=TEAL, fontSize=5.5, fontName="Helvetica-Bold"))
    return d

# ── ECG Drawing Flowable ───────────────────────────────────────────────────────
from reportlab.platypus import Flowable as RLFlowable

class ECGDrawing(RLFlowable):
    def __init__(self, drawing, width=None, height=None):
        RLFlowable.__init__(self)
        self.drawing = drawing
        self.width = width or drawing.width
        self.height = height or drawing.height
    def wrap(self, aW, aH):
        return (self.width, self.height)
    def draw(self):
        renderPDF.draw(self.drawing, self.canv, 0, 0)

# ── Section header ─────────────────────────────────────────────────────────────
def section_header(title, number, color=NAVY):
    data = [[
        Paragraph(f"<b>{number}</b>", ParagraphStyle("Num", fontName="Helvetica-Bold",
                  fontSize=14, textColor=GOLD, alignment=TA_CENTER)),
        Paragraph(title, HDR_STYLE)
    ]]
    t = Table(data, colWidths=[18*mm, None])
    t.setStyle(TableStyle([
        ("BACKGROUND",  (0,0), (-1,-1), color),
        ("VALIGN",      (0,0), (-1,-1), "MIDDLE"),
        ("TOPPADDING",  (0,0), (-1,-1), 4),
        ("BOTTOMPADDING",(0,0),(-1,-1), 4),
        ("LEFTPADDING", (0,0), (0,-1), 3),
        ("RIGHTPADDING",(-1,0),(-1,-1), 6),
        ("ROUNDEDCORNERS", [3]),
    ]))
    return t

def criteria_table(rows, col_widths=None):
    """Two-column criteria table: feature | value."""
    styled_rows = []
    for r in rows:
        if len(r) == 1:  # sub-header
            styled_rows.append([
                Paragraph(f"<b>{r[0]}</b>",
                    ParagraphStyle("SubHdr", fontName="Helvetica-Bold", fontSize=7.5,
                                   textColor=NAVY, leading=10)),
                Paragraph("", BODY_STYLE)
            ])
        else:
            styled_rows.append([
                Paragraph(f"<b>{r[0]}</b>", BOLD_STYLE),
                Paragraph(r[1], BODY_STYLE)
            ])
    cw = col_widths or [45*mm, None]
    t = Table(styled_rows, colWidths=cw, hAlign="LEFT")
    t.setStyle(TableStyle([
        ("BACKGROUND",   (0,0), (-1,-1), LIGHT_GRAY),
        ("BACKGROUND",   (0,0), (-1,0),  LIGHT_BLUE),
        ("ROWBACKGROUNDS",(0,0),(-1,-1), [WHITE, LIGHT_GRAY]),
        ("GRID",         (0,0), (-1,-1), 0.3, MID_GRAY),
        ("VALIGN",       (0,0), (-1,-1), "TOP"),
        ("TOPPADDING",   (0,0), (-1,-1), 3),
        ("BOTTOMPADDING",(0,0), (-1,-1), 3),
        ("LEFTPADDING",  (0,0), (-1,-1), 4),
        ("RIGHTPADDING", (0,0), (-1,-1), 4),
    ]))
    return t

def alert_box(text, bg=colors.HexColor("#FFF3CD"), border=GOLD):
    """Yellow alert/note box."""
    p = Paragraph(f"⚠ {text}", ParagraphStyle("Alert", fontName="Helvetica-Bold",
                  fontSize=7, textColor=colors.HexColor("#7D5A00"), leading=10))
    t = Table([[p]], colWidths=[None])
    t.setStyle(TableStyle([
        ("BACKGROUND",   (0,0),(-1,-1), bg),
        ("BOX",          (0,0),(-1,-1), 1.0, border),
        ("TOPPADDING",   (0,0),(-1,-1), 4),
        ("BOTTOMPADDING",(0,0),(-1,-1), 4),
        ("LEFTPADDING",  (0,0),(-1,-1), 6),
    ]))
    return t

def critical_box(text):
    """Red critical alert."""
    p = Paragraph(f"🚨 {text}", ParagraphStyle("Crit2", fontName="Helvetica-Bold",
                  fontSize=7, textColor=WHITE, leading=10))
    t = Table([[p]], colWidths=[None])
    t.setStyle(TableStyle([
        ("BACKGROUND",   (0,0),(-1,-1), CRIMSON),
        ("TOPPADDING",   (0,0),(-1,-1), 4),
        ("BOTTOMPADDING",(0,0),(-1,-1), 4),
        ("LEFTPADDING",  (0,0),(-1,-1), 6),
    ]))
    return t

# ── Page canvas (header/footer) ────────────────────────────────────────────────
def on_page(canv, doc):
    canv.saveState()
    # top banner
    canv.setFillColor(NAVY)
    canv.rect(0, H - 28, W, 28, fill=1, stroke=0)
    canv.setFillColor(WHITE)
    canv.setFont("Helvetica-Bold", 10)
    canv.drawString(14, H - 18, "ECG REFERENCE CARD")
    canv.setFont("Helvetica", 8)
    canv.setFillColor(colors.HexColor("#A0C4E0"))
    canv.drawRightString(W - 14, H - 18, f"Page {doc.page}  |  Orris Medical Reference")
    # footer
    canv.setFillColor(NAVY)
    canv.rect(0, 0, W, 18, fill=1, stroke=0)
    canv.setFillColor(MID_GRAY)
    canv.setFont("Helvetica-Oblique", 6.5)
    canv.drawString(14, 6, "Sources: Harrison's 22E · Guyton & Hall · Ganong's · Tintinalli's EM")
    canv.setFillColor(colors.HexColor("#A0C4E0"))
    canv.drawRightString(W - 14, 6, "For educational use only — not a substitute for clinical judgment")
    canv.restoreState()

def on_first_page(canv, doc):
    on_page(canv, doc)

# ── Build document ─────────────────────────────────────────────────────────────
def build():
    os.makedirs("/home/daytona/workspace/ecg-reference", exist_ok=True)
    doc = SimpleDocTemplate(
        OUTPUT, pagesize=A4,
        leftMargin=14*mm, rightMargin=14*mm,
        topMargin=34*mm, bottomMargin=24*mm,
        title="ECG Reference Card",
        author="Orris Medical",
        subject="ECG Patterns & Diagnostic Criteria",
    )

    story = []

    # ── Cover block ──────────────────────────────────────────────────────────
    cover_data = [[
        Paragraph("ECG Reference Card", TITLE_STYLE),
    ],[
        Paragraph("Common Heart Diseases · Characteristic ECG Patterns · Diagnostic Criteria", SUB_STYLE),
    ],[
        Paragraph("Harrison's 22E  ·  Guyton & Hall  ·  Ganong's  ·  Tintinalli's EM", SUB_STYLE),
    ]]
    cover = Table(cover_data, colWidths=[None])
    cover.setStyle(TableStyle([
        ("BACKGROUND",   (0,0),(-1,-1), NAVY),
        ("TOPPADDING",   (0,0),(-1,-1), 6),
        ("BOTTOMPADDING",(0,0),(-1,-1), 6),
        ("LEFTPADDING",  (0,0),(-1,-1), 10),
        ("RIGHTPADDING", (0,0),(-1,-1), 10),
        ("ROUNDEDCORNERS", [4]),
    ]))
    story.append(cover)
    story.append(Spacer(1, 5*mm))

    # ── Quick-Reference Summary Table ────────────────────────────────────────
    qr_hdr = [
        Paragraph("<b>Condition</b>", ParagraphStyle("QH", fontName="Helvetica-Bold",
                  fontSize=7.5, textColor=WHITE, alignment=TA_CENTER)),
        Paragraph("<b>Key Feature</b>", ParagraphStyle("QH", fontName="Helvetica-Bold",
                  fontSize=7.5, textColor=WHITE, alignment=TA_CENTER)),
        Paragraph("<b>Rate</b>", ParagraphStyle("QH", fontName="Helvetica-Bold",
                  fontSize=7.5, textColor=WHITE, alignment=TA_CENTER)),
        Paragraph("<b>Rhythm</b>", ParagraphStyle("QH", fontName="Helvetica-Bold",
                  fontSize=7.5, textColor=WHITE, alignment=TA_CENTER)),
        Paragraph("<b>P Wave</b>", ParagraphStyle("QH", fontName="Helvetica-Bold",
                  fontSize=7.5, textColor=WHITE, alignment=TA_CENTER)),
        Paragraph("<b>QRS</b>", ParagraphStyle("QH", fontName="Helvetica-Bold",
                  fontSize=7.5, textColor=WHITE, alignment=TA_CENTER)),
    ]
    qr_rows = [
        ["STEMI",             "ST elevation + Q waves",          "Variable",     "Regular",              "Normal",             "Normal/Wide"],
        ["NSTEMI",            "ST depression / T inversions",    "Variable",     "Regular",              "Normal",             "Normal"],
        ["Atrial Fibrillation","No P waves; f-waves",            "90–170 bpm",   "Irreg. irregular",     "Absent",             "Normal"],
        ["Atrial Flutter",    "Sawtooth F-waves 250–350/min",    "75–150 bpm",   "Regular (fixed ratio)","Flutter waves",      "Normal"],
        ["Ventricular Fib.",  "Chaotic oscillations",            "Indeterm.",    "No rhythm",            "None",               "None"],
        ["Ventricular Tach.", "Wide regular QRS ≥3 beats",       "100–250 bpm",  "Regular",              "Dissociated/absent","Wide ≥120ms"],
        ["1° AV Block",       "PR > 200 ms",                     "Normal",       "Regular",              "Normal",             "Normal"],
        ["2° AV Block (I)",   "Progressive PR → dropped QRS",    "Slow",         "Irregular",            "Normal",             "Normal"],
        ["2° AV Block (II)",  "Fixed PR → sudden dropped QRS",   "Slow",         "Irregular",            "Normal",             "Wide (usually)"],
        ["3° AV Block",       "P-QRS dissociation",              "Vent: 20–60",  "Indep. regular",       "Normal (indep.)",    "Wide or narrow"],
        ["WPW Syndrome",      "Short PR + delta wave",           "Normal",       "Regular (sinus)",      "Normal",             "Wide (≥120ms)"],
        ["LVH",               "Voltage criteria + strain",       "Normal",       "Regular",              "Bifid (LAE)",        "Tall"],
        ["LBBB",              "QS V1; broad R V6; no Q I,V6",    "Normal",       "Regular",              "Normal",             "≥120ms"],
        ["RBBB",              "rSR' V1; wide S I, V6",           "Normal",       "Regular",              "Normal",             "≥120ms"],
    ]
    cell_style = ParagraphStyle("Cell", fontName="Helvetica", fontSize=6.8,
                                textColor=DARK_GRAY, leading=9)
    bold_cell  = ParagraphStyle("BCell", fontName="Helvetica-Bold", fontSize=6.8,
                                textColor=NAVY, leading=9)

    table_data = [qr_hdr]
    for r in qr_rows:
        table_data.append([
            Paragraph(r[0], bold_cell),
            Paragraph(r[1], cell_style),
            Paragraph(r[2], cell_style),
            Paragraph(r[3], cell_style),
            Paragraph(r[4], cell_style),
            Paragraph(r[5], cell_style),
        ])

    cw = [32*mm, 44*mm, 22*mm, 28*mm, 22*mm, 22*mm]
    qr_table = Table(table_data, colWidths=cw, hAlign="LEFT")
    qr_table.setStyle(TableStyle([
        ("BACKGROUND",   (0,0),(-1,0),  NAVY),
        ("ROWBACKGROUNDS",(0,1),(-1,-1),[WHITE, LIGHT_BLUE]),
        ("GRID",         (0,0),(-1,-1), 0.3, MID_GRAY),
        ("VALIGN",       (0,0),(-1,-1), "MIDDLE"),
        ("TOPPADDING",   (0,0),(-1,-1), 2.5),
        ("BOTTOMPADDING",(0,0),(-1,-1), 2.5),
        ("LEFTPADDING",  (0,0),(-1,-1), 3),
        ("RIGHTPADDING", (0,0),(-1,-1), 3),
    ]))
    story.append(Paragraph("<b>QUICK-REFERENCE SUMMARY</b>",
                           ParagraphStyle("QRTitle", fontName="Helvetica-Bold",
                           fontSize=9, textColor=NAVY, spaceBefore=2, spaceAfter=3)))
    story.append(qr_table)
    story.append(PageBreak())

    # ════════════════════════════════════════════════════════════════════════════
    # Helper: build one condition block
    # ════════════════════════════════════════════════════════════════════════════
    def condition_block(num, title, drawing_fn, criteria_rows, notes=None,
                        critical=None, alert=None, ecg_caption=""):
        elems = []
        elems.append(KeepTogether([
            section_header(title, num, color=NAVY),
            Spacer(1, 2*mm),
        ]))

        # ECG drawing (left) + criteria (right)
        d = drawing_fn()
        ecg_cell = [ECGDrawing(d), Spacer(1, 1*mm), Paragraph(ecg_caption, CAPTION)]
        crit_cell = [criteria_table(criteria_rows)]
        if notes:
            crit_cell.append(Spacer(1, 1.5*mm))
            crit_cell.append(Paragraph(notes, NOTE_STYLE))

        side_table = Table(
            [[ecg_cell, crit_cell]],
            colWidths=[d.width + 4, None],
        )
        side_table.setStyle(TableStyle([
            ("VALIGN",       (0,0),(-1,-1), "TOP"),
            ("LEFTPADDING",  (0,0),(-1,-1), 0),
            ("RIGHTPADDING", (0,0),(-1,-1), 4),
            ("TOPPADDING",   (0,0),(-1,-1), 0),
            ("BOTTOMPADDING",(0,0),(-1,-1), 0),
        ]))
        elems.append(side_table)

        if critical:
            elems.append(Spacer(1, 1.5*mm))
            elems.append(critical_box(critical))
        if alert:
            elems.append(Spacer(1, 1.5*mm))
            elems.append(alert_box(alert))

        elems.append(Spacer(1, 4*mm))
        elems.append(HRFlowable(width="100%", thickness=0.5, color=MID_GRAY))
        elems.append(Spacer(1, 3*mm))
        return elems

    # ── 1. STEMI ──────────────────────────────────────────────────────────────
    story += condition_block(
        "1", "Acute Myocardial Infarction — STEMI",
        make_stemi_ecg,
        [
            ["Phase / Feature",     "ECG Finding"],
            ["Hyperacute (min)",    "Tall, peaked T waves (hyperacute T)"],
            ["Early acute (hours)", "ST elevation ≥1 mm (≥2 mm V1-V4) in ≥2 contiguous leads"],
            ["Evolving (hours-days)","T-wave inversions; pathological Q waves develop"],
            ["Established",         "Persistent Q waves; ST normalises"],
            ["Reciprocal changes",  "ST depression in opposite leads (mirror image)"],
            ["Localisation", ""],
            ["Anterior (LAD)",      "ST ↑ V1–V6, I, aVL"],
            ["Inferior (RCA)",      "ST ↑ II, III, aVF; reciprocal ↓ I, aVL"],
            ["Posterior (LCx)",     "ST ↓ V1–V3 + dominant R (ST ↑ equivalent)"],
            ["RV infarct",          "ST ↑ V3R, V4R; seen with inferior MI"],
        ],
        notes="Pathological Q wave: duration ≥40 ms or depth ≥25% of R-wave height.",
        critical="STEMI is a time-critical emergency. Target door-to-balloon ≤90 min.",
        ecg_caption="STEMI: ST elevation with early T-wave changes (V4)",
        col_widths=[38*mm, None]
    )

    # ── 2. NSTEMI ─────────────────────────────────────────────────────────────
    story += condition_block(
        "2", "Acute Coronary Syndrome — NSTEMI / UA",
        make_normal_ecg,   # shows baseline for comparison
        [
            ["Feature",             "ECG Finding"],
            ["ST depression",       "Horizontal or downsloping ≥0.5 mm in ≥2 contiguous leads"],
            ["T-wave inversion",    "Symmetric, ≥1 mm, in leads with prominent R waves"],
            ["Transient ST changes","Changing with symptoms (dynamic ischemia)"],
            ["Wellens' syndrome",   "Biphasic or deep symmetric T inversions V2–V3 (LAD stenosis)"],
            ["De Winter T-waves",   "ST depression + tall peaked T (LAD occlusion equivalent)"],
            ["No ST elevation",     "No pathological Q waves required for NSTEMI"],
            ["Troponin",            "Elevated (distinguishes NSTEMI from unstable angina)"],
        ],
        notes="10–15% of NSTEMI present with a normal ECG — serial ECGs and troponins are essential.",
        alert="Normal ECG does not exclude ACS — obtain serial ECGs and troponins.",
        ecg_caption="NSTEMI: changes may be subtle; compare with baseline ECG",
    )

    # ── 3. Atrial Fibrillation ────────────────────────────────────────────────
    story += condition_block(
        "3", "Atrial Fibrillation",
        make_afib_ecg,
        [
            ["Feature",             "ECG Finding"],
            ["P waves",             "Absent — replaced by fibrillatory f-waves (350–600/min)"],
            ["RR intervals",        "Irregularly irregular — the hallmark"],
            ["f-wave rate",         "350–600 atrial impulses per minute"],
            ["QRS duration",        "Normal (unless aberrant conduction or pre-excitation)"],
            ["Ventricular rate",    "90–170 bpm (uncontrolled); 60–100 bpm (rate-controlled)"],
            ["Baseline",            "Fine, irregular low-voltage oscillations"],
        ],
        notes="Causes: hypertension, valvular disease, thyrotoxicosis, alcohol, HF, lone AF.",
        alert="Assess CHA₂DS₂-VASc score for anticoagulation in all patients with AF.",
        ecg_caption="AF: irregular QRS, absent P waves, fibrillatory baseline",
    )

    # ── 4. Atrial Flutter ─────────────────────────────────────────────────────
    story += condition_block(
        "4", "Atrial Flutter",
        make_aflutter_ecg,
        [
            ["Feature",             "ECG Finding"],
            ["Flutter (F) waves",   "Regular sawtooth waves, 250–350/min; no isoelectric baseline"],
            ["Best seen in",        "Leads II, III, aVF, V1"],
            ["AV conduction ratio", "Typically 2:1 → ventricular rate ~150 bpm"],
            ["Variable ratios",     "3:1, 4:1, or variable block also possible"],
            ["QRS complex",         "Normal morphology (unless aberrant conduction)"],
            ["P waves",             "Flutter waves replace P waves"],
            ["Regularity",          "Ventricular rhythm regular or regularly irregular"],
        ],
        notes="Ventricular rate of exactly 150 bpm should always raise suspicion for atrial flutter with 2:1 block.",
        ecg_caption="Atrial flutter: sawtooth F-waves with 2:1 AV block",
    )

    story.append(PageBreak())

    # ── 5. Ventricular Fibrillation ───────────────────────────────────────────
    story += condition_block(
        "5", "Ventricular Fibrillation",
        make_vfib_ecg,
        [
            ["Feature",             "ECG Finding"],
            ["Waveform",            "Completely chaotic, irregular oscillations — no identifiable complexes"],
            ["Coarse VF (early)",   "High-amplitude irregular waves ~0.5 mV; first 30 sec"],
            ["Fine VF (late)",      "Low-amplitude waves ~0.2 mV after 20–30 sec"],
            ["Rate",                "Indeterminate — 300–500 chaotic ventricular activations/min"],
            ["P waves",             "Absent"],
            ["QRS/T",               "Not identifiable"],
            ["Haemodynamics",       "Cardiac output = zero; immediately fatal"],
        ],
        critical="Initiate CPR immediately. Defibrillate without delay. Epinephrine 1 mg IV q3–5 min.",
        ecg_caption="VF: chaotic oscillations — no organised waveforms",
    )

    # ── 6. Ventricular Tachycardia ────────────────────────────────────────────
    story += condition_block(
        "6", "Ventricular Tachycardia (VT)",
        make_vtach_ecg,
        [
            ["Feature",             "ECG Finding"],
            ["QRS duration",        "≥120 ms (wide complex)"],
            ["Rate",                "100–250 bpm (typically 140–200)"],
            ["Rhythm",              "Regular or slightly irregular"],
            ["P waves",             "Often dissociated from QRS (AV dissociation)"],
            ["Fusion beats",        "Pathognomonic — QRS hybrid of sinus + ectopic"],
            ["Capture beats",       "Narrow QRS during VT (sinus captures ventricle)"],
            ["Concordance",         "All precordial QRS same direction (positive or negative)"],
            ["Axis",                "Extreme axis deviation (NW axis) favours VT"],
            ["Duration",            "Sustained: ≥30 sec; Non-sustained: <30 sec"],
        ],
        notes="Brugada criteria, Vereckei algorithm, or aVR lead algorithm help distinguish VT from SVT with aberrancy. When in doubt, treat as VT.",
        critical="Pulseless VT: defibrillate immediately. Haemodynamically unstable VT: synchronised cardioversion.",
        ecg_caption="VT: wide, regular QRS complexes at rapid rate",
    )

    # ── 7. First-Degree AV Block ──────────────────────────────────────────────
    story += condition_block(
        "7", "First-Degree Atrioventricular Block",
        make_first_degree_block_ecg,
        [
            ["Feature",             "ECG Finding"],
            ["PR interval",         "> 200 ms (0.20 s) — defining criterion"],
            ["Conduction",          "Every P wave is conducted to ventricles (1:1)"],
            ["P wave morphology",   "Normal"],
            ["QRS complex",         "Normal (unless co-existing bundle branch block)"],
            ["Rhythm",              "Regular"],
            ["Site of delay",       "Usually AV node; occasionally His-Purkinje"],
        ],
        notes="Causes: vagal tone (athletes), inferior MI, myocarditis, digitalis, hyperkalaemia, beta-blockers, calcium channel blockers.",
        alert="First-degree block alone rarely requires treatment but warrants monitoring for progression.",
        ecg_caption="1° AV block: PR = 0.38 s (normal ≤ 0.20 s)",
    )

    # ── 8. Second-Degree AV Block ─────────────────────────────────────────────
    story += condition_block(
        "8", "Second-Degree Atrioventricular Block",
        make_wenckebach_ecg,
        [
            ["Type",                "Distinguishing Feature"],
            ["Mobitz I (Wenckebach)","Progressive PR lengthening → dropped QRS; AV node level"],
            ["PR change in Mobitz I","Each PR longer than the last; RR shortens before drop"],
            ["Mobitz II",           "Sudden dropped QRS without prior PR change; infranodal"],
            ["QRS in Mobitz II",    "Usually wide (bundle branch block pattern)"],
            ["2:1 block",           "Every other P blocked; cannot classify without other beats"],
            ["Risk of Mobitz II",   "Higher risk of progression to complete block"],
            ["Wenckebach",          "Usually benign; pacemaker rarely needed"],
        ],
        notes="Mobitz II with wide QRS = high risk of complete heart block → pacemaker often needed.",
        alert="Mobitz II and high-degree block require urgent cardiology referral for pacemaker assessment.",
        ecg_caption="Wenckebach: PR progressively lengthens until 4th P wave is blocked",
    )

    story.append(PageBreak())

    # ── 9. Complete (Third-Degree) AV Block ──────────────────────────────────
    story += condition_block(
        "9", "Third-Degree (Complete) AV Block",
        make_complete_block_ecg,
        [
            ["Feature",             "ECG Finding"],
            ["P waves",             "Present, regular, at normal atrial rate (60–100 bpm)"],
            ["QRS complexes",       "Present, regular, at escape rate (20–60 bpm)"],
            ["P-QRS relationship",  "Completely dissociated — no fixed PR interval"],
            ["QRS morphology",      "Narrow = junctional escape (40–60 bpm); Wide = ventricular escape (20–40 bpm)"],
            ["P rate > QRS rate",   "More P waves than QRS complexes"],
            ["Symptoms",            "Syncope (Stokes-Adams attacks), presyncope, fatigue"],
            ["Causes",              "Inferior MI, Lyme disease, myocarditis, surgical trauma, drugs"],
        ],
        critical="Complete heart block = medical emergency. Atropine (if narrow QRS). Transcutaneous pacing immediately. Cardiology consult for transvenous pacemaker.",
        ecg_caption="3° block: atrial rate 107/min, ventricular escape 43/min — fully dissociated",
    )

    # ── 10. WPW Syndrome ─────────────────────────────────────────────────────
    story += condition_block(
        "10", "Wolff-Parkinson-White (WPW) Syndrome",
        make_wpw_ecg,
        [
            ["Feature (sinus rhythm)", "ECG Finding"],
            ["PR interval",         "Short: < 120 ms (0.12 s) — AV node bypassed"],
            ["Delta wave",          "Slurred, slow upstroke at start of QRS — pre-excitation"],
            ["QRS duration",        "Wide: ≥ 120 ms (fusion of AP + normal conduction)"],
            ["ST-T changes",        "Discordant (opposite to delta wave/QRS direction)"],
            ["Tachyarrhythmias",    ""],
            ["Orthodromic AVRT",    "Narrow QRS tachycardia (most common, 70%)"],
            ["Antidromic AVRT",     "Wide QRS tachycardia (mimics VT)"],
            ["AF with WPW",         "Irregular wide-complex tachycardia — life-threatening"],
        ],
        notes="Localisation: negative delta in lead III → left posterior AP. Negative delta V1 → left-sided AP.",
        critical="AF + WPW: AVOID adenosine, beta-blockers, CCBs, digoxin — may cause VF. Use procainamide or cardioversion.",
        ecg_caption="WPW: short PR, delta (δ) wave slur, wide QRS — dual pathway conduction",
    )

    # ── 11. LVH ───────────────────────────────────────────────────────────────
    story += condition_block(
        "11", "Left Ventricular Hypertrophy (LVH)",
        make_lvh_ecg,
        [
            ["Voltage Criteria",    "Threshold"],
            ["Sokolow-Lyon",        "S (V1) + R (V5 or V6) ≥ 35 mm"],
            ["Cornell (men)",       "R (aVL) + S (V3) > 28 mm"],
            ["Cornell (women)",     "R (aVL) + S (V3) > 20 mm"],
            ["RaVL",                "> 20 mm (women) or > 28 mm (men)"],
            ["Associated findings", ""],
            ["Strain pattern",      "ST depression + asymmetric T-inversion in I, aVL, V5–V6"],
            ["Left axis deviation", "QRS axis < −30°"],
            ["LAE",                 "Bifid P wave ≥120ms in II; biphasic P in V1"],
            ["QRS widening",        "May progress to LBBB"],
        ],
        notes="Sensitivity of voltage criteria is low (~50%) in older adults, obese patients, and those with RBBB. Echocardiography provides definitive diagnosis.",
        alert="Prominent voltage in young athletes may be a normal variant — clinical context is essential.",
        ecg_caption="LVH: deep S in V1, very tall R in V5, lateral strain pattern",
    )

    story.append(PageBreak())

    # ── 12. LBBB ─────────────────────────────────────────────────────────────
    story += condition_block(
        "12", "Left Bundle Branch Block (LBBB)",
        make_lbbb_ecg,
        [
            ["Feature",             "ECG Finding"],
            ["QRS duration",        "≥ 120 ms (complete LBBB)"],
            ["Lead V1",             "Broad, deep QS (or rS) complex"],
            ["Leads I, aVL, V5–V6", "Broad, monophasic R wave (no septal Q)"],
            ["Septal Q waves",      "Absent in I, aVL, V5, V6 (septal activation reversed)"],
            ["ST-T changes",        "Discordant to QRS (ST ↑ where QRS negative; ST ↓ where QRS positive)"],
            ["Sgarbossa criteria",  "Concordant ST ≥1 mm (score 5) or ST ↑ ≥5 mm (score 2) in new LBBB suggest MI"],
            ["Axis",                "Left axis deviation common"],
            ["Causes",              "Hypertension, CAD, dilated cardiomyopathy, aortic stenosis"],
        ],
        critical="New LBBB + chest pain = STEMI equivalent until proven otherwise. Activate cath lab.",
        ecg_caption="LBBB: QS in V1, broad notched R in V6, QRS ≥120ms",
    )

    # ── 13. RBBB ─────────────────────────────────────────────────────────────
    story += condition_block(
        "13", "Right Bundle Branch Block (RBBB)",
        make_rbbb_ecg,
        [
            ["Feature",             "ECG Finding"],
            ["QRS duration",        "≥ 120 ms (complete RBBB)"],
            ["Lead V1",             "rSR' pattern ('M' shape or rabbit ears); terminal R' broad"],
            ["Leads I, V5–V6",      "Wide, slurred S wave (terminal activation rightward)"],
            ["ST-T changes",        "T-wave inversion V1–V3 (secondary to block; not ischemic)"],
            ["Axis",                "Right axis deviation common"],
            ["Causes (new onset)",  "PE, right heart strain, anterior MI, myocarditis"],
            ["Isolated RBBB",       "Often incidental; may be normal variant"],
            ["S₁Q₃T₃ + RBBB",       "Suggests acute right heart strain / PE"],
        ],
        notes="Right bundle branch block is more commonly a benign finding than LBBB. New RBBB warrants investigation for PE or anterior MI.",
        ecg_caption="RBBB: rSR' in V1, wide terminal S in V6, QRS ≥120ms",
    )

    # ── Electrolyte Changes reference ─────────────────────────────────────────
    story.append(Spacer(1, 2*mm))
    story.append(section_header("⚡ ELECTROLYTE & ION EFFECTS ON ECG", "REF", color=TEAL))
    story.append(Spacer(1, 2*mm))

    elec_data = [
        [
            Paragraph("<b>Hyperkalaemia</b>", BOLD_STYLE),
            Paragraph("Tall peaked T waves (early) → PR prolongation → wide QRS → sine wave → asystole", BODY_STYLE),
            Paragraph("<b>Hypokalaemia</b>", BOLD_STYLE),
            Paragraph("Flat/inverted T waves, prominent U waves, ST depression, apparent long QT", BODY_STYLE),
        ],
        [
            Paragraph("<b>Hypercalcaemia</b>", BOLD_STYLE),
            Paragraph("Short QT interval, short ST segment, J waves (Osborn waves)", BODY_STYLE),
            Paragraph("<b>Hypocalcaemia</b>", BOLD_STYLE),
            Paragraph("Prolonged QT interval (prolonged ST segment), unchanged T wave", BODY_STYLE),
        ],
        [
            Paragraph("<b>Long QT Syndrome</b>", BOLD_STYLE),
            Paragraph("QTc > 450 ms (men) / 460 ms (women); risk of torsades de pointes", BODY_STYLE),
            Paragraph("<b>Digoxin effect</b>", BOLD_STYLE),
            Paragraph("Scooped ST depression ('reverse tick'), short QT, PR prolongation", BODY_STYLE),
        ],
    ]
    elec_table = Table(elec_data, colWidths=[35*mm, 60*mm, 35*mm, None])
    elec_table.setStyle(TableStyle([
        ("BACKGROUND",    (0,0),(-1,-1), LIGHT_GRAY),
        ("ROWBACKGROUNDS",(0,0),(-1,-1), [WHITE, LIGHT_BLUE, WHITE]),
        ("GRID",          (0,0),(-1,-1), 0.3, MID_GRAY),
        ("VALIGN",        (0,0),(-1,-1), "TOP"),
        ("TOPPADDING",    (0,0),(-1,-1), 3),
        ("BOTTOMPADDING", (0,0),(-1,-1), 3),
        ("LEFTPADDING",   (0,0),(-1,-1), 4),
    ]))
    story.append(elec_table)
    story.append(Spacer(1, 4*mm))

    # ── ECG Normal Values ─────────────────────────────────────────────────────
    story.append(section_header("📏 NORMAL ECG INTERVALS & VALUES", "NV", color=colors.HexColor("#2E7D32")))
    story.append(Spacer(1, 2*mm))

    nv_data = [
        ["PR interval",       "120–200 ms (3–5 small squares)"],
        ["QRS duration",      "60–100 ms (<120 ms; 3 small squares)"],
        ["QT interval",       "350–440 ms (rate-dependent)"],
        ["QTc (corrected)",   "Men: ≤ 450 ms | Women: ≤ 460 ms (Bazett formula)"],
        ["P-wave duration",   "< 120 ms; amplitude < 2.5 mm in limb leads"],
        ["P-wave axis",       "0° to +75° (upright in I, II; inverted in aVR)"],
        ["QRS axis",          "−30° to +90° (normal); −30° to −90° = LAD; >+90° = RAD"],
        ["Heart rate",        "60–100 bpm (sinus rhythm)"],
        ["Paper speed",       "25 mm/sec | 1 small square = 40 ms, 1 large = 200 ms"],
        ["Amplitude",         "Standard: 10 mm/mV | 1 small square = 0.1 mV"],
    ]
    nv_table_data = []
    for r in nv_data:
        nv_table_data.append([
            Paragraph(f"<b>{r[0]}</b>", BOLD_STYLE),
            Paragraph(r[1], BODY_STYLE),
        ])
    nv_table = Table(nv_table_data, colWidths=[48*mm, None])
    nv_table.setStyle(TableStyle([
        ("ROWBACKGROUNDS",(0,0),(-1,-1), [WHITE, LIGHT_BLUE]),
        ("GRID",          (0,0),(-1,-1), 0.3, MID_GRAY),
        ("VALIGN",        (0,0),(-1,-1), "MIDDLE"),
        ("TOPPADDING",    (0,0),(-1,-1), 3),
        ("BOTTOMPADDING", (0,0),(-1,-1), 3),
        ("LEFTPADDING",   (0,0),(-1,-1), 4),
    ]))
    story.append(nv_table)
    story.append(Spacer(1, 4*mm))

    # ── STEMI Territory localisation table ────────────────────────────────────
    story.append(section_header("🗺  STEMI TERRITORY LOCALISATION", "LOC",
                                color=colors.HexColor("#6A1515")))
    story.append(Spacer(1, 2*mm))

    loc_data = [
        [Paragraph("<b>Territory</b>", ParagraphStyle("LH",fontName="Helvetica-Bold",fontSize=7.5,textColor=WHITE)),
         Paragraph("<b>ST Elevation Leads</b>", ParagraphStyle("LH",fontName="Helvetica-Bold",fontSize=7.5,textColor=WHITE)),
         Paragraph("<b>Reciprocal ST Depression</b>", ParagraphStyle("LH",fontName="Helvetica-Bold",fontSize=7.5,textColor=WHITE)),
         Paragraph("<b>Artery (usually)</b>", ParagraphStyle("LH",fontName="Helvetica-Bold",fontSize=7.5,textColor=WHITE))],
        ["Anterior",      "V1–V4",         "II, III, aVF",      "LAD"],
        ["Anterolateral",  "V4–V6, I, aVL", "II, III, aVF",      "LAD (diagonal) or LCx"],
        ["Lateral",       "I, aVL, V5–V6", "II, III, aVF",      "LCx or diagonal"],
        ["Inferior",      "II, III, aVF",  "I, aVL",            "RCA (80%) or LCx"],
        ["Posterior",     "V7–V9 (direct); ST ↓ V1–V3", "None", "RCA or LCx"],
        ["RV",            "V3R, V4R",      "None",              "Proximal RCA"],
        ["Septal",        "V1–V2",         "Variable",          "Septal perforators (LAD)"],
    ]
    loc_styled = []
    for i, r in enumerate(loc_data):
        if i == 0:
            loc_styled.append(r)
        else:
            loc_styled.append([Paragraph(r[j], SMALL_STYLE) for j in range(4)])
    loc_table = Table(loc_styled, colWidths=[30*mm, 46*mm, 46*mm, None])
    loc_table.setStyle(TableStyle([
        ("BACKGROUND",    (0,0),(-1,0),  colors.HexColor("#6A1515")),
        ("ROWBACKGROUNDS",(0,1),(-1,-1), [WHITE, LIGHT_GRAY]),
        ("GRID",          (0,0),(-1,-1), 0.3, MID_GRAY),
        ("VALIGN",        (0,0),(-1,-1), "MIDDLE"),
        ("TOPPADDING",    (0,0),(-1,-1), 2.5),
        ("BOTTOMPADDING", (0,0),(-1,-1), 2.5),
        ("LEFTPADDING",   (0,0),(-1,-1), 4),
    ]))
    story.append(loc_table)

    # ── Build ──────────────────────────────────────────────────────────────────
    doc.build(story, onFirstPage=on_first_page, onLaterPages=on_page)
    print(f"PDF generated: {OUTPUT}")

if __name__ == "__main__":
    build()
Running Command

cd /home/daytona/workspace/ecg-reference && python build_ecg_pdf.py 2>&1

Editing File

~/ecg-reference/build_ecg_pdf.py

Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/ecg-reference/build_ecg_pdf.py'
Now I need to pass the col_widths argument down to criteria_table. Let me find where criteria_table is called inside condition_block and update it:
Editing File

~/ecg-reference/build_ecg_pdf.py

Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/ecg-reference/build_ecg_pdf.py'
Running Command

cd /home/daytona/workspace/ecg-reference && python build_ecg_pdf.py 2>&1

Running Command

ls -lh /home/daytona/workspace/ecg-reference/ECG_Reference_Card.pdf

Your ECG Reference Card PDF is ready:

ECG Reference Card

PDF Document · PDF


Here's what's included across the pages:
Page 1 - Quick-Reference Summary Table All 14 ECG conditions side-by-side: key feature, rate, rhythm, P wave, and QRS characteristics at a glance.
Pages 2-4 - Individual Condition Cards (13 conditions) Each card includes a custom-drawn ECG waveform strip and a criteria table:
  1. STEMI - ST evolution stages + territory localisation
  2. NSTEMI/UA - Wellens' syndrome, De Winter T-waves
  3. Atrial Fibrillation - f-waves, irregular RR
  4. Atrial Flutter - sawtooth pattern, 2:1 block
  5. Ventricular Fibrillation - coarse vs fine VF
  6. Ventricular Tachycardia - Brugada/Vereckei criteria mentioned
  7. First-Degree AV Block - PR > 200 ms
  8. Second-Degree AV Block - Wenckebach vs Mobitz II
  9. Third-Degree (Complete) AV Block - P-QRS dissociation
  10. WPW Syndrome - delta wave, pathway localisation
  11. LVH - Sokolow-Lyon + Cornell voltage criteria
  12. LBBB - Sgarbossa criteria for MI in LBBB
  13. RBBB - rSR' pattern, S₁Q₃T₃
Bottom of Page 4 - Reference Tables
  • Electrolyte effects (hyperkalaemia, hypokalaemia, hypercalcaemia, long QT, digoxin)
  • Normal ECG intervals (PR, QRS, QTc, axes, paper speed, amplitude)
  • STEMI territory localisation map (artery, ST elevation leads, reciprocal leads)
This is a shared conversation. Sign in to Orris to start your own chat.