Please provide me popular handbook in pdf version of short horror stories of around 45- 50 pages with proficiency of B2 English for ESL students to read with photo in it like a novel

Reading File
Running Command

mkdir -p /home/daytona/workspace/horror-handbook && ls /home/daytona/workspace/h…

Writing File

~/horror-handbook/generate_horror_pdf.py

#!/usr/bin/env python3
"""
Generate: Shadows & Whispers – A Horror Anthology for ESL Learners (B2)
~45-50 pages, novel-style layout with images, vocabulary notes, and comprehension questions.
"""

import os
import urllib.request
import textwrap
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm, mm
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY, TA_RIGHT
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, PageBreak,
    HRFlowable, Image, Table, TableStyle, KeepTogether
)
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus.flowables import HRFlowable
import io

# ─── Page setup ───────────────────────────────────────────────────────────────
PAGE_W, PAGE_H = A4
MARGIN = 2.2 * cm
OUT_PATH = "/home/daytona/workspace/horror-handbook/Shadows_and_Whispers_Horror_Anthology.pdf"
IMG_DIR  = "/home/daytona/workspace/horror-handbook/imgs"
os.makedirs(IMG_DIR, exist_ok=True)

# ─── Free, reliable Unsplash/Picsum images ────────────────────────────────────
IMAGES = {
    "cover":       "https://images.unsplash.com/photo-1509248961158-e54f6934749c?w=900&q=80",
    "haunted":     "https://images.unsplash.com/photo-1504701954957-2010ec3bcec1?w=800&q=75",
    "forest":      "https://images.unsplash.com/photo-1518173946687-a4c8892bbd9f?w=800&q=75",
    "mirror":      "https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=800&q=75",
    "phone":       "https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?w=800&q=75",
    "fog":         "https://images.unsplash.com/photo-1519897933596-d6f86ad44c63?w=800&q=75",
    "door":        "https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=800&q=75",
    "cemetery":    "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=800&q=75",
    "shadow":      "https://images.unsplash.com/photo-1542810634-71277d95dcbb?w=800&q=75",
    "window":      "https://images.unsplash.com/photo-1518176258769-f227c798150e?w=800&q=75",
}

# Fallback: picsum with fixed seeds
FALLBACK = {
    "cover":    "https://picsum.photos/seed/horror1/900/500",
    "haunted":  "https://picsum.photos/seed/haunted/800/400",
    "forest":   "https://picsum.photos/seed/forest/800/400",
    "mirror":   "https://picsum.photos/seed/mirror/800/400",
    "phone":    "https://picsum.photos/seed/phone/800/400",
    "fog":      "https://picsum.photos/seed/fog/800/400",
    "door":     "https://picsum.photos/seed/door/800/400",
    "cemetery": "https://picsum.photos/seed/cemetery/800/400",
    "shadow":   "https://picsum.photos/seed/shadow/800/400",
    "window":   "https://picsum.photos/seed/window/800/400",
}

def download_image(key):
    path = os.path.join(IMG_DIR, f"{key}.jpg")
    if os.path.exists(path):
        return path
    for url in [IMAGES[key], FALLBACK[key]]:
        try:
            req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
            with urllib.request.urlopen(req, timeout=10) as r:
                data = r.read()
            with open(path, "wb") as f:
                f.write(data)
            print(f"  Downloaded {key}")
            return path
        except Exception as e:
            print(f"  Warning – {key} from {url}: {e}")
    return None

# ─── Styles ───────────────────────────────────────────────────────────────────
def make_styles():
    base = getSampleStyleSheet()

    DARK   = colors.HexColor("#1a1a2e")
    RED    = colors.HexColor("#8b0000")
    CREAM  = colors.HexColor("#fdf6e3")
    GREY   = colors.HexColor("#555555")
    LGREY  = colors.HexColor("#888888")

    styles = {}

    styles["cover_title"] = ParagraphStyle(
        "cover_title", parent=base["Title"],
        fontSize=36, leading=42, textColor=RED,
        fontName="Helvetica-Bold", alignment=TA_CENTER, spaceAfter=6
    )
    styles["cover_sub"] = ParagraphStyle(
        "cover_sub", parent=base["Normal"],
        fontSize=14, leading=18, textColor=DARK,
        fontName="Helvetica-Oblique", alignment=TA_CENTER, spaceAfter=4
    )
    styles["cover_note"] = ParagraphStyle(
        "cover_note", parent=base["Normal"],
        fontSize=11, leading=15, textColor=GREY,
        fontName="Helvetica", alignment=TA_CENTER
    )
    styles["chapter_num"] = ParagraphStyle(
        "chapter_num", parent=base["Normal"],
        fontSize=11, leading=14, textColor=RED,
        fontName="Helvetica-Bold", alignment=TA_CENTER, spaceBefore=10
    )
    styles["chapter_title"] = ParagraphStyle(
        "chapter_title", parent=base["Heading1"],
        fontSize=22, leading=28, textColor=DARK,
        fontName="Helvetica-Bold", alignment=TA_CENTER,
        spaceBefore=6, spaceAfter=4
    )
    styles["chapter_meta"] = ParagraphStyle(
        "chapter_meta", parent=base["Normal"],
        fontSize=10, leading=13, textColor=LGREY,
        fontName="Helvetica-Oblique", alignment=TA_CENTER, spaceAfter=14
    )
    styles["body"] = ParagraphStyle(
        "body", parent=base["Normal"],
        fontSize=11, leading=17, textColor=DARK,
        fontName="Helvetica", alignment=TA_JUSTIFY,
        firstLineIndent=18, spaceAfter=8
    )
    styles["body_first"] = ParagraphStyle(
        "body_first", parent=base["Normal"],
        fontSize=11, leading=17, textColor=DARK,
        fontName="Helvetica", alignment=TA_JUSTIFY,
        firstLineIndent=0, spaceAfter=8
    )
    styles["dropcap"] = ParagraphStyle(
        "dropcap", parent=base["Normal"],
        fontSize=11, leading=17, textColor=DARK,
        fontName="Helvetica", alignment=TA_JUSTIFY,
        firstLineIndent=0, spaceAfter=8
    )
    styles["vocab_head"] = ParagraphStyle(
        "vocab_head", parent=base["Normal"],
        fontSize=11, leading=14, textColor=RED,
        fontName="Helvetica-Bold", spaceBefore=12, spaceAfter=4
    )
    styles["vocab_item"] = ParagraphStyle(
        "vocab_item", parent=base["Normal"],
        fontSize=10, leading=14, textColor=DARK,
        fontName="Helvetica", leftIndent=14, spaceAfter=3
    )
    styles["q_head"] = ParagraphStyle(
        "q_head", parent=base["Normal"],
        fontSize=11, leading=14, textColor=RED,
        fontName="Helvetica-Bold", spaceBefore=10, spaceAfter=4
    )
    styles["q_item"] = ParagraphStyle(
        "q_item", parent=base["Normal"],
        fontSize=10, leading=14, textColor=DARK,
        fontName="Helvetica", leftIndent=14, spaceAfter=5
    )
    styles["caption"] = ParagraphStyle(
        "caption", parent=base["Normal"],
        fontSize=9, leading=12, textColor=LGREY,
        fontName="Helvetica-Oblique", alignment=TA_CENTER, spaceAfter=6
    )
    styles["toc_title"] = ParagraphStyle(
        "toc_title", parent=base["Heading1"],
        fontSize=18, leading=24, textColor=DARK,
        fontName="Helvetica-Bold", alignment=TA_CENTER,
        spaceBefore=10, spaceAfter=16
    )
    styles["toc_item"] = ParagraphStyle(
        "toc_item", parent=base["Normal"],
        fontSize=11, leading=18, textColor=DARK,
        fontName="Helvetica", leftIndent=20
    )
    styles["intro_head"] = ParagraphStyle(
        "intro_head", parent=base["Heading2"],
        fontSize=16, leading=22, textColor=DARK,
        fontName="Helvetica-Bold", alignment=TA_CENTER,
        spaceBefore=8, spaceAfter=8
    )
    styles["intro_body"] = ParagraphStyle(
        "intro_body", parent=base["Normal"],
        fontSize=11, leading=17, textColor=DARK,
        fontName="Helvetica", alignment=TA_JUSTIFY,
        spaceAfter=8
    )
    styles["section_divider"] = ParagraphStyle(
        "section_divider", parent=base["Normal"],
        fontSize=14, leading=18, textColor=RED,
        fontName="Helvetica-Bold", alignment=TA_CENTER,
        spaceBefore=6, spaceAfter=6
    )
    return styles

# ─── Story data ───────────────────────────────────────────────────────────────
STORIES = [
    {
        "num": "Story One",
        "title": "The Last Room",
        "level": "B2 | ~800 words | Theme: Haunted house",
        "image": "haunted",
        "caption": "The old Morrow house had been empty for twenty years.",
        "text": [
            '<font color="#8b0000"><b>T</b></font>he real estate agent warned Maya before she even opened the car door. "Room 13," she said quietly, "has never been rented. Not once in forty years." She handed Maya the keys with a tight smile and drove away before Maya could ask why.',
            "Maya was a practical woman. She was a nurse, used to long night shifts and difficult situations. She did not believe in ghosts or superstitions. The old Morrow house was cheap, large, and close to the hospital. That was all she needed to know.",
            "The first week passed without incident. Maya settled into her new home, hung her pictures on the walls, and cooked dinner each evening with the radio playing. The house was cold, yes — old houses always were — but nothing unusual happened.",
            "On the eighth night, she heard the knocking.",
            "It began just after midnight: three slow, deliberate knocks from somewhere above her head. She lay still in her bed and listened. One. Two. Three. Then silence. She told herself it was the pipes. Old houses were full of strange sounds.",
            "But the knocking returned the next night. And the night after that. Always three knocks, always after midnight, always from the same direction — the corridor outside Room 13.",
            "On the fourth night, Maya got up. She pulled on her dressing gown and walked down the dark corridor, her torch casting long shadows on the peeling wallpaper. She stopped in front of the door. The paint was dark with age, and someone had scratched letters into the wood at eye level. She leaned closer and read: <i>DO NOT OPEN. SHE IS STILL WAITING.</i>",
            "Maya's hand rested on the door handle. The metal was ice cold. From inside the room came a sound she had not expected: not a knock, not a moan, but something far worse. A child's voice, singing softly. A lullaby she had heard somewhere before — a melody from her own childhood, from a music box her grandmother had kept on the window ledge.",
            "Her torch flickered and died.",
            "In the total darkness, the singing grew louder.",
            "Maya let go of the handle. She walked back down the corridor, back to her room, and sat on the edge of her bed until morning. She did not run. She did not scream. But at seven o'clock, she called the real estate agent and told her she was leaving.",
            '"Did something happen?" the agent asked.',
            '"No," Maya said. "Nothing happened."',
            "But as she packed her suitcase, she noticed something she had not seen before. On her bedroom wall — the wall that backed onto Room 13 — the paint had bubbled and cracked overnight. In the centre of the damage, clear as a fingerprint, was the outline of a small hand.",
            "Maya finished packing without looking at the wall again. She left the house keys on the kitchen table and stepped out into the bright morning sun. She never went back.",
            "Later, she thought about the lullaby. She looked it up. The melody came from an old folk song — a song about a mother who had lost her daughter. A mother who was still searching.",
            "Maya slept with the lights on for a very long time after that.",
        ],
        "vocab": [
            ("superstition (n.)", "a belief that has no logical explanation, e.g. 'breaking a mirror brings bad luck'"),
            ("deliberate (adj.)", "done slowly and carefully, showing clear intention"),
            ("corridor (n.)", "a long narrow passage in a building"),
            ("peeling (adj.)", "coming off in thin layers; used here to describe old wallpaper"),
            ("lullaby (n.)", "a gentle song sung to help a child sleep"),
            ("back onto (v. phrase)", "to have one surface touching the back of another room or building"),
        ],
        "questions": [
            "1. Why was Maya not afraid of the house at first? Give two reasons from the text.",
            "2. What did Maya find written on the door of Room 13? What do you think it means?",
            "3. Why was the lullaby especially frightening for Maya?",
            "4. Describe the evidence Maya found on her bedroom wall. What might it suggest?",
            "5. Discussion: Why do you think Maya said 'Nothing happened' to the agent? Was she telling the truth?",
        ],
    },
    {
        "num": "Story Two",
        "title": "The Mirror at the End of the Hall",
        "level": "B2 | ~750 words | Theme: Doppelganger / Identity",
        "image": "mirror",
        "caption": "She looked into the mirror and saw someone who was not quite her.",
        "text": [
            '<font color="#8b0000"><b>L</b></font>eila bought the mirror at a weekend market, from a man who would not meet her eyes. It was tall and antique, with a dark wooden frame carved into the shapes of twisted vines. The glass was perfectly clear. She paid thirty euros and carried it home on the bus, balanced awkwardly across her lap.',
            "She hung it at the end of the hallway, where the light from the kitchen window fell across it in the afternoon. It looked beautiful. She was pleased with herself.",
            "For the first few days, the mirror was simply a mirror. Leila checked her appearance in it before leaving for work, as one does. She noticed nothing unusual.",
            "Then, on a Thursday evening, she caught something in the corner of her eye.",
            "She was walking past the mirror when she sensed movement — not in front of her, but inside the glass. She stopped and looked directly at her reflection. Everything was normal: her dark coat, her tired face, her bag over her shoulder. But the reflection had not stopped when she did. It was still moving — still walking forward — for just a fraction of a second after she had frozen.",
            "Leila blinked. The reflection was still now. Perfectly still. She told herself she had imagined it.",
            "Over the following days, she began to watch the mirror more carefully. She noticed small things. The way her reflection sometimes smiled when she was not smiling. The way it tilted its head in the opposite direction. Once, she waved her left hand at the mirror. Her reflection waved back — but with its left hand too. Which, if you think about it, is wrong. A true mirror image should show you waving with the right.",
            "She mentioned it to her colleague at work, a man named David who liked strange stories.",
            '"Sounds like a fetch,' he said.",
            '"A what?"',
            '"A fetch. In old Irish folklore, your fetch is your double — your exact copy. If you see it, it means death is coming for you."',
            '"That\'s ridiculous," Leila said.',
            '"Probably," David agreed.',
            "That night, Leila stood in front of the mirror for a long time. She raised her right hand. The reflection raised its left. She turned her head to the right. The reflection turned left. All normal. But then she stepped back, preparing to leave — and the reflection did not move. It stayed exactly where it was, face forward, watching her.",
            "Leila did not sleep. She spent the night on the sofa, facing away from the hallway.",
            "In the morning, she took the mirror down. She wrapped it in old sheets and carried it to the skip at the end of the street. She dropped it in and walked away quickly, not looking back.",
            "Three days later, she received a parcel in the post. No return address. She opened it to find a small square of dark wooden frame — carved into the shape of twisted vines — and a note in handwriting she did not recognise.",
            "The note said: <i>You forgot a piece.</i>",
            "She stood for a long time holding the wood in her hands. Then she went to the hallway — the empty hallway where the mirror had been — and looked at the wall.",
            "There was a mark on the plaster. A thin scratch, in the exact outline of a human figure, as if something had pressed itself against the wall and left its shape behind.",
            "Leila moved out of the flat that weekend. She left the piece of wood on the kitchen counter and did not take it with her.",
            "She still checks her reflection sometimes — in shop windows, in phone screens — and watches it for just a moment longer than necessary.",
            "It always moves when she does. Always. She makes sure of that.",
        ],
        "vocab": [
            ("antique (adj.)", "old and valuable, usually more than 100 years old"),
            ("doppelganger (n.)", "a ghostly double of a living person; from German, meaning 'double-walker'"),
            ("reflection (n.)", "the image you see in a mirror or shiny surface"),
            ("fetch (n.)", "in Irish folklore, a supernatural double of a living person, seen as a bad omen"),
            ("parcel (n.)", "a wrapped package sent by post"),
            ("plaster (n.)", "the smooth material covering the inside walls of a building"),
        ],
        "questions": [
            "1. What unusual things did Leila notice about her reflection? List at least three.",
            "2. Why was it wrong that the reflection waved back with its left hand?",
            "3. What is a 'fetch' according to the story? Where does the word come from?",
            "4. What did Leila find on the wall after removing the mirror? What is the significance?",
            "5. Discussion: Do you think Leila was safe at the end of the story? Explain your answer using evidence from the text.",
        ],
    },
    {
        "num": "Story Three",
        "title": "Signal Lost",
        "level": "B2 | ~700 words | Theme: Technology / Isolation",
        "image": "phone",
        "caption": "The message arrived from a number she did not recognise.",
        "text": [
            '<font color="#8b0000"><b>T</b></font>he first message arrived on a Sunday night, at 11:47 PM. James was already in bed, half asleep, when his phone lit up on the nightstand.',
            "Unknown number. The message read: <i>You need to leave the house. Now.</i>",
            "James stared at the screen. He typed back: <i>Who is this?</i>",
            "The reply came instantly: <i>There is no time. Please. Leave now.</i>",
            "He put the phone down and told himself it was a wrong number, or a prank. He turned off the screen and closed his eyes.",
            "The second message arrived at 2:14 AM: <i>Why are you still there? I can see the light under your door. Please, James. Leave.</i>",
            "He sat up fast. His name. The message had used his name. And the detail about the light — his bedside lamp was on, visible as a thin orange line under the gap at the bottom of his bedroom door. Whoever this was could see it.",
            "He called the number immediately. It rang once, twice, three times — then a recording: <i>The number you have dialled does not exist. Please check the number and try again.</i>",
            "James got out of bed. He walked through the flat, turning on every light, checking every room. The front door was locked. The windows were closed. The flat was empty. He stood in the kitchen and tried to calm his breathing.",
            "His phone buzzed again: <i>Not outside. It is already inside with you. In the walls. Do you hear it?</i>",
            "He listened. And then he did hear it — a faint, rhythmic sound from inside the wall beside the refrigerator. A slow, wet sound, like something breathing.",
            "His thumbs were shaking as he typed: <i>What is in the walls?</i>",
            "The reply: <i>I don't know. I only know it followed me from the last place I lived. And then I didn't leave in time. You have to go. Take nothing. Just go.</i>",
            "James grabbed his keys and his coat. He ran to the front door, opened it, and stepped into the bright light of the corridor outside.",
            "He never went back into the flat alone. The building manager sent someone to check the walls two days later. The man found nothing unusual.",
            "But that evening, the building manager received a text message from an unknown number. It contained only four words:",
            "<i>Tell him to leave.</i>",
            "And a few seconds later, a second message:",
            "<i>Tell the new one.</i>",
            "The building manager did not understand. He deleted the messages and went home.",
            "James moved to a new city. He changed his phone number. He left no forwarding address.",
            "He still sleeps with all the lights on. And he has developed a habit he cannot explain, even to himself: every night before bed, he presses his ear against the wall of his bedroom and listens.",
            "He has not heard anything yet.",
        ],
        "vocab": [
            ("nightstand (n.)", "a small table beside a bed; also called a 'bedside table'"),
            ("prank (n.)", "a trick played on someone as a joke"),
            ("rhythmic (adj.)", "having a regular, repeated pattern of sound or movement"),
            ("forwarding address (n. phrase)", "a new address you give so that post can be sent on to you"),
            ("faint (adj.)", "very quiet or difficult to hear or see; not strong"),
            ("buzz (v.)", "to make a low, vibrating sound; used here for a phone notification"),
        ],
        "questions": [
            "1. What detail in the second message showed James that the sender could actually see him?",
            "2. Why was the phone number impossible to call back?",
            "3. What did James hear inside the wall? Why was this significant?",
            "4. Who do you think was sending the messages? Give reasons for your answer.",
            "5. Writing task: Write James's diary entry for the night he received the messages (80-100 words).",
        ],
    },
    {
        "num": "Story Four",
        "title": "The Clearing",
        "level": "B2 | ~800 words | Theme: Nature / The unknown",
        "image": "forest",
        "caption": "The forest path ended without warning at the edge of the clearing.",
        "text": [
            '<font color="#8b0000"><b>N</b></font>o one in the village could explain the clearing. It appeared on no map. The trees around it grew dense and close, right up to its edge, but inside the circular space the ground was bare — no grass, no moss, no fallen leaves. Just flat, dark earth, perfectly level, as though it had been prepared for something.',
            "Nina found it on her third walk in the forest. She was a geography teacher from the city, staying at her aunt's cottage for the summer. She liked walking. She had good boots and a compass and a habit of exploring paths that were not on the map.",
            "The clearing made her uncomfortable in a way she could not name. It was the silence, partly — inside the clearing, even the birds stopped. And it was the shape: a perfect circle, roughly thirty metres across, the edges clean and precise as if drawn with a compass.",
            "She photographed it and walked on.",
            "That evening, she looked at the photos on her camera. The clearing was there in most of them — the bare earth, the ring of trees. But in one photograph, taken from the northern edge, there was something else. At the centre of the clearing, clearly visible, stood a figure.",
            "Nina did not remember seeing a figure. She enlarged the photo on her laptop screen. The figure was tall and thin, standing very still, facing directly at the camera. It had no features that she could make out — just a dark shape, like a shadow given a human form.",
            "She showed the photo to her aunt that evening.",
            'Her aunt looked at it for a long moment, then set the laptop down. "You went to the clearing," she said. It was not a question.',
            '"You know it?"',
            '"Everyone in the village knows it. We don\'t go there. We don\'t talk about it to outsiders."',
            '"What is the figure in the photograph?"',
            'Her aunt was quiet for a moment. "Something that stays there," she said. "It has always been there, as long as anyone can remember. It doesn\'t come out of the clearing. It doesn\'t do anything. It just stands and watches. As long as you don\'t go back, you\'ll be fine."',
            '"And if I go back?"',
            'Her aunt looked at her steadily. "Then it will know you saw it."',
            "Nina closed the laptop. She did not go back to the clearing. She spent the rest of the summer walking on the marked paths, within sight of the village, always back before dark.",
            "On her last evening, she deleted the photographs from her camera. All of them except one, which she had already sent to her email before she thought better of it.",
            "When she got back to the city, she opened the email. The photograph was there in her inbox, attached and waiting.",
            "She opened it.",
            "In the photo, the figure was no longer at the centre of the clearing.",
            "It was at the edge. Closer to the camera. Much closer.",
            "Nina forwarded the email to no one. She changed her email password. She marked the original as deleted and emptied the trash folder.",
            "She never went back to her aunt's village. But every night, for months afterwards, she had the same dream: a circle of trees, bare dark earth, and somewhere behind her, just out of sight, the sound of something very still — watching.",
        ],
        "vocab": [
            ("clearing (n.)", "an open space in a forest with no trees"),
            ("precise (adj.)", "exact and accurate, with no errors"),
            ("enlarge (v.)", "to make something bigger; especially an image or photograph"),
            ("make out (v. phrase)", "to see or understand something with difficulty"),
            ("inbox (n.)", "the folder in an email programme where new messages arrive"),
            ("steady (adj.)", "calm, firm, and not nervous"),
        ],
        "questions": [
            "1. What made the clearing unusual? Describe it in your own words.",
            "2. How did Nina's aunt react to the photograph? What does her reaction tell us?",
            "3. What changed in the photograph between when Nina took it and when she saw it on her laptop?",
            "4. What is the significance of the figure moving closer to the edge in the final image?",
            "5. Discussion: Nina deleted the photos but kept one in her email. What does this suggest about her character?",
        ],
    },
    {
        "num": "Story Five",
        "title": "Fog on the Road",
        "level": "B2 | ~750 words | Theme: Road / Supernatural encounter",
        "image": "fog",
        "caption": "The fog came down without warning on the empty road.",
        "text": [
            '<font color="#8b0000"><b>T</b></font>om was driving home along the coastal road when the fog came down. One moment the road was clear; the next, a wall of white had closed around his car from all sides. He slowed to a crawl and turned on his hazard lights, though there was no one else on the road.',
            "He had been driving this road since he was seventeen. He knew every curve, every dip, every loose section of tarmac. He was not afraid of fog.",
            "The woman appeared out of nowhere.",
            "She was standing in the middle of the road, facing him, perfectly still in his headlights. She was wearing a white dress, quite thin for the cold weather, and she was soaking wet — as if she had just climbed out of the sea. Her dark hair was plastered flat against her face.",
            "Tom hit the brakes hard. The car stopped less than two metres from her. His heart was slamming against his ribs.",
            "He lowered his window. 'Are you all right?' he called out. 'Do you need help?'",
            "The woman walked around to the passenger side of the car. Tom leaned over and opened the door, and she got in. She was cold — he could feel it radiating off her, as though she had been standing in the sea for hours. She smelled of salt and something else he could not identify. She was shivering violently.",
            "'Where do you need to go?' he asked.",
            "She gave him an address — a house about two kilometres up the road. Her voice was calm, almost flat, as though she was very tired.",
            "Tom drove. He tried to make conversation — asked if she was all right, whether she needed him to call someone, what had happened — but she answered in short sentences and looked straight ahead. He noticed she left no wet mark on the seat.",
            "He pulled up in front of the address she had given: a small stone cottage set back from the road. The lights were on inside.",
            "'Here you are,' he said, and turned to look at her.",
            "The seat was empty. The door was closed. The passenger seat was dry and undisturbed.",
            "Tom sat for a long moment, his hands on the wheel. Then he got out of the car and walked to the cottage door and knocked.",
            "An old man answered. He looked at Tom for a moment, then past him at the car.",
            "'She found another one,' the old man said quietly, as if to himself.",
            "'I'm sorry?' Tom said.",
            "The old man looked at him properly. 'My daughter,' he said. 'She drowned on this road, seven years ago. There was fog, just like tonight. She drove off the edge into the sea.' He paused. 'She comes back sometimes, on nights like this. She flags down drivers on the road she died on. I think she is trying to find her way home.'",
            "Tom could not speak.",
            "'Come in,' the old man said. 'You look like you need to sit down.'",
            "Tom sat in the warm kitchen and drank tea and listened to the old man talk about his daughter — what she had been like, what she had loved, what she had wanted to do with her life. Tom stayed for an hour. When he left, the fog had lifted.",
            "He drove home carefully. He took no more coastal roads at night for a long time after that.",
            "But he kept thinking about her voice. Calm. Flat. Tired. Not the voice of something frightening.",
            "Just the voice of someone who wanted to go home.",
        ],
        "vocab": [
            ("hazard lights (n. phrase)", "flashing amber lights on a car, used to warn other drivers of danger"),
            ("tarmac (n.)", "the hard black surface material used to make roads"),
            ("plaster (v.)", "to stick flat against a surface, usually when wet"),
            ("radiate (v.)", "to send out heat, light, or cold in all directions"),
            ("undisturbed (adj.)", "not touched or moved; exactly as it was left"),
            ("flag down (v. phrase)", "to wave at a car or person to make them stop"),
        ],
        "questions": [
            "1. How did Tom know the woman had been in the sea? Give two details from the text.",
            "2. What strange detail suggested she might not be an ordinary person?",
            "3. Who was the woman, according to the old man?",
            "4. Why does Tom remember her voice as 'not frightening'? What does this tell us about the tone of the story?",
            "5. Writing task: Rewrite the scene from the perspective of the woman in white (80-100 words).",
        ],
    },
    {
        "num": "Story Six",
        "title": "The House That Came Back",
        "level": "B2 | ~700 words | Theme: Memory / Alternate reality",
        "image": "window",
        "caption": "The house had been demolished five years before. He was sure of it.",
        "text": [
            '<font color="#8b0000"><b>D</b></font>aniel had grown up in Number 14, Farrow Street. He had left at eighteen, gone to university, and never moved back. His parents had sold the house when he was twenty-two, and it had been demolished shortly after to make way for a car park.',
            "He knew this. He had seen the photographs.",
            "So when he drove down Farrow Street on a Thursday afternoon in October, on his way to visit an old school friend, he nearly drove into the pavement.",
            "Number 14 was there. Exactly as he remembered it: red brick, white window frames, the climbing rose his mother had planted along the front wall, the slightly crooked gatepost his father had always meant to fix. The lights were on inside. Smoke was rising from the chimney.",
            "Daniel sat in the car for a long time. He got out, walked up to the gate, and stood looking at the house. Up close, it was exactly right — the same peeling paint above the front door, the same cracked tile on the doorstep, the same worn brass letter box. It was not a similar house. It was his house.",
            "He rang the doorbell.",
            "A woman answered. She was in her fifties, wearing an apron, drying her hands on a tea towel. She looked at him pleasantly.",
            '"Can I help you?" she asked.',
            '"I — I used to live here," Daniel said. "Years ago. I\'m sorry, I just wanted — I mean, I didn\'t think the house was still standing."',
            'The woman smiled. "Oh, we\'ve been here thirty years," she said. "Would you like to come in for a moment? Lots of people from the old days come back sometimes."',
            "He stepped inside. Everything was different and yet the same. The layout was unchanged — hallway, stairs on the left, kitchen ahead — but the furniture and decoration were unfamiliar. He stood in the hallway and felt something pull at him, as though the house itself was applying a gentle pressure.",
            '"Would you like to see your old room?" the woman asked.',
            "He followed her upstairs. The door at the top was the same door — he recognised the slight warp in the wood that had always made it difficult to close. She opened it.",
            "Inside was a child's bedroom. There were drawings pinned to the wall. He looked at them and felt his chest tighten, because he recognised the drawings. They were his — his own childhood drawings, in his own handwriting, with the house's address at the top in the wobbly printed letters of a seven-year-old.",
            '"These are mine,' he said.',
            '"Yes," the woman said, as if this was perfectly ordinary. "They were here when we moved in. We always thought someone would come back for them one day."',
            "Daniel took the drawings down from the wall carefully, folded them, and put them in his pocket. He thanked the woman and left.",
            "He drove straight home. He didn't go to his friend's house. He sat at his kitchen table with the drawings spread in front of him and tried to think clearly.",
            "The next day, he drove back to Farrow Street.",
            "Number 14 was a car park.",
            "It had always been a car park. He stood at the edge of the concrete and looked at the space where his bedroom window would have been, and he tried to remember the woman's face.",
            "He could not. The memory had already gone, like a dream that disappears in the first bright moments of morning.",
            "But the drawings were still in his jacket pocket. He could feel them.",
        ],
        "vocab": [
            ("demolish (v.)", "to deliberately destroy a building"),
            ("crooked (adj.)", "not straight; bent or twisted"),
            ("brass (n./adj.)", "a yellowish metal; used here for a 'letter box' on a door"),
            ("warp (n.)", "a bend in a surface, especially wood, caused by heat or moisture"),
            ("apply pressure (v. phrase)", "to push steadily on something; here used metaphorically"),
            ("wobbly (adj.)", "not steady; moving or written unevenly"),
        ],
        "questions": [
            "1. How did Daniel know the house in front of him was specifically his house, not just a similar one?",
            "2. What was strange about the children's drawings in the upstairs room?",
            "3. What happened when Daniel returned to Farrow Street the next day?",
            "4. What do you think the drawings represent? Consider the story's themes.",
            "5. Discussion: Is this story more sad than scary? Explain your view.",
        ],
    },
    {
        "num": "Story Seven",
        "title": "Knock Twice",
        "level": "B2 | ~650 words | Theme: Folklore / Ritual",
        "image": "door",
        "caption": "Her grandmother had always knocked twice before entering any dark room.",
        "text": [
            '<font color="#8b0000"><b>S</b></font>ara's grandmother had a rule. Before entering any dark room, you knock twice on the door frame. "To let them know you\'re coming," she always said. She never explained who <i>them</i> were.',
            "Sara had grown up with this rule, the way children grow up with all their family's habits — unquestioned, automatic. She knocked twice before entering dark rooms. She did it without thinking.",
            "After her grandmother died, Sara moved into the old house to sort through her belongings. It was a large farmhouse, full of low ceilings and dark corridors and rooms that smelled of old wood and dried lavender.",
            "On the third day, she was carrying boxes down from the attic when she stopped outside the small door to the storage room at the end of the upper corridor. It was dark inside — the single bulb had blown years ago and no one had replaced it. Sara reached for the light switch, then stopped.",
            "She had not knocked.",
            "She felt the urge to laugh at herself. She was thirty-four years old. She was a secondary school teacher. She did not believe in old rules.",
            "She opened the door without knocking.",
            "The room was full of her grandmother's things — old furniture, cardboard boxes, a rolled-up rug. Everything was covered in a thin layer of dust. The only light came from a small window set high in the wall.",
            "Sara stepped inside. She felt it immediately: a pressure behind her eyes, a sudden dizziness, and then — very clearly, very close — the sensation of something standing just behind her left shoulder. Not a sound. Not a movement. Just a presence, as solid and certain as a wall.",
            "She turned around. The room was empty.",
            "But the door was closed.",
            "She was sure — completely sure — that she had left it open.",
            "Her hand shook slightly as she reached for the handle. She pulled. The door opened easily. The corridor was empty. She walked out quickly and stood in the light, breathing.",
            "That night, going to bed, she stopped at the dark entrance to her grandmother's bedroom. Without thinking — without deciding — her hand rose and knocked twice on the door frame.",
            "From inside the dark room came the sound of someone knocking back.",
            "Two slow knocks. Deliberate. Calm. Like an answer.",
            "Sara stood very still. Her heart was moving in her throat.",
            "She reached in, turned on the light. The room was empty and still and full of her grandmother's familiar things — photographs on the dresser, reading glasses on the nightstand, a half-finished book face-down on the pillow.",
            "Sara went to bed and lay awake for hours.",
            "In the morning, she asked her mother about the knocking rule.",
            'Her mother was quiet for a moment. Then: "Mum always said the house was old. That things had been in it for a very long time. The rule wasn\'t about keeping them out. It was about keeping the peace."',
            '"What does that mean?"',
            '"It means: as long as you knock, they know you\'re polite. They leave you alone." She paused. "Did you not knock?"',
            "Sara said nothing.",
            'Her mother said: "Did they knock back?"',
            "Still Sara said nothing.",
            "Her mother took a long breath. 'Finish what you\'re doing there,' she said quietly. 'And don\'t forget to knock.'",
            "Sara spent two more days in the house. She knocked every time, on every dark door.",
            "Nothing knocked back again.",
            "She has no idea whether that is a relief or whether it should worry her more.",
        ],
        "vocab": [
            ("lavender (n.)", "a plant with purple flowers, often used to make rooms smell pleasant"),
            ("attic (n.)", "the space inside the roof of a house, often used for storage"),
            ("sensation (n.)", "a feeling, especially a physical one that you cannot fully explain"),
            ("presence (n.)", "the feeling that someone is nearby, even if you cannot see them"),
            ("deliberate (adj.)", "intentional; done on purpose, not by accident"),
            ("keep the peace (idiom)", "to maintain calm and avoid conflict or trouble"),
        ],
        "questions": [
            "1. What was Sara's grandmother's rule, and why did she have it?",
            "2. What happened when Sara entered the storage room without knocking?",
            "3. What was the most frightening moment in the story? Why?",
            "4. What explanation does Sara's mother give for the knocking rule?",
            "5. Language focus: Find three examples of past perfect tense in the story and explain why the author used it.",
        ],
    },
    {
        "num": "Story Eight",
        "title": "The Shape in the Photograph",
        "level": "B2 | ~650 words | Theme: Photography / The past",
        "image": "shadow",
        "caption": "The old photograph showed something that should not have been there.",
        "text": [
            '<font color="#8b0000"><b>T</b></font>he photograph was taken in 1987. It showed a family on a beach: a man and a woman, their three children, a dog. A happy, ordinary scene. Blue sky. Clear water. Everyone looking at the camera, everyone smiling.',
            "Except for the figure at the far right of the frame.",
            "The figure was tall and thin, slightly out of focus, standing at the edge of the group as if it belonged there — but no one who looked at the photograph had ever been able to say who it was. The family who owned the photograph could not identify it. The children, now adults, had no memory of a stranger on the beach that day.",
            "The photograph came to Clara through an estate sale — a box of old photos from a house whose owners had died without family. She liked old photographs. She collected them.",
            "She noticed the figure immediately. She enlarged it on her computer. The shape was human in outline but oddly proportioned — the limbs slightly too long, the head held at an angle that felt wrong, the face not quite a face, more like an impression of one. A suggestion.",
            "Clara posted the photograph in an online group for people who collected anonymous old photos. The responses were fast and numerous.",
            "Several people said they had a photo like this one. Not the same beach, not the same family — but a similar figure. Always at the edge. Always slightly out of focus. Always unidentifiable.",
            "One user had a photo from 1954. The same shape, standing at the back of a wedding party.",
            "Another had one from 1971 — a school photograph, seventy children in rows, and the figure standing at the very end of the back row.",
            "A third had a photograph from 1939.",
            "Clara began to compile a document. She collected twenty-three photographs over three months, from different users, different countries, different decades. She lined them up in chronological order and looked at them.",
            "The figure was the same in all of them. Same height. Same angle of the head. Same suggestion of a face.",
            "She wrote a post describing her findings. It received more than four hundred comments. Most were curious. Some were frightened. A few were dismissive.",
            "Then she received a private message from an account that had never posted before.",
            "It said: <i>You should stop collecting these. It knows you are looking.</i>",
            "Clara wrote back: <i>Who are you?</i>",
            "The account had been deleted by the time the message sent.",
            "Clara closed the document. She thought about it for a day. Then she packaged up all twenty-three photographs, put them in a cardboard box, and put the box in her garden shed.",
            "She took a photograph of the shed to remember where they were.",
            "When she downloaded the photo and looked at it on her screen, the shed was clearly visible in the background. In the foreground was the garden: the grass, the flower beds, the apple tree.",
            "And at the far right of the frame, slightly out of focus, the figure.",
            "Clara closed the image. She has not opened it again.",
            "She no longer collects old photographs.",
        ],
        "vocab": [
            ("estate sale (n.)", "a sale of the belongings of a person who has died"),
            ("proportioned (adj.)", "having parts that relate well — or badly — to each other in size"),
            ("anonymous (adj.)", "without a known name or identity"),
            ("chronological (adj.)", "arranged in the order in which things happened"),
            ("compile (v.)", "to collect and organise information into a single document or list"),
            ("dismissive (adj.)", "treating something as unimportant or not worth taking seriously"),
        ],
        "questions": [
            "1. What was strange about the figure in the original photograph?",
            "2. What pattern did Clara discover when she collected photographs from other users?",
            "3. Why was the private message Clara received especially frightening?",
            "4. What was the final evidence that the figure was real and not just an old photo defect?",
            "5. Discussion: What does this story suggest about what happens when we pay too much attention to something unknown?",
        ],
    },
]

# ─── Helper: try loading an image ────────────────────────────────────────────
def get_image_obj(key, width_cm=14.0, max_height_cm=7.5):
    path = download_image(key)
    if path and os.path.exists(path):
        try:
            img = Image(path)
            w = width_cm * cm
            aspect = img.imageWidth / img.imageHeight
            h = min(w / aspect, max_height_cm * cm)
            w = h * aspect
            img.drawWidth = w
            img.drawHeight = h
            return img
        except Exception as e:
            print(f"  Image error {key}: {e}")
    return None

# ─── Build document ───────────────────────────────────────────────────────────
def build_pdf():
    doc = SimpleDocTemplate(
        OUT_PATH,
        pagesize=A4,
        leftMargin=MARGIN, rightMargin=MARGIN,
        topMargin=MARGIN, bottomMargin=MARGIN,
        title="Shadows & Whispers: A Horror Anthology for ESL Learners",
        author="Horror Handbook Series",
        subject="B2 English short horror stories for ESL students",
    )

    S = make_styles()
    story = []
    RED = colors.HexColor("#8b0000")
    DARK = colors.HexColor("#1a1a2e")
    CREAM = colors.HexColor("#fdf6e3")

    # ── COVER PAGE ──────────────────────────────────────────────────────────
    cover_img = get_image_obj("cover", width_cm=15.0, max_height_cm=8.5)
    if cover_img:
        cover_img.hAlign = "CENTER"
        story.append(Spacer(1, 0.5*cm))
        story.append(cover_img)
    story.append(Spacer(1, 0.6*cm))
    story.append(Paragraph("SHADOWS &amp; WHISPERS", S["cover_title"]))
    story.append(Paragraph("A Horror Anthology for ESL Learners", S["cover_sub"]))
    story.append(HRFlowable(width="60%", thickness=1.5, color=RED, spaceAfter=6, spaceBefore=6, hAlign="CENTER"))
    story.append(Paragraph("8 Original Short Horror Stories · B2 English Level", S["cover_note"]))
    story.append(Paragraph("With Vocabulary Notes &amp; Comprehension Questions", S["cover_note"]))
    story.append(Spacer(1, 0.4*cm))
    story.append(Paragraph("Horror Handbook Series · ESL Reading Collection", S["cover_note"]))
    story.append(PageBreak())

    # ── INTRODUCTION ────────────────────────────────────────────────────────
    story.append(Spacer(1, 0.8*cm))
    story.append(Paragraph("How to Use This Book", S["intro_head"]))
    story.append(HRFlowable(width="50%", thickness=1, color=RED, spaceAfter=10, spaceBefore=4, hAlign="CENTER"))
    intro_paras = [
        "Welcome to <i>Shadows &amp; Whispers</i>, a collection of eight original short horror stories written for English learners at the B2 (Upper Intermediate) level.",
        "Horror is one of the oldest storytelling traditions in every culture. These stories explore fear, mystery, and the unknown — not through graphic violence, but through atmosphere, suspense, and the power of imagination. They are designed to be read alone, late at night, with the lights on.",
        "<b>Each story includes:</b>",
        "• The story itself, written at a consistent B2 level with challenging but learnable vocabulary.",
        "• A <b>Vocabulary Box</b> with key words and phrases explained in simple English.",
        "• <b>Comprehension Questions</b> to test understanding and develop critical thinking.",
        "• A <b>Writing or Discussion Task</b> to practise active use of the language.",
        "<b>Reading tips for ESL learners:</b>",
        "• Read each story twice: once for pleasure, once for detail.",
        "• Note any words you do not know and check them in context before using the Vocabulary Box.",
        "• The questions range from straightforward comprehension to opinion and analysis — all answers are valid as long as you support them with evidence from the text.",
        "Good luck. And do not read these alone in the dark.",
    ]
    for p in intro_paras:
        story.append(Paragraph(p, S["intro_body"]))
    story.append(PageBreak())

    # ── TABLE OF CONTENTS ───────────────────────────────────────────────────
    story.append(Spacer(1, 0.6*cm))
    story.append(Paragraph("Contents", S["toc_title"]))
    story.append(HRFlowable(width="60%", thickness=1, color=RED, spaceAfter=12, spaceBefore=4, hAlign="CENTER"))
    toc_entries = [
        ("How to Use This Book", "2"),
        ("Story One: The Last Room", "4"),
        ("Story Two: The Mirror at the End of the Hall", "8"),
        ("Story Three: Signal Lost", "12"),
        ("Story Four: The Clearing", "16"),
        ("Story Five: Fog on the Road", "20"),
        ("Story Six: The House That Came Back", "24"),
        ("Story Seven: Knock Twice", "28"),
        ("Story Eight: The Shape in the Photograph", "32"),
        ("About B2 English Level", "36"),
        ("Answer Guidance for Teachers", "37"),
    ]
    for title, page in toc_entries:
        toc_data = [[Paragraph(title, S["toc_item"]), Paragraph(page, ParagraphStyle("pr", parent=S["toc_item"], alignment=TA_RIGHT))]]
        tbl = Table(toc_data, colWidths=[13*cm, 2*cm])
        tbl.setStyle(TableStyle([
            ("BOTTOMPADDING", (0,0), (-1,-1), 4),
            ("TOPPADDING", (0,0), (-1,-1), 4),
            ("LINEBELOW", (0,0), (-1,-1), 0.3, colors.HexColor("#cccccc")),
        ]))
        story.append(tbl)
    story.append(PageBreak())

    # ── STORIES ─────────────────────────────────────────────────────────────
    for s in STORIES:
        # Chapter header
        story.append(Spacer(1, 0.5*cm))
        story.append(Paragraph(s["num"].upper(), S["chapter_num"]))
        story.append(Paragraph(s["title"], S["chapter_title"]))
        story.append(Paragraph(s["level"], S["chapter_meta"]))
        story.append(HRFlowable(width="40%", thickness=1, color=RED, spaceAfter=10, spaceBefore=2, hAlign="CENTER"))

        # Story image
        img = get_image_obj(s["image"], width_cm=13.5, max_height_cm=7.0)
        if img:
            img.hAlign = "CENTER"
            story.append(img)
        story.append(Paragraph(s["caption"], S["caption"]))
        story.append(Spacer(1, 0.3*cm))

        # Story text
        for i, para in enumerate(s["text"]):
            style = S["dropcap"] if i == 0 else S["body"]
            story.append(Paragraph(para, style))

        story.append(Spacer(1, 0.5*cm))
        story.append(HRFlowable(width="30%", thickness=0.8, color=RED, spaceAfter=8, spaceBefore=4, hAlign="CENTER"))

        # Vocab box
        story.append(Paragraph("Vocabulary", S["vocab_head"]))
        for word, defn in s["vocab"]:
            story.append(Paragraph(f"<b>{word}</b> — {defn}", S["vocab_item"]))

        story.append(Spacer(1, 0.3*cm))

        # Questions
        story.append(Paragraph("Comprehension &amp; Discussion", S["q_head"]))
        for q in s["questions"]:
            story.append(Paragraph(q, S["q_item"]))

        story.append(PageBreak())

    # ── B2 LEVEL GUIDE PAGE ─────────────────────────────────────────────────
    story.append(Spacer(1, 0.8*cm))
    story.append(Paragraph("About B2 English Level", S["intro_head"]))
    story.append(HRFlowable(width="50%", thickness=1, color=RED, spaceAfter=10, spaceBefore=4, hAlign="CENTER"))
    b2_paras = [
        "The stories in this book are written for learners at the <b>B2 (Upper Intermediate)</b> level of English, as defined by the Common European Framework of Reference for Languages (CEFR).",
        "<b>What B2 means:</b>",
        "At B2 level, you can understand the main ideas of complex text on both concrete and abstract topics. You can interact with a degree of fluency and spontaneity with native speakers. You can also produce clear, detailed text on a wide range of subjects.",
        "<b>Vocabulary in this book:</b>",
        "Each story uses a mix of common B2 vocabulary and a small number of more challenging words. Every word that may be unfamiliar is included in the Vocabulary Box at the end of each story, with a definition written in simpler English.",
        "<b>Grammar in this book:</b>",
        "The stories use a range of tenses including the past simple, past continuous, past perfect, and present perfect. They also include conditionals, passive structures, and reported speech — all appropriate for the B2 level.",
        "<b>Suggested activities:</b>",
        "• Summarise each story in 5 sentences without looking at the text.",
        "• Choose one character from a story and write a short biography of them (100 words).",
        "• Write your own short horror story using at least 6 words from the Vocabulary Boxes.",
    ]
    for p in b2_paras:
        story.append(Paragraph(p, S["intro_body"]))
    story.append(PageBreak())

    # ── TEACHER NOTES ───────────────────────────────────────────────────────
    story.append(Spacer(1, 0.8*cm))
    story.append(Paragraph("Answer Guidance for Teachers", S["intro_head"]))
    story.append(HRFlowable(width="50%", thickness=1, color=RED, spaceAfter=10, spaceBefore=4, hAlign="CENTER"))
    teacher_notes = [
        "The comprehension questions in this book fall into three categories: <b>factual</b> (with clear answers in the text), <b>inferential</b> (requiring the student to read between the lines), and <b>discussion</b> (open-ended, opinion-based).",
        "<b>Factual questions</b> — Story One Q1: Maya chose the house because it was cheap, large, and close to the hospital. Story One Q2: The words read 'DO NOT OPEN. SHE IS STILL WAITING.' — suggesting the room contains something dangerous and patient.",
        "<b>Inferential questions</b> — Story Two: The reflection waving with its left hand (same side) rather than its right (as a true mirror reflection would) is the key clue that the reflection is not behaving as a mirror should. Story Four: The figure at the edge of the photograph (having been at the centre) implies it is aware and moving.",
        "<b>Discussion questions</b> — There are no wrong answers, provided students use evidence from the text. Encourage students to quote directly: 'The text says... which suggests...'",
        "<b>Writing tasks</b> — These are suitable for in-class writing exercises (20-25 minutes) or homework. Suggested length is 80-100 words. Assess for: accurate use of past tense, appropriate vocabulary, coherent narrative structure.",
        "<b>Vocabulary tasks</b> — Ask students to use each vocabulary word in an original sentence before reading the story, then review whether their meaning was correct after. This encourages active prediction and vocabulary ownership.",
    ]
    for p in teacher_notes:
        story.append(Paragraph(p, S["intro_body"]))

    story.append(Spacer(1, 1*cm))
    story.append(HRFlowable(width="60%", thickness=1, color=RED, spaceAfter=8, spaceBefore=8, hAlign="CENTER"))
    story.append(Paragraph("End of Shadows &amp; Whispers", S["cover_sub"]))
    story.append(Paragraph("Horror Handbook Series · ESL Reading Collection · B2 Level", S["cover_note"]))

    # ── BUILD ────────────────────────────────────────────────────────────────
    doc.build(story)
    print(f"\n✓ PDF written to: {OUT_PATH}")
    return OUT_PATH

if __name__ == "__main__":
    print("Downloading images...")
    for key in IMAGES:
        download_image(key)
    print("\nBuilding PDF...")
    result = build_pdf()
    import os
    size = os.path.getsize(result)
    print(f"File size: {size/1024:.1f} KB")
Running Command

cd /home/daytona/workspace/horror-handbook && python3 generate_horror_pdf.py 2>&…

Reading File
Editing File

~/horror-handbook/generate_horror_pdf.py

Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/horror-handbook/generate_horror_pdf.py'
Running Command

cd /home/daytona/workspace/horror-handbook && python3 -c "import py_compile; py_…

Running Command

cd /home/daytona/workspace/horror-handbook && python3 -c " import re with open('…

Reading File
Running Command

cd /home/daytona/workspace/horror-handbook && python3 -c " import ast, sys with …

Running Command

cd /home/daytona/workspace/horror-handbook && python3 - << 'PYEOF' # Fix all str…

Running Command

cd /home/daytona/workspace/horror-handbook && python3 - << 'PYEOF' import ast, r…

Running Command

cd /home/daytona/workspace/horror-handbook && python3 - << 'ENDFIX' with open('g…

Running Command

cd /home/daytona/workspace/horror-handbook && python3 generate_horror_pdf.py 2>&…

Running Command

python3 -c " from pypdf import PdfReader r = PdfReader('/home/daytona/workspace/…

Running Command

cd /home/daytona/workspace/horror-handbook && python3 - << 'ADDPAGES' # Check cu…

Running Command

cd /home/daytona/workspace/horror-handbook && python3 - << 'EXPAND' # We'll re-r…

Running Command

cd /home/daytona/workspace/horror-handbook && python3 generate_horror_pdf.py 2>&…

Model call limits exceeded: run limit (20/20)

Try again

Running Command

ls /home/daytona/workspace/horror-handbook/ && python3 -c "from pypdf import Pdf…

Writing File

~/horror-handbook/expand_pdf.py

#!/usr/bin/env python3
"""
Expand the horror anthology PDF to 45-50 pages by generating extra content pages
and merging them with the existing PDF.
"""
import os
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY, TA_RIGHT
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, PageBreak,
    HRFlowable, Image, Table, TableStyle
)
from pypdf import PdfWriter, PdfReader

PAGE_W, PAGE_H = A4
MARGIN = 2.2 * cm
EXTRA_PATH = "/home/daytona/workspace/horror-handbook/extra_pages.pdf"
FINAL_PATH = "/home/daytona/workspace/horror-handbook/Shadows_and_Whispers_Horror_Anthology.pdf"
IMG_DIR    = "/home/daytona/workspace/horror-handbook/imgs"

RED   = colors.HexColor("#8b0000")
DARK  = colors.HexColor("#1a1a2e")
GREY  = colors.HexColor("#555555")
LGREY = colors.HexColor("#888888")

# ── Styles ────────────────────────────────────────────────────────────────────
def S():
    base = getSampleStyleSheet()
    return {
        "h1": ParagraphStyle("h1", parent=base["Heading1"],
            fontSize=20, leading=26, textColor=DARK, fontName="Helvetica-Bold",
            alignment=TA_CENTER, spaceBefore=8, spaceAfter=8),
        "h2": ParagraphStyle("h2", parent=base["Heading2"],
            fontSize=14, leading=20, textColor=RED, fontName="Helvetica-Bold",
            spaceBefore=10, spaceAfter=4),
        "body": ParagraphStyle("body", parent=base["Normal"],
            fontSize=11.5, leading=19, textColor=DARK, fontName="Helvetica",
            alignment=TA_JUSTIFY, firstLineIndent=18, spaceAfter=10),
        "body0": ParagraphStyle("body0", parent=base["Normal"],
            fontSize=11.5, leading=19, textColor=DARK, fontName="Helvetica",
            alignment=TA_JUSTIFY, firstLineIndent=0, spaceAfter=10),
        "meta": ParagraphStyle("meta", parent=base["Normal"],
            fontSize=10, leading=13, textColor=LGREY, fontName="Helvetica-Oblique",
            alignment=TA_CENTER, spaceAfter=14),
        "chnum": ParagraphStyle("chnum", parent=base["Normal"],
            fontSize=11, leading=14, textColor=RED, fontName="Helvetica-Bold",
            alignment=TA_CENTER, spaceBefore=10),
        "chtitle": ParagraphStyle("chtitle", parent=base["Heading1"],
            fontSize=22, leading=28, textColor=DARK, fontName="Helvetica-Bold",
            alignment=TA_CENTER, spaceBefore=6, spaceAfter=4),
        "vocab_h": ParagraphStyle("vocab_h", parent=base["Normal"],
            fontSize=11, leading=14, textColor=RED, fontName="Helvetica-Bold",
            spaceBefore=12, spaceAfter=4),
        "vocab": ParagraphStyle("vocab", parent=base["Normal"],
            fontSize=10.5, leading=15, textColor=DARK, fontName="Helvetica",
            leftIndent=14, spaceAfter=4),
        "q_h": ParagraphStyle("q_h", parent=base["Normal"],
            fontSize=11, leading=14, textColor=RED, fontName="Helvetica-Bold",
            spaceBefore=10, spaceAfter=4),
        "q": ParagraphStyle("q", parent=base["Normal"],
            fontSize=10.5, leading=15, textColor=DARK, fontName="Helvetica",
            leftIndent=14, spaceAfter=6),
        "caption": ParagraphStyle("caption", parent=base["Normal"],
            fontSize=9, leading=12, textColor=LGREY, fontName="Helvetica-Oblique",
            alignment=TA_CENTER, spaceAfter=8),
        "note": ParagraphStyle("note", parent=base["Normal"],
            fontSize=11, leading=17, textColor=DARK, fontName="Helvetica",
            alignment=TA_CENTER, spaceAfter=6),
        "bullet": ParagraphStyle("bullet", parent=base["Normal"],
            fontSize=11, leading=17, textColor=DARK, fontName="Helvetica",
            leftIndent=18, spaceAfter=5),
    }

def hr(story, width="40%", thick=1):
    story.append(HRFlowable(width=width, thickness=thick, color=RED,
                             spaceAfter=8, spaceBefore=4, hAlign="CENTER"))

def img(key, w_cm=13.5, max_h_cm=7.0):
    path = os.path.join(IMG_DIR, f"{key}.jpg")
    if os.path.exists(path):
        try:
            im = Image(path)
            w = w_cm * cm
            aspect = im.imageWidth / im.imageHeight
            h = min(w / aspect, max_h_cm * cm)
            w = h * aspect
            im.drawWidth = w
            im.drawHeight = h
            im.hAlign = "CENTER"
            return im
        except Exception as e:
            print(f"  Image error {key}: {e}")
    return None

# ── Build extra pages ─────────────────────────────────────────────────────────
def build_extra():
    doc = SimpleDocTemplate(
        EXTRA_PATH, pagesize=A4,
        leftMargin=MARGIN, rightMargin=MARGIN,
        topMargin=MARGIN, bottomMargin=MARGIN,
    )
    st = S()
    story = []

    # ══════════════════════════════════════════════════════════════════════════
    # STORY 9: "The Waiting Room"
    # ══════════════════════════════════════════════════════════════════════════
    story.append(Spacer(1, 0.5*cm))
    story.append(Paragraph("STORY NINE", st["chnum"]))
    story.append(Paragraph("The Waiting Room", st["chtitle"]))
    story.append(Paragraph("B2 | ~800 words | Theme: Time / The afterlife", st["meta"]))
    hr(story, "40%")

    im = img("cemetery", 13.5, 7.0)
    if im:
        story.append(im)
    story.append(Paragraph("The waiting room had no windows, no clocks, and no exits.", st["caption"]))
    story.append(Spacer(1, 0.3*cm))

    s9_text = [
        '<font color="#8b0000"><b>E</b></font>veryone in the waiting room was pretending to read a magazine. That was the first thing Anna noticed. The magazines were old — the covers faded, the pages soft with handling — and yet everyone held them open, eyes fixed on pages they were clearly not reading. No one spoke. No one looked up.',
        "Anna had been in many waiting rooms in her life. She had waited in hospital corridors for news about her mother, in grey government offices for forms to be processed, in airport terminals for delayed flights. She knew the particular quality of waiting — the way time seems to stretch, the way strangers pretend not to look at each other.",
        "This waiting room felt different. It was large — larger than it should have been, given the narrow door she had come through — and it was very quiet. Not the suppressed, self-conscious quiet of a room full of people trying not to make noise. A deeper quiet. A settled one.",
        "She found an empty seat between an elderly man in a brown coat and a young woman in a nurse's uniform. Both were holding magazines. Neither acknowledged her arrival.",
        "On the wall opposite was a single door marked OFFICE. Beside it, a sign: <i>All visitors will be seen in order. Please be patient.</i>",
        "Anna tried to remember how she had arrived here. She remembered leaving work, the cold air outside, the short walk to the bus stop. She remembered stepping off the kerb — and then nothing. And then this room.",
        "The thought arrived slowly, the way cold water rises: <i>I am not here by accident.</i>",
        "She looked around more carefully. The people in the waiting room were of all ages and types. An old woman in a floral dress. A teenager in a school uniform. A middle-aged man in a suit, his briefcase on his lap, his eyes fixed on an article he was not reading. A child of about six, sitting very still, holding a soft toy.",
        "None of them had any visible injuries. None of them appeared to be in distress. They were all just waiting. Patiently, quietly, without complaint.",
        "Anna stood up. She walked to the door marked OFFICE and knocked.",
        '"Yes?" A voice from inside. Polite. Efficient.',
        "She opened the door. Behind a wooden desk sat a man in a pale suit. He had kind eyes and a great deal of paperwork. He looked up when she entered and gave her a small, professional smile.",
        '"Ah,' he said. 'Anna Marsh. Please, sit down.'",
        '"I want to know where I am," Anna said. She remained standing.',
        "The man looked at her with an expression of gentle patience — the expression of someone who had answered this question many thousands of times.",
        '"I think,' he said carefully, 'that you already know where you are.'",
        "Anna sat down.",
        "She said: 'Am I dead?'",
        "The man picked up a pen. 'There are some forms,' he said. 'It will not take long. And then we can discuss what happens next.'",
        '"What does happen next?"',
        "He smiled again. 'That,' he said, 'depends very much on the answers to the forms.'",
        "He placed a sheet of paper in front of her. At the top, in small, neat print, it said: <i>Please answer honestly. There are no wrong answers. There is only the truth.</i>",
        "Anna picked up the pen.",
        "In the waiting room outside, someone turned the page of a magazine.",
        "The clock on the wall — the only clock Anna had not noticed, set high above the door — ticked once and did not tick again.",
        "Anna began to write.",
    ]
    for i, para in enumerate(s9_text):
        style = st["body0"] if i == 0 else st["body"]
        story.append(Paragraph(para, style))

    story.append(Spacer(1, 0.5*cm))
    hr(story, "30%", 0.8)

    story.append(Paragraph("Vocabulary", st["vocab_h"]))
    s9_vocab = [
        ("suppress (v.)", "to prevent something from being expressed or noticed"),
        ("acknowledge (v.)", "to show that you have noticed someone or something"),
        ("distress (n.)", "a state of great pain, worry, or suffering"),
        ("kerb (n.)", "the edge of the pavement beside the road; AmE: curb"),
        ("efficient (adj.)", "working quickly and without wasting time or energy"),
        ("briefcase (n.)", "a flat bag used for carrying documents, usually to work"),
    ]
    for word, defn in s9_vocab:
        story.append(Paragraph(f"<b>{word}</b> - {defn}", st["vocab"]))

    story.append(Spacer(1, 0.3*cm))
    story.append(Paragraph("Comprehension &amp; Discussion", st["q_h"]))
    s9_qs = [
        "1. What did Anna notice about the people in the waiting room? What was strange about their behaviour?",
        "2. What clue in the story suggests that Anna is dead before she asks the man directly?",
        "3. What does the sign on the OFFICE door say? How is this darkly humorous?",
        "4. The clock 'ticked once and did not tick again.' What does this symbolise?",
        "5. Discussion: The story deliberately does not explain what 'happens next.' Why might the author have made this choice? What effect does it create?",
        "6. Writing task: Write the first question on Anna's form and her answer (60-80 words).",
    ]
    for q in s9_qs:
        story.append(Paragraph(q, st["q"]))
    story.append(PageBreak())

    # ══════════════════════════════════════════════════════════════════════════
    # STORY 10: "The Neighbour"
    # ══════════════════════════════════════════════════════════════════════════
    story.append(Spacer(1, 0.5*cm))
    story.append(Paragraph("STORY TEN", st["chnum"]))
    story.append(Paragraph("The Neighbour", st["chtitle"]))
    story.append(Paragraph("B2 | ~750 words | Theme: Paranoia / The ordinary made strange", st["meta"]))
    hr(story, "40%")

    im2 = img("window", 13.5, 7.0)
    if im2:
        story.append(im2)
    story.append(Paragraph("She had never once seen her neighbour during the day.", st["caption"]))
    story.append(Spacer(1, 0.3*cm))

    s10_text = [
        '<font color="#8b0000"><b>T</b></font>he neighbour in Flat 4B was perfectly pleasant. He held the door open when he saw you coming. He said good morning. He signed for parcels when you were out. He was quiet, which in a block of flats was appreciated above almost everything else. His name, according to the letter box, was R. Caldwell.',
        "Emma had lived in 4A for two years. She and Mr Caldwell had exchanged perhaps fifty words in that time. This was normal. This was the standard relationship between neighbours in a city block.",
        "It was her friend Priya who first pointed out the thing about the curtains.",
        '"Have you noticed," Priya said, on a Sunday afternoon, looking out of Emma\'s kitchen window at the building opposite, "that 4B\'s curtains are always closed during the day? And always open at night?"',
        "Emma had not noticed. But once Priya said it, she could not stop noticing. Every morning when she left for work: the curtains in 4B were drawn. Every evening when she returned: they were open. Light visible inside.",
        "She told herself it was nothing. Some people worked night shifts. Some people preferred darkness. It meant nothing.",
        "Then she started paying attention to the communal areas.",
        "The post for 4B always arrived — she could see it in the boxes — but it was never collected during the day. It was always gone by morning. The recycling bin outside 4B's door appeared and disappeared between midnight and six AM. She knew because she checked.",
        "She felt slightly ashamed of herself for checking. But she checked again the next night.",
        "She knocked on his door on a Thursday evening at eight o'clock. He answered quickly, as though he had been nearby.",
        '"Hello,' she said. 'I just wanted to ask - do you have any milk I could borrow? I\'ve run out.'",
        "Mr Caldwell smiled. He had a pleasant, forgettable face. 'Of course,' he said, and disappeared inside. He was back in a moment with a small bottle. 'Keep it,' he said.",
        "'Thank you,' Emma said. She looked past him, trying to see into the flat. She had a brief impression of a large, dimly lit space, very neat, with almost no furniture that she could see.",
        "'You're welcome,' he said. And closed the door.",
        "He was warm milk. Not cold. Emma stood in the corridor looking at the bottle in her hand. The milk was warm. Not room temperature - warm, as if it had been heated.",
        "She went back to her flat. She poured the milk down the sink without tasting it. She felt very silly.",
        "That night she lay awake listening. Around two AM she heard his door open and close. Footsteps in the corridor. Then nothing.",
        "In the morning she checked the post boxes. His was empty. She had not heard him return.",
        "She told Priya about it over the phone.",
        '"So he\'s probably just weird," Priya said. "Lots of people are weird. It doesn\'t mean anything."',
        '"I know," Emma said.',
        '"Stop watching him."',
        '"I know."',
        "But she did not stop. And a week later, when she passed him in the lobby in the evening and said hello, she noticed something she had not before.",
        "He had no shadow.",
        "The lobby had bright overhead lights, and everyone who walked through it cast a clear dark shape on the white floor. Emma's shadow moved beside her. The delivery driver who came through the door behind her had a shadow.",
        "R. Caldwell walked through the light and left nothing behind him.",
        "Emma watched him go up the stairs. She stood in the lobby for a long time.",
        "The next morning, she put her flat on the market. She did not give a reason.",
        "She still does not know what he was. She only knows that whatever it was, it was polite and quiet and helpful.",
        "And that, she has decided, is somehow the most frightening thing of all.",
    ]
    for i, para in enumerate(s10_text):
        style = st["body0"] if i == 0 else st["body"]
        story.append(Paragraph(para, style))

    story.append(Spacer(1, 0.5*cm))
    hr(story, "30%", 0.8)

    story.append(Paragraph("Vocabulary", st["vocab_h"]))
    s10_vocab = [
        ("communal (adj.)", "shared by all members of a group or building"),
        ("recycling bin (n.)", "a container used to collect materials for recycling"),
        ("impression (n.)", "a feeling or idea about something, especially a quick one"),
        ("dim (adj.)", "not bright; having little light"),
        ("lobby (n.)", "the entrance hall of a building, especially a block of flats"),
        ("put on the market (v. phrase)", "to make a property available for sale"),
    ]
    for word, defn in s10_vocab:
        story.append(Paragraph(f"<b>{word}</b> - {defn}", st["vocab"]))

    story.append(Spacer(1, 0.3*cm))
    story.append(Paragraph("Comprehension &amp; Discussion", st["q_h"]))
    s10_qs = [
        "1. What were the three strange things Emma noticed about Mr Caldwell's routine?",
        "2. Why did Emma feel 'slightly ashamed' of herself? What does this tell us about her character?",
        "3. What was strange about the milk? Why might this detail be significant?",
        "4. What did Emma notice about Mr Caldwell in the lobby? Why is this the key horror moment?",
        "5. The last line says his politeness was 'the most frightening thing of all.' Do you agree? Why might ordinary behaviour from something inhuman be more frightening than obvious threats?",
    ]
    for q in s10_qs:
        story.append(Paragraph(q, st["q"]))
    story.append(PageBreak())

    # ══════════════════════════════════════════════════════════════════════════
    # HORROR TECHNIQUES GUIDE (extended)
    # ══════════════════════════════════════════════════════════════════════════
    story.append(Spacer(1, 0.6*cm))
    story.append(Paragraph("Horror Writing: Advanced Techniques", st["h1"]))
    hr(story, "50%", 1)

    tech_sections = [
        ("The Rule of Threes",
         "Horror writers frequently use groups of three to build tension. Three knocks on the door. Three strange events before the climax. Three details that seem unrelated until the end. The repetition creates a pattern, and patterns create expectation - and it is the violation of that expectation that generates fear. Look at <i>The Last Room</i>: the knocking always comes three times. When it stops, the reader is almost more afraid than when it continues."),
        ("Restraint and Implication",
         "The best horror rarely shows the monster directly. What is implied is always more frightening than what is described. In <i>The Shape in the Photograph</i>, the figure is never explained. Its origin, its nature, its intentions - all unknown. The story is more frightening because of what it does not say. Exercise: Take a horror scene you have written and remove one key piece of information. Does it become more or less effective?"),
        ("The Reliable Unreliable Narrator",
         "Several stories in this collection use narrators who are rational and observant - nurses, teachers, scientists. This is deliberate. When a credible person experiences something they cannot explain, the reader is more likely to believe it. If your narrator is already predisposed to fear or imagination, the reader can dismiss the horror. If your narrator is the last person who would invent a story, the horror lands harder."),
        ("Everyday Objects Made Strange",
         "A mirror, a phone, a photograph, a milk bottle. The most effective horror often uses the most ordinary objects. The reader has a mirror at home. They have received text messages from unknown numbers. By using familiar objects, horror writers make the reader feel that the same thing could happen to them. This is called the 'uncanny' - the familiar made strange. Exercise: List five objects in the room you are in now. How could each one become the centre of a horror story?"),
        ("Pacing: Slow and Fast",
         "Notice how the stories in this book slow down at the most frightening moments. Short sentences. Single words as paragraphs. Line breaks where there would normally be continuation. This slowing-down forces the reader to experience the moment at the same speed as the character. Then, when the horror arrives, it is often described very briefly - almost passing. The contrast between slow build-up and fast payoff is one of the most reliable tools in horror writing."),
        ("Endings That Do Not Resolve",
         "Every story in this collection ends with something unresolved. Maya leaves the house but we never learn what was in Room 13. Clara stops collecting photographs but the figure has already been in her garden. These open endings are deliberate. Horror that is explained loses its power. If you name the monster, it becomes smaller. If you leave the door ajar, the reader's imagination does the rest - and the reader's own imagination is always more frightening than anything you can write."),
    ]
    for title, para in tech_sections:
        story.append(Paragraph(title, st["h2"]))
        story.append(Paragraph(para, st["body0"]))
        story.append(Spacer(1, 0.2*cm))
    story.append(PageBreak())

    # ══════════════════════════════════════════════════════════════════════════
    # GRAMMAR IN CONTEXT
    # ══════════════════════════════════════════════════════════════════════════
    story.append(Spacer(1, 0.6*cm))
    story.append(Paragraph("Grammar in Context", st["h1"]))
    hr(story, "50%", 1)
    story.append(Paragraph(
        "Horror stories make excellent grammar teaching material because they use a wide range of tenses and structures to create suspense. This section highlights the key grammar points used in this anthology.",
        st["body0"]))

    grammar_sections = [
        ("Past Perfect: The Backstory of Dread",
         'The past perfect (had + past participle) is essential in horror because it tells us what happened BEFORE the main events. This creates layers of history that deepen the dread. Example from <i>The Last Room</i>: "The last room had never been rented. Not once in forty years." The past perfect tells us the strange history exists long before Maya arrives. Task: Find three more examples of the past perfect in the stories and explain what each one reveals about the backstory.'),
        ("Passive Voice: Removing the Agent",
         'The passive voice (be + past participle) is frequently used in horror to create mystery about who or what is responsible for an action. Instead of "Something had moved the figure," we write "The figure had been moved." The passive hides the cause, and hidden causes are frightening. Example: "The drawings had been left there when we moved in." Task: Find four passive constructions in the stories. For each one, ask: why did the author choose not to name the agent?'),
        ("Modal Verbs: Uncertainty and Possibility",
         'Modal verbs (might, could, would, must, can\'t) are the grammar of uncertainty - and uncertainty is the foundation of horror. Example from <i>The Clearing</i>: "It doesn\'t come out of the clearing. It doesn\'t do anything. It just stands and watches. As long as you don\'t go back, you\'ll be fine." The certainty of the aunt\'s description makes it more frightening than if she had said "it might watch you." Task: Rewrite the aunt\'s speech using only modal verbs (it might, it could) and compare the effect. Which version is more frightening?'),
        ("Sentence Length as Tension",
         'Short sentences create tension. They slow the reader down. They make each word important. Long sentences, by contrast, can rush the reader through information, building pace and momentum towards a climax, using the length of the sentence itself to mirror the gathering speed of events. Horror writers switch between these deliberately. Task: Find one long sentence and one short sentence (or sequence of short sentences) in the anthology. Explain how each creates a different effect.'),
        ("Reported Speech: Distance and Doubt",
         "When a character repeats what someone else told them, reported speech creates a useful layer of distance. It allows the horror to be reported at second hand, which can be both reassuring (it didn't happen to the narrator) and unsettling (we can't verify the original source). Example: \"She said the house was old. That things had been in it for a very long time.\" Task: Find two examples of reported speech in the anthology and discuss how they affect the reader's trust in the information."),
    ]
    for title, para in grammar_sections:
        story.append(Paragraph(title, st["h2"]))
        story.append(Paragraph(para, st["body0"]))
        story.append(Spacer(1, 0.2*cm))
    story.append(PageBreak())

    # ══════════════════════════════════════════════════════════════════════════
    # CULTURAL NOTES ON HORROR TRADITIONS
    # ══════════════════════════════════════════════════════════════════════════
    story.append(Spacer(1, 0.6*cm))
    story.append(Paragraph("Horror Traditions Around the World", st["h1"]))
    hr(story, "50%", 1)
    story.append(Paragraph(
        "Horror is a universal human experience. Every culture has its own traditions of frightening stories, and many of the themes in this anthology echo folklore from around the world.",
        st["body0"]))

    cultural = [
        ("The Doppelganger (Germany and Northern Europe)",
         'The doppelganger - a ghostly double of a living person - appears in <i>The Mirror at the End of the Hall.</i> In German and Scandinavian folklore, seeing your own double was a fatal omen. The word comes from German: "Doppel" (double) + "Ganger" (walker). Similar figures appear in Scottish, Irish, and Finnish tradition. The Irish equivalent is the "fetch" - used directly in Story Two - which is said to appear shortly before a person dies.'),
        ("The Vanishing Hitchhiker (Worldwide)",
         'The story of a ghost who accepts a lift in a car and disappears before the journey ends - as in <i>Fog on the Road</i> - is one of the most widespread ghost story types in the world. It has been recorded in cultures across Europe, America, Asia, and Africa. Folklorists call it a "vanishing hitchhiker" narrative. The details change by culture: sometimes the ghost leaves a physical object behind; sometimes it reveals the location of its grave; sometimes it simply disappears.'),
        ("The Watching Figure (Universal)",
         'The motif of a figure that watches from a distance but does not approach - as in <i>The Clearing</i> and <i>The Shape in the Photograph</i> - appears in folklore and horror across all cultures. It reflects a deep evolutionary fear: the sensation of being observed. In Japanese horror (J-horror), this figure often appears at the edge of photographs. In British folk tradition, it appears as a standing stone, a tree, or a dark shape at the edge of a field.'),
        ("The Haunted House (British and American Tradition)",
         'The haunted house is perhaps the most familiar horror setting in English-language literature. It originates in Gothic fiction of the 18th and 19th centuries, developed through writers like Horace Walpole, Ann Radcliffe, and Shirley Jackson. The essential elements - a building with a dark history, rooms that should not be entered, the gradual revelation of what happened there - are all present in <i>The Last Room.</i> The haunted house is effective because a house is a symbol of safety and home. When that symbol is corrupted, the reader has nowhere to feel safe.'),
        ("Folklore and Rules (Universal)",
         'Many cultures have elaborate sets of rules for interacting with the supernatural - knock before entering, do not say certain names, leave food out on certain nights, never cut down a particular tree. <i>Knock Twice</i> uses this tradition directly. These rule-based folk beliefs serve a psychological function: they give humans a sense of control over forces they do not understand. The horror of the story comes precisely from breaking the rule - and discovering that the rules were not superstition after all.'),
    ]
    for title, para in cultural:
        story.append(Paragraph(title, st["h2"]))
        story.append(Paragraph(para, st["body0"]))
        story.append(Spacer(1, 0.2*cm))
    story.append(PageBreak())

    # ══════════════════════════════════════════════════════════════════════════
    # READING RECORD + CERTIFICATE
    # ══════════════════════════════════════════════════════════════════════════
    story.append(Spacer(1, 0.5*cm))
    story.append(Paragraph("My Reading Record", st["h1"]))
    hr(story, "50%", 1)
    story.append(Paragraph("Track your progress through the anthology. Rate each story and note your favourite vocabulary words.", st["body0"]))
    story.append(Spacer(1, 0.3*cm))

    table_data = [
        [Paragraph("<b>Story</b>", st["vocab"]),
         Paragraph("<b>Completed</b>", st["vocab"]),
         Paragraph("<b>Rating (1-5)</b>", st["vocab"]),
         Paragraph("<b>Favourite word</b>", st["vocab"])],
    ]
    story_titles = [
        "1. The Last Room",
        "2. The Mirror at the End of the Hall",
        "3. Signal Lost",
        "4. The Clearing",
        "5. Fog on the Road",
        "6. The House That Came Back",
        "7. Knock Twice",
        "8. The Shape in the Photograph",
        "9. The Waiting Room",
        "10. The Neighbour",
    ]
    for t in story_titles:
        table_data.append([
            Paragraph(t, st["vocab"]),
            Paragraph("", st["vocab"]),
            Paragraph("", st["vocab"]),
            Paragraph("", st["vocab"]),
        ])
    tbl = Table(table_data, colWidths=[7.0*cm, 2.8*cm, 2.8*cm, 3.8*cm])
    tbl.setStyle(TableStyle([
        ("BACKGROUND", (0,0), (-1,0), colors.HexColor("#1a1a2e")),
        ("TEXTCOLOR",  (0,0), (-1,0), colors.white),
        ("FONTNAME",   (0,0), (-1,0), "Helvetica-Bold"),
        ("FONTSIZE",   (0,0), (-1,-1), 10),
        ("ROWBACKGROUNDS", (0,1), (-1,-1), [colors.HexColor("#fdf6e3"), colors.white]),
        ("GRID",       (0,0), (-1,-1), 0.5, colors.HexColor("#cccccc")),
        ("VALIGN",     (0,0), (-1,-1), "MIDDLE"),
        ("TOPPADDING", (0,0), (-1,-1), 6),
        ("BOTTOMPADDING", (0,0), (-1,-1), 6),
        ("LEFTPADDING", (0,0), (-1,-1), 6),
    ]))
    story.append(tbl)
    story.append(Spacer(1, 0.8*cm))

    story.append(Paragraph("My three favourite stories were:", st["body0"]))
    for _ in range(3):
        story.append(HRFlowable(width="80%", thickness=0.5, color=GREY,
                                spaceAfter=16, spaceBefore=4, hAlign="LEFT"))
    story.append(Paragraph("A new word I want to remember:", st["body0"]))
    story.append(HRFlowable(width="80%", thickness=0.5, color=GREY,
                            spaceAfter=16, spaceBefore=4, hAlign="LEFT"))
    story.append(PageBreak())

    # ── CERTIFICATE PAGE ─────────────────────────────────────────────────────
    story.append(Spacer(1, 1.5*cm))
    story.append(HRFlowable(width="85%", thickness=3, color=RED,
                            spaceAfter=18, spaceBefore=4, hAlign="CENTER"))
    story.append(Paragraph("CERTIFICATE OF COMPLETION", st["chtitle"]))
    story.append(Spacer(1, 0.4*cm))
    story.append(Paragraph("Horror Handbook Series &nbsp; | &nbsp; ESL Reading Collection", st["meta"]))
    story.append(HRFlowable(width="85%", thickness=1.5, color=RED,
                            spaceAfter=24, spaceBefore=8, hAlign="CENTER"))
    story.append(Paragraph("This is to certify that", st["note"]))
    story.append(Spacer(1, 0.5*cm))
    story.append(HRFlowable(width="65%", thickness=0.8, color=GREY,
                            spaceAfter=6, spaceBefore=6, hAlign="CENTER"))
    story.append(Paragraph("has successfully completed all ten stories in", st["note"]))
    story.append(Spacer(1, 0.4*cm))
    story.append(Paragraph("<b>Shadows &amp; Whispers: A Horror Anthology for ESL Learners</b>", st["note"]))
    story.append(Paragraph("B2 Upper Intermediate Level", st["meta"]))
    story.append(Spacer(1, 0.5*cm))
    cert_items = [
        "10 original horror stories read",
        "48 vocabulary items studied",
        "Approx. 7,500 words of reading practice",
        "Comprehension &amp; discussion tasks completed",
    ]
    for item in cert_items:
        story.append(Paragraph(f"* {item}", ParagraphStyle("ci", fontName="Helvetica",
            fontSize=11, leading=17, textColor=DARK, alignment=TA_CENTER, spaceAfter=4)))
    story.append(Spacer(1, 1.2*cm))
    col_data = [[
        Paragraph("_______________________", st["note"]),
        Paragraph("_______________________", st["note"]),
    ],[
        Paragraph("Teacher / Tutor", st["meta"]),
        Paragraph("Date", st["meta"]),
    ]]
    cert_tbl = Table(col_data, colWidths=[8*cm, 8*cm])
    cert_tbl.setStyle(TableStyle([
        ("ALIGN", (0,0), (-1,-1), "CENTER"),
        ("VALIGN", (0,0), (-1,-1), "MIDDLE"),
        ("TOPPADDING", (0,0), (-1,-1), 4),
        ("BOTTOMPADDING", (0,0), (-1,-1), 4),
    ]))
    story.append(cert_tbl)
    story.append(Spacer(1, 1.2*cm))
    story.append(HRFlowable(width="85%", thickness=2, color=RED,
                            spaceAfter=8, spaceBefore=8, hAlign="CENTER"))
    story.append(Paragraph("www.horrorhandbook.esl &nbsp; | &nbsp; &copy; Horror Handbook Series", st["meta"]))

    doc.build(story)
    print(f"Extra pages PDF written: {EXTRA_PATH}")
    from pypdf import PdfReader as PR
    r = PR(EXTRA_PATH)
    print(f"Extra pages count: {len(r.pages)}")

# ── Merge PDFs ────────────────────────────────────────────────────────────────
def merge():
    base_r = PdfReader(FINAL_PATH)
    extra_r = PdfReader(EXTRA_PATH)
    writer = PdfWriter()
    for page in base_r.pages:
        writer.add_page(page)
    for page in extra_r.pages:
        writer.add_page(page)
    # Update metadata
    writer.add_metadata({
        "/Title": "Shadows & Whispers: A Horror Anthology for ESL Learners",
        "/Author": "Horror Handbook Series",
        "/Subject": "10 original B2-level horror stories for ESL students with vocabulary notes",
    })
    with open(FINAL_PATH, "wb") as f:
        writer.write(f)
    from pypdf import PdfReader as PR2
    final_r = PR2(FINAL_PATH)
    print(f"Final merged PDF: {len(final_r.pages)} pages")
    import os
    print(f"File size: {os.path.getsize(FINAL_PATH)/1024:.0f} KB")

if __name__ == "__main__":
    build_extra()
    merge()
    print("Done!")
Running Command

cd /home/daytona/workspace/horror-handbook && python3 expand_pdf.py 2>&1

Running Command

cd /home/daytona/workspace/horror-handbook && python3 - << 'FIXER' import ast w…

Running Command

cd /home/daytona/workspace/horror-handbook && python3 - << 'FIXER2' import ast …

Running Command

cd /home/daytona/workspace/horror-handbook && python3 -c " with open('expand_pdf…

Running Command

cd /home/daytona/workspace/horror-handbook && python3 - << 'FIXALL' import ast, …

Running Command

cd /home/daytona/workspace/horror-handbook && python3 -c " import ast with open(…

Running Command

cd /home/daytona/workspace/horror-handbook && python3 -c " with open('expand_pdf…

Running Command

cd /home/daytona/workspace/horror-handbook && python3 - << 'FIX3' import ast wi…

Running Command

cd /home/daytona/workspace/horror-handbook && python3 -c " with open('expand_pdf…

Running Command

cd /home/daytona/workspace/horror-handbook && python3 - << 'FIX4' import ast wi…

Running Command

cd /home/daytona/workspace/horror-handbook && python3 expand_pdf.py 2>&1

Running Command

python3 -c " from pypdf import PdfReader r = PdfReader('/home/daytona/workspace/…

Your horror anthology is ready! Here is your download link:

Shadows and Whispers Horror Anthology

PDF Document · PDF


What's inside (49 pages, ~1 MB)

10 Original Horror Stories at B2 Level

#StoryTheme
1The Last RoomHaunted house
2The Mirror at the End of the HallDoppelganger / Identity
3Signal LostTechnology / Isolation
4The ClearingNature / The unknown
5Fog on the RoadRoad / Ghost encounter
6The House That Came BackMemory / Alternate reality
7Knock TwiceFolklore / Ritual
8The Shape in the PhotographPhotography / The past
9The Waiting RoomTime / The afterlife
10The NeighbourParanoia / The ordinary made strange

Features of each story

  • ~650-800 words per story - readable in one sitting
  • Atmospheric photo at the start of each story (sourced and embedded)
  • Vocabulary Box with 6 key B2 terms defined in plain English
  • 5-6 comprehension questions ranging from factual to discussion/opinion
  • Writing or speaking extension tasks

Bonus sections

  • Horror Writing Workshop - 6 craft techniques with exercises
  • Grammar in Context - past perfect, passive voice, modal verbs, sentence pacing
  • Horror Traditions Around the World - cultural notes on folklore origins
  • My Reading Record - fillable table to track progress
  • Certificate of Completion - printable and signable
This is a shared conversation. Sign in to Orris to start your own chat.