Teach me as a paeditrician/Neonatologist for the topic Pyloric stenosis

Reading File
Now let me search the medical library for pyloric stenosis content:
Finding Sources
Finding Sources
Searching PubMed

"pyloric stenosis" infantile hypertrophic

Excellent - rich content found. Let me now read the most relevant sections in parallel:
Reading File
Reading File
Reading File
Reading File
Reading File
Excellent data gathered. Let me now get the radiology imaging details and Harriet Lane / Mulholland clinical sections, plus fetch the key ultrasound image:
Reading File
Reading File
Reading File
Loading Image
Let me fetch one more key image and get the Mulholland text on surgical management:
Loading Image
Loading Image
Reading File
I now have comprehensive, authoritative content from multiple textbooks. Let me compile the full teaching session.

Infantile Hypertrophic Pyloric Stenosis (IHPS) - A Complete Teaching Session for the Paediatrician/Neonatologist


1. OVERVIEW & DEFINITION

Infantile Hypertrophic Pyloric Stenosis (IHPS) is a condition of progressive hypertrophy and hyperplasia of the pyloric muscularis propria, causing gastric outlet obstruction in young infants. It is not a congenital disease - the pyloric muscle is normal at birth and hypertrophies postnatally.
  • Robbins, Cotran & Kumar Pathologic Basis of Disease, p. 703
  • Schwartz's Principles of Surgery, p. 1750

2. EPIDEMIOLOGY

ParameterData
Incidence~1 in 300 live births (~0.2-0.4% in white infants; slightly lower in Black infants)
Sex ratioMale:Female = 4-5:1
Birth orderMore common in first-born males
SiblingsRisk rises to ~6% in siblings of affected patients
TwinsHigh concordance in monozygotic twins
Peak age3-6 weeks (range 2-12 weeks); rare after 12 weeks
At-risk groups:
  • First-born males
  • Premature infants
  • Young maternal age
  • Infants with Turner syndrome or trisomy 18
  • Family history: if a mother had IHPS, offspring risk is markedly increased (Carter effect - females must accumulate more genetic burden to express the disease, so if a mother is affected, her sons are at very high risk)
Macrolide exposure: Erythromycin or azithromycin exposure - oral or via breast milk - in the first 2 weeks of life is associated with IHPS. This is a critical neonatal prescribing point. - Robbins, Cotran & Kumar, p. 703

3. PATHOPHYSIOLOGY & EMBRYOLOGY

The Pylorus

The pylorus is the distal sphincteric region of the stomach. In IHPS, both circular (predominantly) and longitudinal muscle layers hypertrophy, causing:
  • Severe stenosis of the pyloric canal
  • Obstruction of food passage
  • Massive gastric distension
  • Progressive projectile vomiting

The "Olive"

The hypertrophied pyloric muscle forms a firm, ovoid, 1-2 cm palpable mass in the right upper quadrant - pathognomonic when found.

Why Hypertrophy Occurs

  • The exact cause is unknown
  • Genome-wide association studies have linked genes related to GI development
  • A significant locus at chromosome 11q23.3 has been identified (related to cholesterol regulation - possible dietary link)
  • Loss of nitric oxide synthase (NOS) in the myenteric plexus has been implicated in failure of pyloric relaxation
  • Mucosal and submucosal edema may further exacerbate muscular outflow obstruction

4. CLINICAL PRESENTATION

Classic Sequence

Week 1-2: Infant feeds well, no vomiting Week 2-4: Onset of non-bilious vomiting after feeds Week 3-6: Vomiting becomes progressively forceful and projectile Late stage: Constant hunger, repeated unsuccessful feeding, dehydration, weight loss

Key Symptoms

  1. Non-bilious projectile vomiting - The hallmark. Non-bilious because obstruction is proximal to the ampulla of Vater. Vomiting can be forceful enough to land across the room.
  2. Hunger after vomiting - The infant vomits and immediately wants to feed again ("hungry vomiter").
  3. Weight loss / failure to thrive - Progressive as caloric intake falls.
  4. Reduced wet diapers - Sign of dehydration and decreased urine output.
  5. Constipation - Decreased stool from poor intake ("starvation stools" - small, green mucoid).

Signs on Examination

FindingDescription
Palpable "olive"Firm, mobile, 1-2 cm ovoid mass in RUQ/midepigastrium - pathognomonic (found in up to 90% by experienced hands; now <30% in early presentations due to earlier diagnosis)
Visible gastric peristalsisLeft-to-right peristaltic waves across upper abdomen before vomiting
Dehydration signsSunken fontanelle, dry mucous membranes, reduced skin turgor
JaundiceIndirect hyperbilirubinemia occurs in ~2-5% (mechanism unclear - possibly decreased hepatic glucuronyl transferase activity from poor caloric intake)
Clinical pearl: To palpate the olive, you need an empty stomach (pass NGT first), a quiet/sleeping infant, and patience. The examiner places fingertips below the liver edge in the RUQ and feels for a smooth, hard, mobile mass during quiet respiration. - Mulholland and Greenfield's Surgery, p. 5544

5. METABOLIC DERANGEMENT - THE CLASSIC BIOCHEMISTRY

This is one of the most tested topics in neonatology/paediatrics.

Why it happens:

  • Repeated vomiting causes loss of gastric juice (rich in HCl, K⁺, H⁺)
  • This results in: Hypochloraemic, Hypokalaemic Metabolic Alkalosis

Step-by-step mechanism:

Vomiting → Loss of H⁺ + Cl⁻ → Metabolic alkalosis + Hypochloraemia
           → Volume depletion → Aldosterone activation
           → Renal Na⁺ retention + K⁺ loss → Hypokalaemia

Initially: Kidney excretes HCO₃⁻ in urine → Alkaline urine (compensatory)

Late stage: Volume depletion severe → Kidney prioritises Na⁺ over acid-base
            → H⁺ exchanged for Na⁺ in distal tubule
            → PARADOXICAL ACIDURIA (acidic urine despite systemic alkalosis)

Blood Gas Pattern:

ParameterValue
pHElevated (>7.45)
pCO₂Elevated (compensatory hypoventilation)
HCO₃⁻Markedly elevated (>30 mEq/L)
Cl⁻Low (<95 mEq/L)
K⁺Low
Na⁺May be low (hyponatraemia from vomiting + free water intake)
Danger: Bicarbonate >30 mEq/L = relative contraindication to general anaesthesia due to diminished respiratory drive and risk of postoperative apnoea. Surgery must wait until metabolic alkalosis is corrected. - Current Surgical Therapy 14e, p. block 15

6. INVESTIGATIONS

Step 1: Bloods (ALL infants)

  • Full metabolic panel: Na⁺, K⁺, Cl⁻, HCO₃⁻, pH
  • Blood glucose (hypoglycaemia from poor intake)
  • Bilirubin if jaundiced

Step 2: Imaging

Ultrasound - GOLD STANDARD

Sensitivity 95-98%, Specificity up to 100%
MeasurementNormalIHPS
Pyloric muscle thickness<3 mm≥4 mm (most reliable)
Pyloric channel length<12 mm≥15-17 mm
Ultrasound signs to know:
  • Target/doughnut sign: Hypoechoic thickened muscle ring on transverse view
  • Shoulder sign: Hypertrophied pylorus bulging into the gastric antrum
  • Nipple sign: Hypertrophic double-layered mucosa protruding into the stomach
  • Cervix sign: The overall sonographic appearance resembles a uterine cervix
  • Real-time: failure of fluid to pass through the pylorus; exaggerated gastric peristalsis
Note for neonates: Younger/preterm babies may have smaller absolute values and still be abnormal. Clinical correlation is mandatory. If pyloric muscle thickness is >3 mm on repeat ultrasound, IHPS is confirmed regardless of age/weight. - Grainger & Allison's Diagnostic Radiology
Ultrasound image - pyloric stenosis showing thickened pyloric wall with measurements:
Ultrasound of pyloric stenosis - coronal plane showing pylorus with muscle wall thickness 4.3 mm and channel length 2.26 cm
Pylorus ultrasound (coronal plane): muscle wall thickness 4.3 mm (measurement 2), pyloric channel length 2.26 cm (measurement 1) - both above diagnostic thresholds for IHPS.
Classic 3-panel imaging (ultrasound + barium):
Three-panel image: A - transverse US showing pyloric wall thickness >4mm; B - horizontal US showing channel length >14mm; C - barium contrast showing narrowed pyloric end with distended fundus (F)
A: Transverse US - pyloric muscle wall thickness >4mm. B: US showing channel length >14mm. C: Contrast study - narrowed pyloric channel (arrow) and distended stomach fundus (F). - The Developing Human, Clinically Oriented Embryology

Upper GI Contrast Series (when US equivocal)

  • "String sign" / "railroad track sign" - contrast trickling through narrow elongated pyloric channel
  • "Mushroom/umbrella sign" - indentation of pyloric mass on the gastric antrum
  • Advantages: also evaluates for malrotation, GOR disease, antroduodenal webs in the differential
  • Disadvantage: contrast remains in a poorly emptying stomach; radiation

7. DIFFERENTIAL DIAGNOSIS

ConditionKey Distinguishing Feature
Gastroesophageal Reflux Disease (GORD)Most common DDx; non-projectile, positional, responds to thickened feeds; normal US
PylorospasmTemporary; ultrasound normal or equivocal; usually resolves spontaneously
OverfeedingHistory; no organic cause; thriving infant
Malrotation with midgut volvulusBILIOUS vomiting - urgent emergency
Duodenal atresia/stenosisBILIOUS vomiting, "double bubble" on AXR; often Down syndrome
Antral/prepyloric webSimilar presentation; seen on contrast study
Adrenal insufficiency (CAH)Vomiting + hypoglycaemia + hyponatraemia + hyperkalaemia (opposite electrolytes)
Raised ICPProjectile vomiting without feeds; other neuro signs
Gastritis / cow's milk protein allergyBlood in stool; eosinophils; responds to formula change
The single most important differentiator from surgical emergencies: IHPS vomiting is NON-BILIOUS. Bilious vomiting in a neonate = surgical emergency until proven otherwise.

8. MANAGEMENT

Phase 1: Resuscitation (NEVER rush to theatre)

IHPS is never a surgical emergency. The child must be metabolically optimised first.
IV Fluid Protocol:
  • 1-2 boluses of normal saline (20 mL/kg) for dehydration
  • Maintenance: D5/0.45% NaCl + KCl 2-4 mEq/kg/day at 1.5x maintenance rate (~150-175 mL/kg/day)
  • NO potassium until urine output is confirmed (>2 mL/kg/hr)
  • Check electrolytes every 6-12 hours
Goals before surgery ("Go criteria"):
ParameterTarget
Chloride≥90-95 mEq/L
Bicarbonate≤30 mEq/L
Potassium≥3 mEq/L
Urine output>2 mL/kg/hr (wet diapers returning)
  • Nasogastric tube (NGT) to decompress the stomach
  • NBM (nil by mouth)
  • Resuscitation typically takes 24-48 hours

Phase 2: Surgery - Fredet-Ramstedt Pyloromyotomy

The definitive treatment. Not a pyloroplasty or bypass - a myotomy only.
Technique:
  • A single longitudinal incision through the anterior wall of the hypertrophied pyloric muscle (avascular plane)
  • Carried from stomach side to the duodenal side (where muscle softens)
  • Submucosal bulging confirms complete myotomy - both muscle edges must be freely mobile
  • Leak test: 30-60 mL of air injected via orogastric tube - confirms no mucosal perforation
Approaches:
ApproachNotes
Laparoscopic (preferred at most paediatric centres)3 ports: umbilical port + two 3-mm stab incisions; shorter hospital stay, better cosmesis
Open - umbilical incisionCosmetically favourable
Open - right upper quadrant transverse incisionTraditional approach
Recent systematic review (2024) comparing umbilical vs. right upper transverse incisions found no significant difference in outcomes.

Phase 3: Postoperative Feeding

  • Start feeds 6-8 hours after recovery from anaesthesia
  • Ad libitum feeding with a 60 mL limit (easiest and best approach per current evidence)
  • Most infants tolerate feeds and are discharged within 24-36 hours
  • Some post-op vomiting is expected and normal (mucosal oedema); true failure to feed at 48-72 hours suggests incomplete myotomy

9. COMPLICATIONS OF PYLOROMYOTOMY

ComplicationPresentationManagement
Incomplete myotomyOngoing projectile vomiting at 48-72 hrsReturn to OR for revision myotomy
Mucosal perforationIntraoperative: air leak on leak test; Postoperative: sepsis, peritonitisClose perforation + buttress with omentum; OR rotate pylorus 180° and redo myotomy
Wound infectionErythema, dischargeAntibiotics ± wound care
Postoperative apnoea1st 24 hrs - especially if alkalosis not correctedApnoea monitoring; correct alkalosis pre-op
PeritonitisIf unrecognised mucosal perforationEmergency laparotomy + peritoneal washout
A 2024 meta-analysis found that prophylactic preoperative antibiotics reduce surgical site infections in IHPS patients undergoing pyloromyotomy (Gulia et al., Pediatr Surg Int 2024).

10. NON-SURGICAL MANAGEMENT (Medical Pyloromyotomy)

IV Atropine has been used as an alternative to surgery in selected centres (Japan, some European centres):
  • Atropine sulphate IV relaxes the pyloric muscle
  • Requires prolonged hospitalisation (weeks) and strict protocols
  • Success rate ~85-90% with prolonged treatment
  • Not widely adopted - surgery remains first-line in most guidelines
  • May be considered in high anaesthetic risk infants

11. PROGNOSIS

  • Surgical cure rate: virtually 100% with adequate pyloromyotomy
  • Mortality is distinctly rare in the absence of concomitant medical problems
  • Recovery typically uncomplicated
  • Outcomes are better when operated by specialty-trained paediatric surgeons (lower complication rate, shorter hospital stay vs. general surgeons)
  • Long-term: no predisposition to adult GI disease; normal GI function

12. KEY MNEMONICS & EXAM PEARLS

"IHPS = Hungry, Non-bilious, 3-6 weeks, Male"

  • H - Hungry after vomiting (wants to re-feed immediately)
  • N - Non-bilious vomiting (key distinguisher)
  • 3-6 - Age of presentation in weeks
  • M - Male predominantly (4-5:1 ratio)

Metabolic Alkalosis Mnemonic: "HypoChloraemic HypoKalaemic Metabolic Alkalosis with Paradoxical Aciduria"

  • Cl⁻ goes DOWN (lost in vomit)
  • K⁺ goes DOWN (aldosterone effect + lost in vomit)
  • HCO₃⁻ goes UP (metabolic alkalosis)
  • Urine pH goes DOWN (paradoxical aciduria - late sign)

US Criteria: "4 and 14" (or "4 and 16")

  • Muscle thickness ≥4 mm
  • Channel length ≥14-16 mm (varies by source; use ≥16 mm as diagnostic threshold)

"Never rush to theatre" - Surgery is never an emergency

  • Correct the metabolic derangement FIRST
  • BiCarbonate >30 = do NOT anaesthetise

13. CLINICAL SCENARIO PRACTICE

A 5-week-old male, first-born, presents with 1-week history of increasing non-bilious projectile vomiting after feeds. He feeds vigorously immediately after vomiting. On exam: slightly sunken fontanelle, firm mobile mass palpable in RUQ. Blood gas: pH 7.52, PCO₂ 52, HCO₃⁻ 38, Cl⁻ 85, K⁺ 2.8.
Q: What is the diagnosis? IHPS (non-bilious projectile vomiting, age, sex, palpable olive, hypochloraemic hypokalaemic metabolic alkalosis)
Q: What do you do next?
  1. Confirm with pyloric ultrasound (muscle >4 mm, channel >16 mm)
  2. IV resuscitation: NS bolus + D5/0.45% NaCl + KCl
  3. NGT for gastric decompression
  4. Monitor UO - add K⁺ only when voiding
  5. Repeat electrolytes every 6-12 hrs
  6. Surgery when: Cl⁻ ≥90-95, HCO₃⁻ ≤30, K⁺ ≥3, adequate UO
Q: Why NOT take this child to theatre now? HCO₃⁻ 38 mEq/L - dangerous risk of post-op respiratory depression and apnoea under general anaesthesia.

Summary Table

FeatureIHPS
Incidence1 in 300
SexMale >>Female (4-5:1)
Age3-6 weeks
VomitingNon-bilious, projectile
Key signPalpable "olive"
ElectrolytesHypochloraemic, hypokalaemic metabolic alkalosis
ImagingUltrasound gold standard (muscle ≥4mm, length ≥16mm)
SurgeryFredet-Ramstedt pyloromyotomy
Pre-op mustCorrect metabolic alkalosis (HCO₃⁻ <30, Cl⁻ >90)
PrognosisExcellent - virtually 100% cure

Sources:

above all content in pdf

Reading File
Running Command

ls /home/daytona/workspace/ 2>/dev/null || mkdir -p /home/daytona/workspace/pyloric-stenosis

Running Command

mkdir -p /home/daytona/workspace/pyloric-stenosis && echo "done"

Now I'll create the full PDF using ReportLab with professional formatting, images, and tables:
Writing File

~/pyloric-stenosis/generate_pdf.py

#!/usr/bin/env python3
"""Generate a comprehensive Pyloric Stenosis teaching PDF for Paediatricians/Neonatologists."""

import urllib.request
import os
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm, mm
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
    HRFlowable, Image, KeepTogether, PageBreak
)
from reportlab.platypus.tableofcontents import TableOfContents
from reportlab.pdfgen import canvas
from io import BytesIO

OUTPUT = "/home/daytona/workspace/pyloric-stenosis/Pyloric_Stenosis_Paediatrics.pdf"

# ── Colour palette ──────────────────────────────────────────────────────────
C_HEADER    = colors.HexColor("#1a3a5c")   # deep navy
C_SUBHDR    = colors.HexColor("#2e6da4")   # medium blue
C_ACCENT    = colors.HexColor("#e8f0fa")   # light blue fill
C_TABLE_HDR = colors.HexColor("#2e6da4")
C_TABLE_ALT = colors.HexColor("#f0f5fb")
C_RED       = colors.HexColor("#c0392b")
C_GREEN     = colors.HexColor("#1e8449")
C_ORANGE    = colors.HexColor("#d35400")
C_YELLOW    = colors.HexColor("#fff9e6")
C_PEARL     = colors.HexColor("#f4f8ff")
C_BORDER    = colors.HexColor("#2e6da4")

PAGE_W, PAGE_H = A4
MARGIN = 2 * cm

# ── Styles ───────────────────────────────────────────────────────────────────
base = getSampleStyleSheet()

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

title_style = S("TitleMain",
    fontName="Helvetica-Bold", fontSize=22, textColor=colors.white,
    alignment=TA_CENTER, spaceAfter=4)

subtitle_style = S("Subtitle",
    fontName="Helvetica", fontSize=13, textColor=colors.HexColor("#d0e8ff"),
    alignment=TA_CENTER, spaceAfter=2)

author_style = S("Author",
    fontName="Helvetica-Oblique", fontSize=10, textColor=colors.HexColor("#b0cce8"),
    alignment=TA_CENTER, spaceAfter=0)

h1 = S("H1",
    fontName="Helvetica-Bold", fontSize=14, textColor=colors.white,
    backColor=C_HEADER, borderPad=6,
    spaceBefore=14, spaceAfter=6, leftIndent=0, rightIndent=0,
    leading=18)

h2 = S("H2",
    fontName="Helvetica-Bold", fontSize=12, textColor=C_SUBHDR,
    spaceBefore=10, spaceAfter=4, leading=16,
    borderPadding=(0, 0, 2, 0))

h3 = S("H3",
    fontName="Helvetica-BoldOblique", fontSize=10.5, textColor=C_HEADER,
    spaceBefore=7, spaceAfter=3, leading=14)

body = S("Body",
    fontName="Helvetica", fontSize=9.5, leading=14,
    spaceBefore=2, spaceAfter=3, alignment=TA_JUSTIFY)

bullet = S("Bullet",
    fontName="Helvetica", fontSize=9.5, leading=13,
    leftIndent=14, bulletIndent=4, spaceBefore=1, spaceAfter=1,
    bulletText="\u2022")

bullet2 = S("Bullet2",
    fontName="Helvetica", fontSize=9, leading=12,
    leftIndent=28, bulletIndent=18, spaceBefore=1, spaceAfter=1,
    bulletText="\u25e6")

note_style = S("Note",
    fontName="Helvetica-Oblique", fontSize=8.8, leading=12,
    backColor=C_YELLOW, borderColor=C_ORANGE, borderWidth=1,
    borderPadding=6, textColor=colors.HexColor("#7d3600"),
    spaceBefore=5, spaceAfter=5)

pearl_style = S("Pearl",
    fontName="Helvetica-BoldOblique", fontSize=9, leading=13,
    backColor=C_PEARL, borderColor=C_BORDER, borderWidth=1,
    borderPadding=7, textColor=C_HEADER,
    spaceBefore=4, spaceAfter=4)

caption_style = S("Caption",
    fontName="Helvetica-Oblique", fontSize=8, textColor=colors.HexColor("#555555"),
    alignment=TA_CENTER, spaceBefore=2, spaceAfter=6)

code_style = S("Code",
    fontName="Courier", fontSize=8.5, leading=12,
    backColor=colors.HexColor("#f5f5f5"), borderColor=colors.HexColor("#cccccc"),
    borderWidth=0.5, borderPadding=5,
    spaceBefore=4, spaceAfter=4)

# ── Helpers ───────────────────────────────────────────────────────────────────
def H1(text):
    return Paragraph(f"&nbsp; {text}", h1)

def H2(text):
    return [
        HRFlowable(width="100%", thickness=1.5, color=C_SUBHDR, spaceAfter=1),
        Paragraph(text, h2)
    ]

def H3(text):
    return Paragraph(text, h3)

def B(text):
    return Paragraph(text, body)

def BUL(text):
    return Paragraph(text, bullet)

def BUL2(text):
    return Paragraph(text, bullet2)

def NOTE(text):
    return Paragraph(f"<b>Note:</b> {text}", note_style)

def PEARL(text):
    return Paragraph(f"<b>Clinical Pearl:</b> {text}", pearl_style)

def SP(n=4):
    return Spacer(1, n)

def tbl(data, col_widths, header=True, alt=True):
    t = Table(data, colWidths=col_widths, repeatRows=1 if header else 0)
    style_cmds = [
        ("FONTNAME",    (0, 0), (-1, -1), "Helvetica"),
        ("FONTSIZE",    (0, 0), (-1, -1), 8.5),
        ("GRID",        (0, 0), (-1, -1), 0.4, colors.HexColor("#aabbcc")),
        ("ROWBACKGROUNDS", (0, 0), (-1, -1), [colors.white, C_TABLE_ALT] if alt else [colors.white]),
        ("VALIGN",      (0, 0), (-1, -1), "MIDDLE"),
        ("TOPPADDING",  (0, 0), (-1, -1), 4),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
        ("LEFTPADDING", (0, 0), (-1, -1), 5),
        ("RIGHTPADDING",(0, 0), (-1, -1), 5),
    ]
    if header:
        style_cmds += [
            ("BACKGROUND",  (0, 0), (-1, 0), C_TABLE_HDR),
            ("TEXTCOLOR",   (0, 0), (-1, 0), colors.white),
            ("FONTNAME",    (0, 0), (-1, 0), "Helvetica-Bold"),
            ("FONTSIZE",    (0, 0), (-1, 0), 9),
        ]
    t.setStyle(TableStyle(style_cmds))
    return t

def download_image(url, filename):
    path = f"/home/daytona/workspace/pyloric-stenosis/{filename}"
    if not os.path.exists(path):
        try:
            urllib.request.urlretrieve(url, path)
        except Exception as e:
            print(f"Failed to download {url}: {e}")
            return None
    return path

# ── Page number canvas ────────────────────────────────────────────────────────
class NumberedCanvas(canvas.Canvas):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._saved_page_states = []

    def showPage(self):
        self._saved_page_states.append(dict(self.__dict__))
        self._startPage()

    def save(self):
        num_pages = len(self._saved_page_states)
        for state in self._saved_page_states:
            self.__dict__.update(state)
            self.draw_page_number(num_pages)
            super().showPage()
        super().save()

    def draw_page_number(self, page_count):
        pg = self._pageNumber
        self.setFont("Helvetica", 8)
        self.setFillColor(colors.HexColor("#888888"))
        self.drawRightString(PAGE_W - MARGIN, 1.2 * cm,
                             f"Page {pg} of {page_count}")
        self.drawString(MARGIN, 1.2 * cm,
                        "Infantile Hypertrophic Pyloric Stenosis | Paediatrics/Neonatology Teaching")
        self.setStrokeColor(colors.HexColor("#cccccc"))
        self.line(MARGIN, 1.5 * cm, PAGE_W - MARGIN, 1.5 * cm)


# ── Build content ─────────────────────────────────────────────────────────────
def build_story():
    story = []
    avail_w = PAGE_W - 2 * MARGIN

    # ── COVER PAGE ────────────────────────────────────────────────────────────
    story.append(Spacer(1, 1.5 * cm))

    # Cover banner
    cover_data = [[Paragraph(
        "<b>INFANTILE HYPERTROPHIC<br/>PYLORIC STENOSIS</b>",
        S("CoverTitle", fontName="Helvetica-Bold", fontSize=26,
          textColor=colors.white, alignment=TA_CENTER, leading=34))]]
    ct = Table(cover_data, colWidths=[avail_w])
    ct.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), C_HEADER),
        ("TOPPADDING", (0, 0), (-1, -1), 22),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 22),
        ("LEFTPADDING", (0, 0), (-1, -1), 10),
        ("RIGHTPADDING", (0, 0), (-1, -1), 10),
        ("ROUNDEDCORNERS", [8, 8, 8, 8]),
    ]))
    story.append(ct)
    story.append(SP(12))

    # Subtitle box
    sub_data = [[Paragraph(
        "A Comprehensive Teaching Module for Paediatricians &amp; Neonatologists",
        S("SubTitle2", fontName="Helvetica-Oblique", fontSize=13,
          textColor=C_HEADER, alignment=TA_CENTER))]]
    st = Table(sub_data, colWidths=[avail_w])
    st.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), C_ACCENT),
        ("TOPPADDING", (0, 0), (-1, -1), 12),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 12),
        ("BOX", (0, 0), (-1, -1), 1, C_BORDER),
    ]))
    story.append(st)
    story.append(SP(20))

    # Key facts panel on cover
    kf_data = [
        [Paragraph("<b>Key Facts at a Glance</b>",
                   S("kfhdr", fontName="Helvetica-Bold", fontSize=11,
                     textColor=C_HEADER, alignment=TA_CENTER)), "", ""],
        [Paragraph("Incidence\n<b>1 in 300 live births</b>",
                   S("kf", fontName="Helvetica", fontSize=9.5,
                     textColor=C_HEADER, alignment=TA_CENTER, leading=13)),
         Paragraph("Peak Age\n<b>3 - 6 weeks</b>",
                   S("kf", fontName="Helvetica", fontSize=9.5,
                     textColor=C_HEADER, alignment=TA_CENTER, leading=13)),
         Paragraph("Sex Ratio\n<b>Male : Female = 4-5:1</b>",
                   S("kf", fontName="Helvetica", fontSize=9.5,
                     textColor=C_HEADER, alignment=TA_CENTER, leading=13))],
        [Paragraph("Vomiting\n<b>Non-bilious, Projectile</b>",
                   S("kf2", fontName="Helvetica", fontSize=9.5,
                     textColor=C_HEADER, alignment=TA_CENTER, leading=13)),
         Paragraph("Metabolics\n<b>Hypochloraemic Hypokalaemic Alkalosis</b>",
                   S("kf2", fontName="Helvetica", fontSize=9.5,
                     textColor=C_HEADER, alignment=TA_CENTER, leading=13)),
         Paragraph("Treatment\n<b>Fredet-Ramstedt Pyloromyotomy</b>",
                   S("kf2", fontName="Helvetica", fontSize=9.5,
                     textColor=C_HEADER, alignment=TA_CENTER, leading=13))],
    ]
    kf_col = avail_w / 3
    kft = Table(kf_data, colWidths=[kf_col, kf_col, kf_col])
    kft.setStyle(TableStyle([
        ("SPAN",           (0, 0), (2, 0)),
        ("BACKGROUND",     (0, 0), (2, 0), C_HEADER),
        ("TEXTCOLOR",      (0, 0), (2, 0), colors.white),
        ("BACKGROUND",     (0, 1), (2, 2), C_ACCENT),
        ("BOX",            (0, 0), (-1, -1), 1.2, C_BORDER),
        ("INNERGRID",      (0, 0), (-1, -1), 0.5, C_BORDER),
        ("TOPPADDING",     (0, 0), (-1, -1), 8),
        ("BOTTOMPADDING",  (0, 0), (-1, -1), 8),
        ("VALIGN",         (0, 0), (-1, -1), "MIDDLE"),
    ]))
    story.append(kft)
    story.append(SP(16))

    sources = Paragraph(
        "<i>Sources: Schwartz's Principles of Surgery 11e &bull; Mulholland &amp; Greenfield's Surgery 7e &bull; "
        "Current Surgical Therapy 14e &bull; Robbins, Cotran &amp; Kumar Pathologic Basis of Disease &bull; "
        "Grainger &amp; Allison's Diagnostic Radiology &bull; Harriet Lane Handbook 23e &bull; "
        "The Developing Human (Clinically Oriented Embryology)</i>",
        S("src", fontName="Helvetica-Oblique", fontSize=7.5,
          textColor=colors.HexColor("#666666"), alignment=TA_CENTER))
    story.append(sources)
    story.append(PageBreak())

    # ═══════════════════════════════════════════════════════════════════════════
    # SECTION 1 – Overview & Definition
    # ═══════════════════════════════════════════════════════════════════════════
    story.append(H1("1. OVERVIEW & DEFINITION"))
    story.append(SP(6))
    story.append(B(
        "Infantile Hypertrophic Pyloric Stenosis (IHPS) is a condition of progressive <b>hypertrophy "
        "and hyperplasia of the pyloric muscularis propria</b>, causing gastric outlet obstruction "
        "in young infants. It is <b>not a congenital disease</b> - the pyloric muscle is normal at "
        "birth and hypertrophies postnatally, typically manifesting between 3-6 weeks of age."))
    story.append(SP(4))
    story.append(PEARL(
        "IHPS is NOT congenital - it is acquired postnatally. Do not confuse with congenital atresias "
        "or webs. The pylorus is histologically and functionally normal at birth."))

    # ═══════════════════════════════════════════════════════════════════════════
    # SECTION 2 – Epidemiology
    # ═══════════════════════════════════════════════════════════════════════════
    story.append(SP(8))
    story.append(H1("2. EPIDEMIOLOGY"))
    story.append(SP(6))

    epi_data = [
        ["Parameter", "Data / Detail"],
        ["Incidence", "~1 in 300 live births (0.1-0.4% in White infants; slightly lower in Black infants)"],
        ["Sex ratio", "Male : Female = 4-5 : 1"],
        ["Birth order", "More common in first-born males"],
        ["Peak age", "3-6 weeks (range 2-12 weeks); rare after 12 weeks"],
        ["Siblings", "Risk rises to ~6% in siblings of affected patients"],
        ["Twins", "High concordance in monozygotic twins; lesser risk in dizygotic twins"],
        ["Associated syndromes", "Turner syndrome, Trisomy 18 - confer increased risk"],
        ["Premature infants", "Increased risk; presentation may be later (corrected age)"],
        ["Macrolide exposure", "Erythromycin/Azithromycin in first 2 weeks of life (oral or via breast milk) - associated trigger"],
    ]
    story.append(tbl(epi_data, [5.5*cm, avail_w - 5.5*cm]))
    story.append(SP(6))

    story.append(NOTE(
        "Macrolide antibiotics (especially erythromycin) given to neonates or nursing mothers "
        "in the first 2 weeks of life are a well-recognised risk factor for IHPS. Always warn "
        "parents when prescribing to neonates or breastfeeding mothers of young infants."))

    story.append(SP(4))
    story.append(H3("Carter Effect (Genetic Liability)"))
    story.append(B(
        "Females must accumulate more variant alleles to express IHPS. Therefore, when a <b>mother</b> "
        "is the affected individual, her sons have a very high risk (~20%) of IHPS. Genome-wide "
        "association studies have identified a significant locus at <b>chromosome 11q23.3</b> "
        "related to cholesterol regulation. The exact pathogenic mechanism remains unclear."))

    # ═══════════════════════════════════════════════════════════════════════════
    # SECTION 3 – Pathophysiology
    # ═══════════════════════════════════════════════════════════════════════════
    story.append(SP(8))
    story.append(H1("3. PATHOPHYSIOLOGY & EMBRYOLOGY"))
    story.append(SP(6))

    story.append(H3("The Pyloric Muscle"))
    story.append(B(
        "The pylorus is the distal sphincteric region of the stomach, guarding the gastroduodenal "
        "junction. In IHPS, both the <b>circular (predominantly)</b> and longitudinal muscle layers "
        "undergo hypertrophy, resulting in:"))
    for txt in [
        "Severe stenosis of the pyloric canal",
        "Failure of normal pyloric relaxation",
        "Progressive gastric outlet obstruction",
        "Massive gastric distension with forceful projectile vomiting",
    ]:
        story.append(BUL(txt))

    story.append(SP(4))
    story.append(H3("Proposed Mechanisms"))
    path_data = [
        ["Mechanism", "Evidence/Detail"],
        ["Loss of nitric oxide synthase (NOS)", "NOS deficiency in myenteric plexus → failure of pyloric relaxation → functional obstruction"],
        ["Mucosal/submucosal oedema", "Exacerbates mechanical obstruction on top of muscular hypertrophy"],
        ["Reduced peptidergic innervation", "Decreased VIP, substance P and other neuropeptides in pyloric wall"],
        ["Genetic factors", "Chromosome 11q23.3 locus; multiple GWAS-identified GI development genes"],
        ["Environmental triggers", "Macrolide exposure (motilin receptor agonist - stimulates pyloric contraction)"],
        ["Hypergastrinaemia", "Proposed but not confirmed; maternal prostaglandins also implicated"],
    ]
    story.append(tbl(path_data, [5.5*cm, avail_w - 5.5*cm]))
    story.append(SP(4))
    story.append(PEARL(
        "The hypertrophied pyloric mass forms the classic palpable 'olive' - a firm, smooth, mobile, "
        "ovoid 1-2 cm mass in the right upper quadrant. When found, it is essentially pathognomonic "
        "and no further imaging is strictly required."))

    # ═══════════════════════════════════════════════════════════════════════════
    # SECTION 4 – Clinical Presentation
    # ═══════════════════════════════════════════════════════════════════════════
    story.append(SP(8))
    story.append(H1("4. CLINICAL PRESENTATION"))
    story.append(SP(6))

    story.append(H3("Classic Temporal Sequence"))
    timeline_data = [
        ["Age / Stage", "Clinical Features"],
        ["Birth to 2 weeks", "Normal feeding, no vomiting. Pyloric muscle histologically normal."],
        ["2-3 weeks", "Onset of postprandial non-bilious vomiting. Initially mild, intermittent."],
        ["3-6 weeks (peak)", "Vomiting becomes FORCEFUL and PROJECTILE. Infant hungry immediately after vomiting."],
        ["Late / untreated", "Complete gastric outlet obstruction, severe dehydration, weight loss, metabolic alkalosis."],
    ]
    story.append(tbl(timeline_data, [4*cm, avail_w - 4*cm]))
    story.append(SP(6))

    story.append(H3("Symptoms"))
    syms = [
        ("<b>Non-bilious projectile vomiting</b> - The HALLMARK. Non-bilious because obstruction is "
         "PROXIMAL to the ampulla of Vater. Vomiting can propel across the room."),
        ("<b>Hunger after vomiting</b> - Infant vomits and immediately demands re-feeding ('hungry vomiter'). "
         "Helps distinguish from neurological vomiting."),
        ("<b>Weight loss / failure to thrive</b> - Progressive as caloric intake falls."),
        ("<b>Reduced wet diapers</b> - Dehydration and decreased urine output."),
        ("<b>Constipation / scanty stools</b> - 'Starvation stools' - small, dark green mucoid stools."),
        ("<b>Jaundice</b> - Indirect hyperbilirubinaemia in ~2-5% of cases (reduced hepatic glucuronyl "
         "transferase activity from poor caloric intake / starvation)."),
    ]
    for s in syms:
        story.append(BUL(s))

    story.append(SP(6))
    story.append(H3("Physical Examination Findings"))
    exam_data = [
        ["Finding", "Description", "Significance"],
        ["Palpable 'Olive'", "Firm, mobile, ovoid 1-2 cm mass in RUQ/midepigastrium", "Nearly pathognomonic; found in up to 90% by experienced examiner (now <30% in early presentations)"],
        ["Visible gastric peristalsis", "Left-to-right waves across upper abdomen just before vomiting", "Also seen in any gastric/duodenal obstruction"],
        ["Sunken fontanelle", "Sign of moderate-severe dehydration", "Late finding"],
        ["Dry mucous membranes", "Sign of dehydration", "Late finding"],
        ["Reduced skin turgor", "Pinch test - skin returns slowly", "Severe dehydration"],
        ["Jaundice", "Scleral icterus, yellow skin", "Indirect (unconjugated) type"],
    ]
    story.append(tbl(exam_data, [3.5*cm, 5.5*cm, avail_w - 9*cm]))

    story.append(SP(4))
    story.append(NOTE(
        "To palpate the 'olive': decompress stomach via NGT first, wait for a quiet/sleeping infant, "
        "place fingertips below the right liver edge during quiet respiration. Repeat examinations "
        "may be necessary. An experienced clinician should be able to palpate the pylorus in "
        "nearly all cases. Inability to palpate in a quiet/anaesthetised infant should question the diagnosis."))

    # ═══════════════════════════════════════════════════════════════════════════
    # SECTION 5 – Metabolic Derangement
    # ═══════════════════════════════════════════════════════════════════════════
    story.append(PageBreak())
    story.append(H1("5. METABOLIC DERANGEMENT - THE CLASSIC BIOCHEMISTRY"))
    story.append(SP(6))

    story.append(B(
        "The repeated vomiting of gastric juice (rich in H⁺ and Cl⁻) produces the classical "
        "metabolic disturbance: <b>Hypochloraemic, Hypokalaemic Metabolic Alkalosis</b> with "
        "<b>Paradoxical Aciduria</b>."))
    story.append(SP(6))

    story.append(H3("Step-by-Step Pathophysiology"))
    steps = [
        "<b>Step 1:</b> Vomiting → Loss of HCl (H⁺ + Cl⁻) from stomach → Metabolic alkalosis + Hypochloraemia",
        "<b>Step 2:</b> Volume depletion → Activation of renin-angiotensin-aldosterone system (RAAS)",
        "<b>Step 3:</b> Aldosterone → Renal Na⁺ retention + K⁺ secretion → Hypokalaemia",
        "<b>Step 4 (Early):</b> Kidney excretes excess HCO₃⁻ in urine to compensate → ALKALINE urine",
        "<b>Step 5 (Late):</b> Severe volume depletion → Kidney prioritises Na⁺ retention over acid-base",
        "<b>Step 6:</b> H⁺ is preferentially exchanged for Na⁺ in distal tubule → PARADOXICAL ACIDURIA",
        "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;(Acidic urine despite systemic metabolic alkalosis - hallmark of severe/late IHPS)",
    ]
    for s in steps:
        story.append(BUL(s))

    story.append(SP(6))
    story.append(H3("Blood Gas / Electrolyte Pattern"))
    bg_data = [
        ["Parameter", "Direction", "Typical Value / Range"],
        ["pH", "↑ HIGH", "> 7.45 (often 7.50-7.55)"],
        ["pCO₂", "↑ HIGH (compensatory)", "Elevated (respiratory compensation)"],
        ["HCO₃⁻", "↑ MARKEDLY HIGH", "> 30 mEq/L (often 35-45)"],
        ["Chloride (Cl⁻)", "↓ LOW", "< 90 mEq/L (sometimes <80)"],
        ["Potassium (K⁺)", "↓ LOW", "< 3.5 mEq/L"],
        ["Sodium (Na⁺)", "Variable", "May be low (hypovolaemic hyponatraemia)"],
        ["Urine pH (early)", "↑ HIGH (alkaline)", "> 7 (bicarbonate excretion)"],
        ["Urine pH (late)", "↓ LOW (acidic)", "< 6 - PARADOXICAL ACIDURIA"],
    ]
    story.append(tbl(bg_data, [4*cm, 3.5*cm, avail_w - 7.5*cm]))

    story.append(SP(6))
    story.append(PEARL(
        "DANGER THRESHOLD: Bicarbonate > 30 mEq/L is a relative contraindication to general "
        "anaesthesia. These infants have diminished respiratory drive and are at HIGH RISK for "
        "postoperative apnoea and respiratory arrest. Surgery MUST WAIT until HCO₃⁻ < 30 mEq/L."))

    # ═══════════════════════════════════════════════════════════════════════════
    # SECTION 6 – Differential Diagnosis
    # ═══════════════════════════════════════════════════════════════════════════
    story.append(SP(8))
    story.append(H1("6. DIFFERENTIAL DIAGNOSIS"))
    story.append(SP(6))

    story.append(B(
        "The single most important step: <b>Is the vomiting bilious or non-bilious?</b> "
        "Bilious vomiting in a neonate = SURGICAL EMERGENCY until proven otherwise."))
    story.append(SP(4))

    ddx_data = [
        ["Condition", "Key Distinguishing Feature", "Bilious?"],
        ["GORD (most common DDx)", "Non-projectile; positional; responds to thickened feeds; normal US", "No"],
        ["Pylorospasm", "Temporary, transient; US equivocal or normal; self-resolving", "No"],
        ["Overfeeding", "History; thriving infant; no organic cause", "No"],
        ["Antral/prepyloric web", "Similar to IHPS; contrast study diagnostic", "No"],
        ["Malrotation + midgut volvulus", "BILIOUS; haemodynamic compromise; URGENT emergency", "YES"],
        ["Duodenal atresia/stenosis", "BILIOUS; 'double bubble' on AXR; often Down syndrome; prenatal polyhydramnios", "YES"],
        ["Jejunal/ileal atresia", "BILIOUS; abdominal distension", "YES"],
        ["Raised ICP", "Projectile vomiting NOT related to feeds; bulging fontanelle; other neuro signs", "No"],
        ["Congenital Adrenal Hyperplasia (CAH)", "Vomiting + hypoglycaemia + HYPONATRAEMIA + HYPERKALAEMIA (OPPOSITE electrolytes!)", "No"],
        ["Cow's milk protein allergy", "Blood in stool; eosinophils; responds to hypoallergenic formula", "No"],
        ["Sepsis/meningitis", "Ill-looking; fever; altered tone; bulging fontanelle", "Variable"],
    ]
    story.append(tbl(ddx_data, [4.5*cm, avail_w - 7*cm, 2.5*cm]))

    story.append(SP(4))
    story.append(NOTE(
        "CAH vs IHPS electrolytes: IHPS gives hypOnatraemia, hypOkalaemia, hypOchloraemia, alkalosis. "
        "CAH gives hypOnatraemia, hypERkalaemia. The potassium direction is the KEY differentiator."))

    # ═══════════════════════════════════════════════════════════════════════════
    # SECTION 7 – Investigations
    # ═══════════════════════════════════════════════════════════════════════════
    story.append(PageBreak())
    story.append(H1("7. INVESTIGATIONS"))
    story.append(SP(6))

    story.append(H3("Step 1: Blood Tests (All Infants)"))
    for txt in [
        "<b>Electrolytes panel:</b> Na⁺, K⁺, Cl⁻, HCO₃⁻ - identify and quantify metabolic alkalosis",
        "<b>Blood gas (VBG):</b> pH, pCO₂, HCO₃⁻ - confirm alkalosis and respiratory compensation",
        "<b>Blood glucose:</b> Hypoglycaemia from poor caloric intake is common",
        "<b>Urea and creatinine:</b> Assess degree of dehydration",
        "<b>Full blood count:</b> Baseline; haemoconcentration in dehydration",
        "<b>Serum bilirubin:</b> If clinically jaundiced - confirm unconjugated hyperbilirubinaemia",
    ]:
        story.append(BUL(txt))

    story.append(SP(6))
    story.append(H3("Step 2: Imaging - Ultrasound is GOLD STANDARD"))
    story.append(B(
        "Pyloric ultrasound has replaced all other modalities as the investigation of choice. "
        "It has a <b>sensitivity of 95-98% and specificity of up to 100%</b>."))
    story.append(SP(4))

    us_data = [
        ["Measurement", "Normal", "IHPS (Diagnostic Threshold)"],
        ["Pyloric muscle thickness (most reliable)", "< 3 mm", "≥ 4 mm"],
        ["Pyloric channel length", "< 12 mm", "≥ 15-17 mm"],
        ["Pyloric diameter (transverse)", "< 10 mm", "> 13-14 mm"],
    ]
    story.append(tbl(us_data, [6*cm, 3*cm, avail_w - 9*cm]))
    story.append(SP(4))

    story.append(H3("Ultrasound Signs to Recognise"))
    us_signs = [
        "<b>Target / Doughnut sign:</b> Hypoechoic thickened muscle ring on transverse view - the classic appearance",
        "<b>Shoulder sign:</b> Hypertrophied pyloric mass bulging into the gastric antrum",
        "<b>Nipple sign:</b> Double-layered hypertrophic mucosa protruding into the stomach",
        "<b>Cervix sign:</b> Overall appearance resembles a uterine cervix on sagittal view",
        "<b>Real-time:</b> Failure of fluid to pass through the pyloric channel; exaggerated peristaltic waves",
        "<b>Hypoechoic muscle:</b> Thickened hypoechoic muscle surrounding hyperechoic mucosa",
    ]
    for s in us_signs:
        story.append(BUL(s))

    story.append(SP(4))
    story.append(NOTE(
        "Neonatal caution: Younger/preterm babies may have smaller absolute measurements yet still "
        "have IHPS. If pyloric muscle thickness > 3 mm on REPEAT ultrasound (regardless of age/weight), "
        "IHPS is confirmed. Close clinical correlation is MANDATORY. When equivocal, repeat US in "
        "24-48 hours or proceed to contrast study."))

    story.append(SP(6))

    # Download and insert ultrasound images
    img1_path = download_image(
        "https://cdn.orris.care/cdss_images/700217f7cd566645b516ab50679b279c80a7a9e054c354e6c61675aaaa6a785a.png",
        "us_3panel.png")
    if img1_path and os.path.exists(img1_path):
        img1 = Image(img1_path, width=avail_w * 0.85, height=avail_w * 0.85 * 0.37)
        story.append(img1)
        story.append(Paragraph(
            "<i>Fig 1. A: Transverse US - pyloric muscle wall thickness >4 mm. "
            "B: Horizontal US - channel length >14 mm. "
            "C: Contrast study - narrowed pyloric channel (arrow) and distended gastric fundus (F). "
            "[Source: The Developing Human - Clinically Oriented Embryology]</i>",
            caption_style))

    story.append(SP(6))

    img2_path = download_image(
        "https://cdn.orris.care/cdss_images/b5830f70e685d5b742fbb1dfc488dda080f33d2037c2fd145d5167429ca575a9.png",
        "us_coronal.png")
    if img2_path and os.path.exists(img2_path):
        img2 = Image(img2_path, width=avail_w * 0.6, height=avail_w * 0.6 * 0.78)
        img2.hAlign = "CENTER"
        story.append(img2)
        story.append(Paragraph(
            "<i>Fig 2. Pylorus ultrasound (coronal plane): muscle wall thickness 4.3 mm (measurement 2), "
            "pyloric channel length 2.26 cm (measurement 1) - both exceeding diagnostic thresholds for IHPS. "
            "[Source: Mulholland and Greenfield's Surgery, 7e]</i>",
            caption_style))

    story.append(SP(6))
    story.append(H3("Upper GI Contrast Series (when US equivocal)"))
    contrast_signs = [
        "<b>'String sign' / 'Railroad track sign':</b> Contrast trickling as a thin line through the narrow elongated pyloric channel",
        "<b>'Mushroom / Umbrella sign':</b> Indentation of pyloric mass on the gastric antrum",
        "<b>'Shoulder sign' on contrast:</b> Filling defect at the gastroduodenal junction",
        "<b>Advantage:</b> Evaluates for other causes of vomiting (malrotation, GOR, antroduodenal webs)",
        "<b>Disadvantage:</b> Contrast remains in a poorly-emptying stomach; radiation exposure",
    ]
    for s in contrast_signs:
        story.append(BUL(s))

    # ═══════════════════════════════════════════════════════════════════════════
    # SECTION 8 – Management
    # ═══════════════════════════════════════════════════════════════════════════
    story.append(PageBreak())
    story.append(H1("8. MANAGEMENT"))
    story.append(SP(6))

    story.append(PEARL(
        "IHPS is NEVER a surgical emergency. The child must be metabolically optimised "
        "BEFORE going to theatre. Rushing to surgery with uncorrected alkalosis risks "
        "fatal postoperative respiratory depression."))

    story.append(SP(6))
    story.append(H3("Phase 1: Resuscitation (typically 24-48 hours)"))

    story.append(B("<b>IV Fluid Protocol:</b>"))
    fluids = [
        "Give 1-2 boluses of <b>Normal Saline (0.9% NaCl) 20 mL/kg</b> for dehydration correction",
        "Maintenance: <b>5% Dextrose / 0.45% NaCl + KCl 2-4 mEq/kg/day</b> at 1.5× maintenance rate (~150-175 mL/kg/day)",
        "<b>DO NOT add potassium</b> until urine output is confirmed (> 2 mL/kg/hr)",
        "Nasogastric tube (NGT) for gastric decompression + nil by mouth",
        "Monitor electrolytes every 6-12 hours",
        "Monitor urine output - aim > 2 mL/kg/hr (wet diapers returning = good sign)",
    ]
    for f in fluids:
        story.append(BUL(f))

    story.append(SP(4))
    story.append(B("<b>Surgical 'Go' Criteria (safe to proceed to theatre):</b>"))
    go_data = [
        ["Parameter", "Target Before Anaesthesia"],
        ["Serum Chloride", "≥ 90-95 mEq/L"],
        ["Serum Bicarbonate (CO₂)", "≤ 30 mEq/L"],
        ["Serum Potassium", "≥ 3.0-3.5 mEq/L"],
        ["Urine output", "> 2 mL/kg/hr (adequate hydration)"],
        ["Serum Sodium", "Normal range"],
    ]
    story.append(tbl(go_data, [5*cm, avail_w - 5*cm]))

    story.append(SP(8))
    story.append(H3("Phase 2: Surgery - Fredet-Ramstedt Pyloromyotomy"))
    story.append(B(
        "The definitive, essentially curative treatment. It is a <b>myotomy only</b> - not a "
        "pyloroplasty or bypass. The mucosa is left intact."))
    story.append(SP(4))

    story.append(B("<b>Surgical Technique:</b>"))
    steps_surg = [
        "A single <b>longitudinal incision</b> through the <b>anterior wall</b> of the hypertrophied pyloric muscle (avascular plane)",
        "Carried from the stomach side to the duodenal side (where muscle softens and transitions to normal bowel wall)",
        "<b>Submucosal bulging</b> into the myotomy site = adequate pyloromyotomy",
        "Both edges of divided muscle must be <b>freely mobile</b>",
        "<b>Leak test:</b> 30-60 mL of air injected via orogastric tube - confirms pyloric patency and NO mucosal perforation",
    ]
    for s in steps_surg:
        story.append(BUL(s))

    story.append(SP(4))
    story.append(H3("Surgical Approaches"))
    approach_data = [
        ["Approach", "Description", "Notes"],
        ["Laparoscopic (preferred)", "3 ports: umbilical + two 3-mm stab incisions bilaterally", "Standard at most paediatric surgical centres; shorter stay, better cosmesis"],
        ["Open - umbilical incision", "Circumferential incision around umbilicus", "Cosmetically superior; comparable outcomes to laparoscopic"],
        ["Open - RUQ transverse", "Right upper quadrant transverse incision", "Traditional; slightly higher wound visibility"],
    ]
    story.append(tbl(approach_data, [4*cm, 6.5*cm, avail_w - 10.5*cm]))

    story.append(SP(4))
    story.append(NOTE(
        "A 2024 systematic review and meta-analysis (Khandelia et al., Pediatr Surg Int) comparing "
        "umbilical vs. right upper transverse incisions found no significant difference in clinical outcomes. "
        "Choice of approach depends on surgeon preference and training."))

    story.append(SP(6))
    story.append(H3("Phase 3: Postoperative Feeding & Discharge"))
    post_op = [
        "Start feeds <b>6-8 hours</b> after recovery from anaesthesia",
        "<b>Ad libitum feeding with 60 mL limit</b> per feed (current best practice)",
        "Some post-op vomiting is <b>expected and normal</b> (residual mucosal oedema at myotomy site)",
        "True failure to feed at 48-72 hrs post-op → suspect <b>incomplete myotomy</b>",
        "Most infants can be <b>discharged within 24-36 hours</b> after tolerating at least two consecutive feeds",
        "Outcomes are significantly better with <b>specialty-trained paediatric surgeons</b> vs. general surgeons",
    ]
    for p in post_op:
        story.append(BUL(p))

    # ═══════════════════════════════════════════════════════════════════════════
    # SECTION 9 – Complications
    # ═══════════════════════════════════════════════════════════════════════════
    story.append(SP(8))
    story.append(H1("9. COMPLICATIONS OF PYLOROMYOTOMY"))
    story.append(SP(6))

    comp_data = [
        ["Complication", "Presentation", "Management"],
        ["Incomplete myotomy", "Ongoing projectile vomiting at 48-72 hrs post-op", "Return to OR for revision myotomy on posterior pyloric wall (180° from first)"],
        ["Mucosal perforation\n(intraoperative)", "Air leak on leak test", "Close perforation + omental buttress; OR close myotomy and rotate pylorus 180° for new myotomy"],
        ["Unrecognised mucosal\nperforation", "Sepsis, peritonitis, deterioration", "Emergency laparotomy + peritoneal washout"],
        ["Postoperative apnoea", "First 24 hrs; esp. if alkalosis not corrected pre-op", "Pre-operative correction of alkalosis; apnoea monitoring post-op"],
        ["Wound infection", "Erythema, discharge, fever", "Antibiotics ± wound care; prophylactic antibiotics reduce risk"],
        ["Inadvertent duodenotomy", "Air leak on leak test (duodenal side)", "Repair + close myotomy + rotate for new myotomy"],
    ]
    story.append(tbl(comp_data, [4*cm, 5*cm, avail_w - 9*cm]))
    story.append(SP(4))
    story.append(NOTE(
        "A 2024 meta-analysis (Gulia et al., Pediatr Surg Int) demonstrated that prophylactic "
        "preoperative antibiotics significantly reduce surgical site infections in IHPS patients "
        "undergoing pyloromyotomy. This supports routine prophylactic antibiotic use."))

    # ═══════════════════════════════════════════════════════════════════════════
    # SECTION 10 – Non-surgical / Medical Management
    # ═══════════════════════════════════════════════════════════════════════════
    story.append(SP(8))
    story.append(H1("10. NON-SURGICAL (MEDICAL) MANAGEMENT"))
    story.append(SP(6))
    story.append(B(
        "IV Atropine has been used as an alternative to surgery in selected centres "
        "(Japan, some European centres). Atropine sulphate acts as an anticholinergic agent "
        "relaxing the pyloric smooth muscle."))
    story.append(SP(4))
    atrop_data = [
        ["Feature", "Detail"],
        ["Drug", "Atropine sulphate IV (or oral after initial IV phase)"],
        ["Mechanism", "Anticholinergic → pyloric muscle relaxation → allows gradual pyloric dilation"],
        ["Success rate", "~85-90% with prolonged protocol treatment"],
        ["Duration", "Weeks to months of treatment; prolonged hospitalisation"],
        ["Limitations", "Relapse possible; not widely adopted; requires strict protocols"],
        ["When to consider", "High anaesthetic risk infants; parental preference in experienced centres"],
        ["Current status", "Surgery (pyloromyotomy) remains FIRST-LINE in most international guidelines"],
    ]
    story.append(tbl(atrop_data, [4.5*cm, avail_w - 4.5*cm]))

    # ═══════════════════════════════════════════════════════════════════════════
    # SECTION 11 – Prognosis
    # ═══════════════════════════════════════════════════════════════════════════
    story.append(SP(8))
    story.append(H1("11. PROGNOSIS"))
    story.append(SP(6))
    for p in [
        "<b>Surgical cure rate: virtually 100%</b> with adequate pyloromyotomy",
        "Mortality is distinctly rare in the absence of concomitant medical problems",
        "Recovery is typically uncomplicated; most infants discharged within 24-36 hours",
        "Outcomes are better when operated by <b>specialty-trained paediatric surgeons</b>",
        "<b>Long-term:</b> No predisposition to adult peptic ulcer disease or GI malignancy",
        "Normal GI function restored; infants grow and thrive normally after adequate repair",
    ]:
        story.append(BUL(p))

    # ═══════════════════════════════════════════════════════════════════════════
    # SECTION 12 – Mnemonics & Exam Pearls
    # ═══════════════════════════════════════════════════════════════════════════
    story.append(PageBreak())
    story.append(H1("12. MNEMONICS & EXAM PEARLS"))
    story.append(SP(6))

    story.append(H3("Mnemonic 1: IHPS Profile - 'HNMM'"))
    mnemo_data = [
        ["Letter", "Stands For", "Key Point"],
        ["H", "Hungry after vomiting", "Infant vomits and immediately wants to re-feed"],
        ["N", "Non-bilious vomiting", "Critical differentiator - obstruction proximal to ampulla of Vater"],
        ["M", "Male predominance (4-5:1)", "Especially first-born males"],
        ["M", "3-6 weeks (Mid-infancy)", "Rare before 2 weeks, rare after 12 weeks"],
    ]
    story.append(tbl(mnemo_data, [1.5*cm, 5*cm, avail_w - 6.5*cm]))

    story.append(SP(6))
    story.append(H3("Mnemonic 2: Metabolic Alkalosis - 'HypoC, HypoK, High HCO₃, Paradox Acid Urine'"))
    met_mnemo = [
        "<b>Cl⁻ goes DOWN</b> (lost in vomit directly)",
        "<b>K⁺ goes DOWN</b> (lost in vomit + aldosterone-driven renal losses)",
        "<b>HCO₃⁻ goes UP</b> (metabolic alkalosis)",
        "<b>Urine pH initially UP</b> (early compensation - kidney excretes HCO₃⁻)",
        "<b>Urine pH eventually DOWN</b> = PARADOXICAL ACIDURIA (late, severe dehydration)",
    ]
    for m in met_mnemo:
        story.append(BUL(m))

    story.append(SP(6))
    story.append(H3("Mnemonic 3: Ultrasound Criteria - '4 and 16'"))
    us_mnemo_data = [
        ["Measurement", "Threshold", "Memory Aid"],
        ["Muscle thickness", "≥ 4 mm", "'4 mm = 4 letters in IHPS'"],
        ["Channel length", "≥ 16 mm (or ≥15 mm)", "'16 weeks = typical first trimester scan age'"],
    ]
    story.append(tbl(us_mnemo_data, [4.5*cm, 3*cm, avail_w - 7.5*cm]))

    story.append(SP(6))
    story.append(H3("Key Rule: NEVER Rush to Theatre"))
    rule_data = [[Paragraph(
        "<b>Pyloric Stenosis = NEVER a surgical emergency</b><br/>"
        "Correct FIRST: Cl⁻ ≥ 90 &bull; HCO₃⁻ ≤ 30 &bull; K⁺ ≥ 3 &bull; Urine output > 2 mL/kg/hr<br/>"
        "HCO₃⁻ > 30 mEq/L + General Anaesthesia = Risk of fatal respiratory arrest",
        S("rule", fontName="Helvetica-Bold", fontSize=10, textColor=C_RED,
          alignment=TA_CENTER, leading=16))]]
    rt = Table(rule_data, colWidths=[avail_w])
    rt.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), colors.HexColor("#fff0f0")),
        ("BOX", (0, 0), (-1, -1), 2, C_RED),
        ("TOPPADDING", (0, 0), (-1, -1), 12),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 12),
    ]))
    story.append(rt)

    # ═══════════════════════════════════════════════════════════════════════════
    # SECTION 13 – Clinical Scenario
    # ═══════════════════════════════════════════════════════════════════════════
    story.append(SP(8))
    story.append(H1("13. CLINICAL SCENARIO - PRACTICE CASE"))
    story.append(SP(6))

    scenario_text = (
        "A 5-week-old male infant, first-born, is brought with a 1-week history of increasing "
        "non-bilious, projectile vomiting after every feed. His mother reports he vomits 'across "
        "the room' and then immediately wants to feed again. He has had fewer wet diapers over "
        "the last 3 days. On examination: slightly sunken anterior fontanelle, dry mucous membranes, "
        "a firm mobile mass palpable in the right upper quadrant. "
        "VBG: pH 7.54, pCO₂ 52 mmHg, HCO₃⁻ 38 mEq/L, Cl⁻ 83 mEq/L, K⁺ 2.7 mEq/L."
    )
    sbox = [[Paragraph(scenario_text,
                       S("scen", fontName="Helvetica-Oblique", fontSize=9.5,
                         textColor=C_HEADER, leading=14))]]
    st2 = Table(sbox, colWidths=[avail_w])
    st2.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), C_ACCENT),
        ("BOX", (0, 0), (-1, -1), 1.5, C_BORDER),
        ("TOPPADDING", (0, 0), (-1, -1), 10),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 10),
        ("LEFTPADDING", (0, 0), (-1, -1), 12),
        ("RIGHTPADDING", (0, 0), (-1, -1), 12),
    ]))
    story.append(st2)
    story.append(SP(6))

    qa = [
        ("Q: What is the diagnosis?",
         "Infantile Hypertrophic Pyloric Stenosis (IHPS). Supported by: age 5 weeks, male, "
         "first-born, non-bilious projectile vomiting, hunger after vomiting, palpable 'olive', "
         "and hypochloraemic hypokalaemic metabolic alkalosis."),
        ("Q: What investigation will confirm the diagnosis?",
         "Pyloric ULTRASOUND - looking for muscle wall thickness ≥4 mm and channel length ≥16 mm. "
         "Also assess real-time: inability of fluid to pass through the pylorus."),
        ("Q: Why NOT take this child to theatre immediately?",
         "HCO₃⁻ is 38 mEq/L (>30 = danger threshold). General anaesthesia under severe metabolic "
         "alkalosis causes diminished respiratory drive and high risk of postoperative apnoea/respiratory "
         "arrest. IHPS is NEVER a surgical emergency."),
        ("Q: What is your immediate management plan?",
         "1. IV access + NGT (gastric decompression, NBM). "
         "2. Normal saline 20 mL/kg bolus × 1-2 for volume resuscitation. "
         "3. D5/0.45% NaCl at 1.5× maintenance. Add KCl only after urine output confirmed. "
         "4. Electrolytes every 6-12 hours. 5. Surgery when Cl⁻ ≥90, HCO₃⁻ ≤30, K⁺ ≥3, UO adequate."),
        ("Q: What surgical procedure will be performed?",
         "Fredet-Ramstedt Pyloromyotomy - a single longitudinal incision through the hypertrophied "
         "pyloric muscle (anterior wall). Confirmed complete when submucosa bulges into myotomy site. "
         "Performed laparoscopically at most paediatric centres."),
    ]
    for q, a in qa:
        story.append(Paragraph(q, S("q", fontName="Helvetica-Bold", fontSize=9.5,
                                     textColor=C_HEADER, leading=14, spaceBefore=5)))
        story.append(Paragraph(a, S("a", fontName="Helvetica", fontSize=9.5,
                                     textColor=colors.HexColor("#222222"), leading=14,
                                     leftIndent=12, spaceAfter=4)))

    # ═══════════════════════════════════════════════════════════════════════════
    # SECTION 14 – Summary Table
    # ═══════════════════════════════════════════════════════════════════════════
    story.append(PageBreak())
    story.append(H1("14. QUICK REFERENCE SUMMARY TABLE"))
    story.append(SP(6))

    summary_data = [
        ["Feature", "IHPS Summary"],
        ["Full name", "Infantile Hypertrophic Pyloric Stenosis"],
        ["Incidence", "1 in 300 live births (~0.2-0.4%)"],
        ["Sex ratio", "Male >> Female (4-5:1)"],
        ["Peak age", "3-6 weeks (range 2-12 weeks)"],
        ["Aetiology", "Unknown; postnatal pyloric muscle hypertrophy; genetic + environmental"],
        ["Key symptom", "NON-BILIOUS, PROJECTILE vomiting with hunger after vomiting"],
        ["Key sign", "Palpable 'olive' - firm, mobile, ovoid RUQ mass"],
        ["Metabolics", "Hypochloraemic, Hypokalaemic, Metabolic Alkalosis (+/- Paradoxical Aciduria)"],
        ["Imaging", "Ultrasound GOLD STANDARD: muscle ≥4 mm, channel ≥16 mm"],
        ["Treatment", "Fredet-Ramstedt Pyloromyotomy (open or laparoscopic)"],
        ["Pre-op MUST", "Correct metabolic alkalosis first: HCO₃⁻ <30, Cl⁻ >90, K⁺ >3"],
        ["Surgery timing", "NEVER an emergency - resuscitate first (24-48 hrs)"],
        ["Post-op feeding", "Start 6-8 hrs post-op; ad lib with 60 mL limit; discharge at 24-36 hrs"],
        ["Complications", "Incomplete myotomy, mucosal perforation, wound infection, apnoea"],
        ["Prognosis", "EXCELLENT - virtually 100% cure with adequate pyloromyotomy"],
    ]
    story.append(tbl(summary_data, [5.5*cm, avail_w - 5.5*cm]))

    story.append(SP(10))
    story.append(HRFlowable(width="100%", thickness=1, color=C_SUBHDR))
    story.append(SP(6))
    story.append(Paragraph(
        "<b>References &amp; Sources</b>",
        S("refhdr", fontName="Helvetica-Bold", fontSize=10, textColor=C_HEADER,
          spaceBefore=4, spaceAfter=4)))
    refs = [
        "Schwartz's Principles of Surgery, 11th Edition - Chapter on Paediatric Surgery",
        "Mulholland and Greenfield's Surgery: Scientific Principles and Practice, 7th Edition",
        "Current Surgical Therapy, 14th Edition",
        "Robbins, Cotran &amp; Kumar: Pathologic Basis of Disease (10th Ed) - GI Chapter",
        "Grainger &amp; Allison's Diagnostic Radiology - Paediatric Radiology",
        "The Developing Human: Clinically Oriented Embryology",
        "The Harriet Lane Handbook, 23rd Edition (Johns Hopkins Hospital)",
        "Khandelia H et al. Comparison between umbilical and right upper transverse abdominal incisions "
        "for pyloromyotomy: systematic review and meta-analysis. Pediatr Surg Int. 2024. PMID: 38935193",
        "Gulia A et al. Utility of preoperative prophylactic antibiotics for preventing surgical site "
        "infections in children with IHPS: systematic review and meta-analysis. Pediatr Surg Int. 2024. PMID: 39503899",
    ]
    for i, r in enumerate(refs, 1):
        story.append(Paragraph(
            f"{i}. {r}",
            S("ref", fontName="Helvetica", fontSize=8.2, textColor=colors.HexColor("#444444"),
              leading=12, leftIndent=10, spaceBefore=2)))

    return story


# ── Build PDF ─────────────────────────────────────────────────────────────────
def main():
    doc = SimpleDocTemplate(
        OUTPUT,
        pagesize=A4,
        rightMargin=MARGIN,
        leftMargin=MARGIN,
        topMargin=MARGIN,
        bottomMargin=2.2 * cm,
        title="Infantile Hypertrophic Pyloric Stenosis - Paediatrics Teaching",
        author="Orris Medical Education",
        subject="Neonatology / Paediatric Surgery Teaching Module",
    )
    story = build_story()
    doc.build(story, canvasmaker=NumberedCanvas)
    print(f"PDF created: {OUTPUT}")


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

cd /home/daytona/workspace/pyloric-stenosis && python3 generate_pdf.py 2>&1

Running Command

ls -lh /home/daytona/workspace/pyloric-stenosis/Pyloric_Stenosis_Paediatrics.pdf

Your PDF is ready - 987 KB, professionally formatted with all the content from the teaching session.

Pyloric Stenosis Paediatrics

PDF Document · PDF

What's included in the PDF (14 sections):
  1. Cover page - key facts panel with incidence, age, sex ratio, vomiting type, metabolics, and treatment at a glance
  2. Overview & Definition - IHPS is NOT congenital
  3. Epidemiology - full table including macrolide risk and Carter effect
  4. Pathophysiology & Embryology - NOS deficiency, genetic loci, mechanism table
  5. Clinical Presentation - temporal sequence table, symptoms, signs, olive palpation technique
  6. Metabolic Derangement - step-by-step mechanism, blood gas pattern table, danger threshold highlighted
  7. Differential Diagnosis - full table including CAH vs. IHPS electrolyte comparison
  8. Investigations - ultrasound criteria table, all US signs (target/doughnut/shoulder/nipple/cervix), 2 clinical ultrasound images + 3-panel barium image
  9. Management - Phase 1 (resuscitation), IV fluid protocol, surgical "Go criteria" table, Phase 2 (Ramstedt pyloromyotomy technique), Phase 3 (post-op feeding)
  10. Complications - full table with management of each complication
  11. Non-surgical/Medical management - atropine protocol
  12. Prognosis
  13. Mnemonics & Exam Pearls - HNMM mnemonic, metabolic alkalosis mnemonic, "4 and 16" US mnemonic, red-box NEVER rush to theatre rule
  14. Clinical Scenario - practice case with Q&A + Quick Reference Summary Table
This is a shared conversation. Sign in to Orris to start your own chat.