attachment
attachment
attachment
attachment
attachment
attachment

Please create a handbook pdf for my today's new B1 or B1+ ESL intermediate student from this pdf I want to check his level of English his proficiency and I also want to teach him something very easy and interesting in my today's first trial lesson according to his proficiency as intermediate ESL ,so that he will be happy and join my English lessons permanently. My lesson duration is standard for 1 hour. Please mention Russian translation of each and every sentence under every English sentences and do write the topic/ title name also in English first and then under that Russian translation. Please remember that handbook pdf should contain any evidence that it will look like prepared by any artificial intelligence, create in a smart way. Also most important is that the handbook pdf should not be boring for my ESL student and it must be created in a presentation PPT look so that it will be very interesting to look and colorful. Also I am adding photos for the intensive speaking practice of exercise for describing the photos and later comparing both of photos also.

analyse_media
analyse_media
analyse_media
Reading File
Running Command

ls -la /tmp/workspace/ 2>/dev/null || mkdir -p /tmp/workspace && echo "workspace ready"

Running Command

mkdir -p /tmp/workspace/esl-handbook && cd /tmp/workspace/esl-handbook && python3 -c " # Download images import urllib.request import os imgs = { 'eating.jpg': 'https://d361swluvexzqs.cloudfront.net/chats-attachments/1000288414-1786782026256-b33kry.jpg', 'exercise.jpg': 'https://d361swluvexzqs.cloudfront.net/chats-attachments/1000288415-1786782027798-niem7q.jpg', 'transport.jpg': 'https://d361swluvexzqs.cloudfront.net/chats-attachments/1000288420-1786782028596-cuqz9k.jpg', } for name, url in imgs.items(): try: req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) with urllib.request.urlopen(req, timeout=30) as r, open(name, 'wb') as f: f.write(r.read()) print(f'Downloaded {name}: {os.path.getsize(name)} bytes') except Exception as e: print(f'Failed {name}: {e}') "

Writing File

~/esl-handbook/build_handbook.py

#!/usr/bin/env python3
"""
ESL B1/B1+ Intermediate Handbook – Trial Lesson
Bilingual English + Russian, PPT-style colorful PDF
"""

from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
from reportlab.lib.styles import ParagraphStyle
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
    HRFlowable, KeepTogether, Image as RLImage, PageBreak
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus.flowables import Flowable
from PIL import Image as PILImage
import os, textwrap

# ─── Colour Palette ──────────────────────────────────────────────────────────
C_NAVY   = colors.HexColor("#1B2A4A")
C_TEAL   = colors.HexColor("#0D8B8B")
C_ORANGE = colors.HexColor("#FF6B35")
C_GOLD   = colors.HexColor("#F5A623")
C_LIME   = colors.HexColor("#4CAF50")
C_PURPLE = colors.HexColor("#7B52AB")
C_PINK   = colors.HexColor("#E91E8C")
C_LBLUE  = colors.HexColor("#E3F4FB")
C_LYELL  = colors.HexColor("#FFF8E1")
C_LGRN   = colors.HexColor("#E8F5E9")
C_LPURP  = colors.HexColor("#F3E5F5")
C_LORANG = colors.HexColor("#FFF3E0")
C_WHITE  = colors.white
C_LGREY  = colors.HexColor("#F5F5F5")
C_DGREY  = colors.HexColor("#424242")

W, H = A4   # 595 x 842

# ─── Font Setup ──────────────────────────────────────────────────────────────
FONT_DIR = "/usr/share/fonts/truetype"
fonts_loaded = {}

def try_load(name, path):
    if os.path.exists(path):
        pdfmetrics.registerFont(TTFont(name, path))
        fonts_loaded[name] = True
        return True
    return False

# Try DejaVu (has Cyrillic)
try_load("DejaVu",      "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")
try_load("DejaVuBold",  "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf")
try_load("DejaVuItal",  "/usr/share/fonts/truetype/dejavu/DejaVuSans-Oblique.ttf")

BASE  = "DejaVu"     if "DejaVu"     in fonts_loaded else "Helvetica"
BOLD  = "DejaVuBold" if "DejaVuBold" in fonts_loaded else "Helvetica-Bold"
ITAL  = "DejaVuItal" if "DejaVuItal" in fonts_loaded else "Helvetica-Oblique"

print(f"Fonts: base={BASE}, bold={BOLD}, ital={ITAL}")

# ─── Paragraph Styles ────────────────────────────────────────────────────────
def S(name, **kw):
    return ParagraphStyle(name, **kw)

sTitle = S("sTitle", fontName=BOLD, fontSize=28, textColor=C_WHITE,
           alignment=TA_CENTER, spaceAfter=4, leading=34)
sSubtitle = S("sSubtitle", fontName=BASE, fontSize=14, textColor=C_GOLD,
              alignment=TA_CENTER, spaceAfter=2, leading=18)
sSlideHead = S("sSlideHead", fontName=BOLD, fontSize=19, textColor=C_WHITE,
               alignment=TA_CENTER, spaceAfter=6, leading=24)
sSlideHeadDark = S("sSlideHeadDark", fontName=BOLD, fontSize=18, textColor=C_NAVY,
                   alignment=TA_CENTER, spaceAfter=6, leading=24)
sSectionTag = S("sSectionTag", fontName=BOLD, fontSize=11, textColor=C_WHITE,
                alignment=TA_CENTER, spaceAfter=2, leading=14)
sEN  = S("sEN",  fontName=BOLD, fontSize=11, textColor=C_NAVY,
         alignment=TA_LEFT, spaceAfter=1, leading=15)
sRU  = S("sRU",  fontName=ITAL, fontSize=10, textColor=C_TEAL,
         alignment=TA_LEFT, spaceAfter=6, leading=14)
sENc = S("sENc",  fontName=BOLD, fontSize=11, textColor=C_NAVY,
         alignment=TA_CENTER, spaceAfter=1, leading=15)
sRUc = S("sRUc",  fontName=ITAL, fontSize=10, textColor=C_TEAL,
         alignment=TA_CENTER, spaceAfter=6, leading=14)
sBulletEN = S("sBulletEN", fontName=BASE, fontSize=10.5, textColor=C_NAVY,
              alignment=TA_LEFT, spaceAfter=1, leading=14, leftIndent=10)
sBulletRU = S("sBulletRU", fontName=ITAL, fontSize=9.5, textColor=C_PURPLE,
              alignment=TA_LEFT, spaceAfter=5, leading=13, leftIndent=10)
sCapEN = S("sCapEN", fontName=BOLD, fontSize=10, textColor=C_NAVY,
           alignment=TA_CENTER, spaceAfter=1, leading=13)
sCapRU = S("sCapRU", fontName=ITAL, fontSize=9, textColor=C_TEAL,
           alignment=TA_CENTER, spaceAfter=4, leading=12)
sSmall = S("sSmall", fontName=BASE, fontSize=9, textColor=C_DGREY,
           alignment=TA_LEFT, spaceAfter=2, leading=12)
sSmallRU = S("sSmallRU", fontName=ITAL, fontSize=8.5, textColor=C_PURPLE,
             alignment=TA_LEFT, spaceAfter=4, leading=12)
sQuest = S("sQuest", fontName=BOLD, fontSize=11, textColor=C_ORANGE,
           alignment=TA_LEFT, spaceAfter=2, leading=15)
sQuestRU = S("sQuestRU", fontName=ITAL, fontSize=10, textColor=C_DGREY,
             alignment=TA_LEFT, spaceAfter=6, leading=14)
sAns  = S("sAns", fontName=BASE, fontSize=10, textColor=C_NAVY,
          alignment=TA_LEFT, spaceAfter=1, leading=13, leftIndent=14)
sAnsRU = S("sAnsRU", fontName=ITAL, fontSize=9.5, textColor=C_TEAL,
           alignment=TA_LEFT, spaceAfter=5, leading=13, leftIndent=14)
sTip  = S("sTip", fontName=ITAL, fontSize=10, textColor=C_WHITE,
          alignment=TA_LEFT, spaceAfter=2, leading=14)
sGramEN = S("sGramEN", fontName=BOLD, fontSize=10.5, textColor=C_NAVY,
            alignment=TA_LEFT, spaceAfter=1, leading=14)
sGramRU = S("sGramRU", fontName=ITAL, fontSize=9.5, textColor=C_PURPLE,
            alignment=TA_LEFT, spaceAfter=5, leading=13)
sFooter = S("sFooter", fontName=ITAL, fontSize=8, textColor=colors.HexColor("#9E9E9E"),
            alignment=TA_CENTER, leading=10)

# ─── Helper Flowables ────────────────────────────────────────────────────────
class ColorBand(Flowable):
    """Full-width coloured rectangle used as a section header band."""
    def __init__(self, color, height=30):
        Flowable.__init__(self)
        self.color = color
        self.band_h = height
        self.width = W - 40  # page width minus margins
    def wrap(self, aW, aH):
        return aW, self.band_h + 4
    def draw(self):
        self.canv.setFillColor(self.color)
        self.canv.rect(0, 0, self.width, self.band_h, fill=1, stroke=0)

class RoundedBox(Flowable):
    """Coloured rounded-corner box."""
    def __init__(self, color, height=40, radius=8, width_frac=1.0):
        Flowable.__init__(self)
        self.color = color
        self.box_h = height
        self.r = radius
        self.wf = width_frac
    def wrap(self, aW, aH):
        self.box_w = aW * self.wf
        return aW, self.box_h + 6
    def draw(self):
        self.canv.setFillColor(self.color)
        self.canv.roundRect(0, 2, self.box_w, self.box_h, self.r, fill=1, stroke=0)

def spacer(h=6):
    return Spacer(1, h)

def hr(color=C_TEAL, thickness=1.5):
    return HRFlowable(width="100%", thickness=thickness, color=color, spaceAfter=4, spaceBefore=4)

def en_ru(en_text, ru_text, en_style=sEN, ru_style=sRU):
    return [Paragraph(en_text, en_style), Paragraph(ru_text, ru_style)]

def bullet_en_ru(en_text, ru_text, bullet="✦"):
    return en_ru(f"{bullet} {en_text}", f"  {ru_text}")

def numbered_en_ru(n, en_text, ru_text):
    return en_ru(f"{n}. {en_text}", f"   {ru_text}")

def section_header(title_en, title_ru, bg_color, text_color=C_WHITE):
    """Returns a coloured table acting as a slide header bar."""
    data = [[Paragraph(title_en, S("sh", fontName=BOLD, fontSize=16,
                                    textColor=text_color, alignment=TA_CENTER, leading=20)),
             Paragraph(title_ru, S("shr", fontName=ITAL, fontSize=11,
                                    textColor=text_color, alignment=TA_CENTER, leading=14))]]
    t = Table(data, colWidths=[W - 40])
    t.setStyle(TableStyle([
        ("BACKGROUND",  (0,0), (-1,-1), bg_color),
        ("TOPPADDING",  (0,0), (-1,-1), 8),
        ("BOTTOMPADDING",(0,0),(-1,-1), 8),
        ("LEFTPADDING", (0,0), (-1,-1), 12),
        ("RIGHTPADDING",(0,0), (-1,-1), 12),
        ("ROUNDEDCORNERS", [6]),
    ]))
    return t

def colored_box_table(content_rows, bg_color, border_color=None, col_widths=None):
    """Wrap content rows in a coloured table box."""
    t = Table(content_rows, colWidths=col_widths or [W - 60])
    style_cmds = [
        ("BACKGROUND",   (0,0), (-1,-1), bg_color),
        ("TOPPADDING",   (0,0), (-1,-1), 6),
        ("BOTTOMPADDING",(0,0), (-1,-1), 4),
        ("LEFTPADDING",  (0,0), (-1,-1), 10),
        ("RIGHTPADDING", (0,0), (-1,-1), 10),
        ("VALIGN",       (0,0), (-1,-1), "TOP"),
    ]
    if border_color:
        style_cmds += [("BOX", (0,0), (-1,-1), 1.5, border_color)]
    t.setStyle(TableStyle(style_cmds))
    return t

# ─── Cover Page ──────────────────────────────────────────────────────────────
def cover_page():
    elems = []
    # big gradient-look header band
    cover_data = [[
        Paragraph("🎓  WELCOME TO YOUR ENGLISH LESSON!", sTitle),
        Paragraph("Добро пожаловать на урок английского языка!", sSubtitle),
        Spacer(1, 6),
        Paragraph("B1 / B1+  Intermediate  |  Trial Class  |  60 Minutes",
                  S("cv1", fontName=BOLD, fontSize=12, textColor=C_GOLD, alignment=TA_CENTER, leading=16)),
        Paragraph("B1 / B1+  Средний уровень  |  Пробный урок  |  60 минут",
                  S("cv2", fontName=ITAL, fontSize=10, textColor=C_WHITE, alignment=TA_CENTER, leading=14)),
    ]]
    cvt = Table(cover_data, colWidths=[W - 40])
    cvt.setStyle(TableStyle([
        ("BACKGROUND",   (0,0), (-1,-1), C_NAVY),
        ("TOPPADDING",   (0,0), (-1,-1), 22),
        ("BOTTOMPADDING",(0,0), (-1,-1), 22),
        ("LEFTPADDING",  (0,0), (-1,-1), 20),
        ("RIGHTPADDING", (0,0), (-1,-1), 20),
    ]))
    elems.append(cvt)
    elems.append(spacer(12))

    # Welcome message box
    wm_data = [[
        Paragraph("Hello! Today is your first lesson. You are at B1/B1+ (intermediate) level, which means you can already communicate in English – and today we will make it even better!",
                  S("wm", fontName=BASE, fontSize=11, textColor=C_NAVY, leading=16, alignment=TA_LEFT)),
        Paragraph("Привет! Сегодня ваш первый урок. Вы находитесь на уровне B1/B1+ (средний), что означает – вы уже умеете общаться по-английски, а сегодня мы сделаем это ещё лучше!",
                  S("wmr", fontName=ITAL, fontSize=10, textColor=C_TEAL, leading=14, alignment=TA_LEFT)),
    ]]
    wmt = Table(wm_data, colWidths=[W - 60])
    wmt.setStyle(TableStyle([
        ("BACKGROUND",   (0,0), (-1,-1), C_LBLUE),
        ("TOPPADDING",   (0,0), (-1,-1), 12),
        ("BOTTOMPADDING",(0,0), (-1,-1), 12),
        ("LEFTPADDING",  (0,0), (-1,-1), 14),
        ("RIGHTPADDING", (0,0), (-1,-1), 14),
        ("BOX",          (0,0), (-1,-1), 2, C_TEAL),
    ]))
    elems.append(wmt)
    elems.append(spacer(10))

    # Lesson overview table
    overview_header = [[Paragraph("📋  LESSON OVERVIEW / ПЛАН УРОКА", S("oh", fontName=BOLD, fontSize=13,
                                   textColor=C_WHITE, alignment=TA_CENTER, leading=16))]]
    oht = Table(overview_header, colWidths=[W - 40])
    oht.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_ORANGE),
                              ("TOPPADDING",(0,0),(-1,-1),7),("BOTTOMPADDING",(0,0),(-1,-1),7)]))
    elems.append(oht)

    rows = [
        ["⏱ Time / Время", "Activity / Деятельность"],
        ["0–10 min", "Level Check – Warm-up Interview\nПроверка уровня – Разминочное интервью"],
        ["10–20 min", "Grammar Spotlight: Present Perfect vs. Past Simple\nГрамматика: Present Perfect vs. Past Simple"],
        ["20–30 min", "Vocabulary Power: Food, Sport & Travel\nСловарный запас: Еда, Спорт и Путешествия"],
        ["30–50 min", "Speaking Practice: Describe & Compare Photos\nРечевая практика: Описание и сравнение фото"],
        ["50–60 min", "Fun Quiz + Wrap-Up\nВесёлая викторина + Подведение итогов"],
    ]
    col_w = [(W - 40) * 0.22, (W - 40) * 0.78]
    ot = Table(rows, colWidths=col_w)
    ts = TableStyle([
        ("BACKGROUND",   (0,0), (1,0),  C_NAVY),
        ("TEXTCOLOR",    (0,0), (1,0),  C_WHITE),
        ("FONTNAME",     (0,0), (1,0),  BOLD),
        ("FONTSIZE",     (0,0), (1,0),  11),
        ("ALIGN",        (0,0), (1,0),  "CENTER"),
        ("TOPPADDING",   (0,0), (-1,-1), 6),
        ("BOTTOMPADDING",(0,0), (-1,-1), 6),
        ("LEFTPADDING",  (0,0), (-1,-1), 8),
        ("RIGHTPADDING", (0,0), (-1,-1), 8),
        ("ROWBACKGROUNDS",(0,1),(-1,-1), [C_LYELL, C_LGRN, C_LYELL, C_LGRN, C_LYELL]),
        ("FONTNAME",     (0,1), (0,-1), BOLD),
        ("FONTNAME",     (1,1), (1,-1), BASE),
        ("FONTSIZE",     (0,1), (-1,-1), 10),
        ("TEXTCOLOR",    (0,1), (0,-1), C_ORANGE),
        ("TEXTCOLOR",    (1,1), (1,-1), C_NAVY),
        ("GRID",         (0,0), (-1,-1), 0.5, colors.HexColor("#BDBDBD")),
        ("VALIGN",       (0,0), (-1,-1), "MIDDLE"),
    ])
    ot.setStyle(ts)
    elems.append(ot)

    elems.append(spacer(12))
    tip_data = [[
        Paragraph("💡 Teacher's Note: This handbook belongs to YOU. Keep it, write in it, and bring it every lesson!",
                  S("tn", fontName=ITAL, fontSize=10, textColor=C_WHITE, leading=14)),
        Paragraph("Заметка учителя: Этот справочник принадлежит ВАМ. Храните его, пишите в нём и приносите на каждый урок!",
                  S("tnr", fontName=ITAL, fontSize=9.5, textColor=C_GOLD, leading=13)),
    ]]
    tipt = Table(tip_data, colWidths=[W - 60])
    tipt.setStyle(TableStyle([
        ("BACKGROUND",   (0,0),(-1,-1), C_PURPLE),
        ("TOPPADDING",   (0,0),(-1,-1), 10),
        ("BOTTOMPADDING",(0,0),(-1,-1), 10),
        ("LEFTPADDING",  (0,0),(-1,-1), 14),
        ("RIGHTPADDING", (0,0),(-1,-1), 14),
    ]))
    elems.append(tipt)
    return elems

# ─── Part 1 – Level Check ────────────────────────────────────────────────────
def part1_level_check():
    elems = []
    elems.append(PageBreak())
    elems.append(section_header(
        "PART 1  |  LEVEL CHECK  –  WHO ARE YOU?",
        "ЧАСТЬ 1  |  ПРОВЕРКА УРОВНЯ  –  КТО ВЫ?",
        C_TEAL))
    elems.append(spacer(8))

    # Intro box
    intro = [[
        Paragraph("Your teacher will ask you these questions. Just speak naturally – there are no wrong answers! The goal is for your teacher to understand your level.",
                  S("i1", fontName=BASE, fontSize=10.5, textColor=C_NAVY, leading=15)),
        Paragraph("Учитель задаст вам эти вопросы. Просто говорите естественно – неправильных ответов нет! Цель состоит в том, чтобы учитель понял ваш уровень.",
                  S("i2", fontName=ITAL, fontSize=10, textColor=C_PURPLE, leading=14)),
    ]]
    it = Table(intro, colWidths=[W - 60])
    it.setStyle(TableStyle([
        ("BACKGROUND",   (0,0),(-1,-1), C_LPURP),
        ("TOPPADDING",   (0,0),(-1,-1), 10),
        ("BOTTOMPADDING",(0,0),(-1,-1), 10),
        ("LEFTPADDING",  (0,0),(-1,-1), 12),
        ("RIGHTPADDING", (0,0),(-1,-1), 12),
        ("BOX",          (0,0),(-1,-1), 1.5, C_PURPLE),
    ]))
    elems.append(it)
    elems.append(spacer(10))

    # Question blocks
    qs = [
        ("A", C_ORANGE, C_LORANG,
         "Tell me about yourself.",
         "Расскажите о себе.",
         ["What is your name? Where are you from?",
          "What do you do? (job / study)",
          "How long have you been learning English?"],
         ["Как вас зовут? Откуда вы?",
          "Чем вы занимаетесь? (работа / учёба)",
          "Как давно вы изучаете английский язык?"]),
        ("B", C_LIME, C_LGRN,
         "Tell me about your free time.",
         "Расскажите о вашем свободном времени.",
         ["What do you usually do at weekends?",
          "What hobbies or sports do you enjoy?",
          "Have you ever travelled abroad? Where did you go?"],
         ["Что вы обычно делаете по выходным?",
          "Какие у вас увлечения или спорт?",
          "Вы когда-нибудь путешествовали за границу? Куда вы ездили?"]),
        ("C", C_PURPLE, C_LPURP,
         "What do you want from English lessons?",
         "Чего вы хотите от уроков английского?",
         ["Why are you learning English?",
          "What is difficult for you in English?",
          "What is your goal – business, travel, conversation?"],
         ["Почему вы учите английский?",
          "Что вам трудно даётся в английском?",
          "Какова ваша цель – бизнес, путешествия, общение?"]),
    ]

    for letter, accent, bg, en_q, ru_q, en_subs, ru_subs in qs:
        # Header chip
        qh_data = [[
            Paragraph(f"Question {letter}", S("qhd", fontName=BOLD, fontSize=11, textColor=C_WHITE, leading=14)),
        ]]
        qht = Table(qh_data, colWidths=[90])
        qht.setStyle(TableStyle([
            ("BACKGROUND",(0,0),(-1,-1), accent),
            ("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),4),
            ("LEFTPADDING",(0,0),(-1,-1),10),
        ]))
        elems.append(qht)

        qb_data = [
            [Paragraph(f"❓ {en_q}", S("qq", fontName=BOLD, fontSize=11.5, textColor=accent, leading=15))],
            [Paragraph(f"    {ru_q}", S("qqr", fontName=ITAL, fontSize=10, textColor=C_DGREY, leading=14))],
        ]
        for i in range(len(en_subs)):
            qb_data.append([Paragraph(f"    • {en_subs[i]}", S("qs", fontName=BASE, fontSize=10, textColor=C_NAVY, leading=13))])
            qb_data.append([Paragraph(f"       {ru_subs[i]}", S("qsr", fontName=ITAL, fontSize=9.5, textColor=C_TEAL, leading=13))])

        qbt = Table(qb_data, colWidths=[W - 60])
        qbt.setStyle(TableStyle([
            ("BACKGROUND",   (0,0),(-1,-1), bg),
            ("TOPPADDING",   (0,0),(-1,-1), 5),
            ("BOTTOMPADDING",(0,0),(-1,-1), 4),
            ("LEFTPADDING",  (0,0),(-1,-1), 12),
            ("RIGHTPADDING", (0,0),(-1,-1), 12),
            ("BOX",          (0,0),(-1,-1), 1.5, accent),
        ]))
        elems.append(qbt)
        elems.append(spacer(8))

    # What the teacher is listening for
    listen_data = [[
        Paragraph("🎧  What is your teacher listening for? / Что проверяет учитель?",
                  S("lh", fontName=BOLD, fontSize=11, textColor=C_WHITE, leading=14)),
    ],[
        Table([
            [Paragraph("✔ Vocabulary range / Словарный запас", S("lv", fontName=BASE, fontSize=10, textColor=C_NAVY, leading=13)),
             Paragraph("✔ Sentence structure / Структура предложений", S("lv", fontName=BASE, fontSize=10, textColor=C_NAVY, leading=13))],
            [Paragraph("✔ Grammar accuracy / Точность грамматики", S("lv", fontName=BASE, fontSize=10, textColor=C_NAVY, leading=13)),
             Paragraph("✔ Confidence & fluency / Уверенность и беглость", S("lv", fontName=BASE, fontSize=10, textColor=C_NAVY, leading=13))],
        ], colWidths=[(W-80)/2, (W-80)/2], style=[
            ("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),3),
            ("LEFTPADDING",(0,0),(-1,-1),4),("RIGHTPADDING",(0,0),(-1,-1),4),
            ("BACKGROUND",(0,0),(-1,-1),C_LGREY),
        ])
    ]]
    lt = Table(listen_data, colWidths=[W - 60])
    lt.setStyle(TableStyle([
        ("BACKGROUND",   (0,0),(0,0), C_TEAL),
        ("BACKGROUND",   (0,1),(0,1), C_LGREY),
        ("TOPPADDING",   (0,0),(-1,-1), 8),
        ("BOTTOMPADDING",(0,0),(-1,-1), 8),
        ("LEFTPADDING",  (0,0),(-1,-1), 12),
        ("RIGHTPADDING", (0,0),(-1,-1), 12),
        ("BOX",          (0,0),(-1,-1), 1.5, C_TEAL),
    ]))
    elems.append(lt)
    return elems

# ─── Part 2 – Grammar Spotlight ──────────────────────────────────────────────
def part2_grammar():
    elems = []
    elems.append(PageBreak())
    elems.append(section_header(
        "PART 2  |  GRAMMAR SPOTLIGHT",
        "ЧАСТЬ 2  |  ГРАММАТИКА: В ЦЕНТРЕ ВНИМАНИЯ",
        C_ORANGE))
    elems.append(spacer(6))

    topic_hdr = [[Paragraph("Present Perfect  VS  Past Simple",
                             S("th", fontName=BOLD, fontSize=16, textColor=C_NAVY, alignment=TA_CENTER, leading=20)),
                  Paragraph("Настоящее совершённое время  VS  Прошедшее простое время",
                             S("thr", fontName=ITAL, fontSize=11, textColor=C_TEAL, alignment=TA_CENTER, leading=14))]]
    tht = Table(topic_hdr, colWidths=[W - 40])
    tht.setStyle(TableStyle([
        ("BACKGROUND",(0,0),(-1,-1),C_LYELL),
        ("TOPPADDING",(0,0),(-1,-1),8),("BOTTOMPADDING",(0,0),(-1,-1),8),
        ("BOX",(0,0),(-1,-1),2,C_ORANGE),
    ]))
    elems.append(tht)
    elems.append(spacer(8))

    # Two-column comparison
    pp_data = [
        [Paragraph("PRESENT PERFECT", S("pph", fontName=BOLD, fontSize=12, textColor=C_WHITE, alignment=TA_CENTER, leading=16)),
         Paragraph("PAST SIMPLE", S("psh", fontName=BOLD, fontSize=12, textColor=C_WHITE, alignment=TA_CENTER, leading=16))],
        [Paragraph("Настоящее совершённое", S("pphr", fontName=ITAL, fontSize=10, textColor=C_GOLD, alignment=TA_CENTER, leading=13)),
         Paragraph("Прошедшее простое", S("pshr", fontName=ITAL, fontSize=10, textColor=C_GOLD, alignment=TA_CENTER, leading=13))],
        [Paragraph("have / has + past participle\nI have visited Paris.\nЯ посещал(а) Париж.", S("ppf", fontName=BASE, fontSize=10, textColor=C_NAVY, leading=14)),
         Paragraph("verb + -ed / irregular form\nI visited Paris last year.\nЯ посетил(а) Париж в прошлом году.", S("psf", fontName=BASE, fontSize=10, textColor=C_NAVY, leading=14))],
        [Paragraph("Use when: No specific time / experience / result matters\nКогда: Нет точного времени / опыт / важен результат",
                   S("ppu", fontName=ITAL, fontSize=9.5, textColor=C_TEAL, leading=13)),
         Paragraph("Use when: Specific time in the past\nКогда: Конкретное время в прошлом",
                   S("psu", fontName=ITAL, fontSize=9.5, textColor=C_TEAL, leading=13))],
        [Paragraph("🔑 Signal words:\nalready, yet, ever, never, just, recently, since, for\nСлова-маркеры: уже, ещё не, когда-либо, никогда, только что, недавно, с тех пор, в течение",
                   S("ppk", fontName=BASE, fontSize=9.5, textColor=C_NAVY, leading=13)),
         Paragraph("🔑 Signal words:\nyesterday, last week/year, ago, in 2010, when I was...\nСлова-маркеры: вчера, на прошлой неделе/году, назад, в 2010 году, когда я был(а)...",
                   S("psk", fontName=BASE, fontSize=9.5, textColor=C_NAVY, leading=13))],
    ]
    ppt = Table(pp_data, colWidths=[(W-60)/2, (W-60)/2])
    ppt.setStyle(TableStyle([
        ("BACKGROUND",   (0,0),(0,1), C_TEAL),
        ("BACKGROUND",   (1,0),(1,1), C_ORANGE),
        ("BACKGROUND",   (0,2),(-1,-1), C_LGRN),
        ("ROWBACKGROUNDS",(0,2),(-1,-1), [C_LGRN, C_LGREY, C_LYELL]),
        ("TOPPADDING",   (0,0),(-1,-1), 7),
        ("BOTTOMPADDING",(0,0),(-1,-1), 7),
        ("LEFTPADDING",  (0,0),(-1,-1), 8),
        ("RIGHTPADDING", (0,0),(-1,-1), 8),
        ("GRID",         (0,0),(-1,-1), 1, colors.HexColor("#BDBDBD")),
        ("VALIGN",       (0,0),(-1,-1), "TOP"),
    ]))
    elems.append(ppt)
    elems.append(spacer(10))

    # Examples in context
    ex_hdr = [[Paragraph("📝  EXAMPLES IN CONTEXT / ПРИМЕРЫ В КОНТЕКСТЕ",
                          S("exh", fontName=BOLD, fontSize=11, textColor=C_WHITE, leading=14))]]
    exht = Table(ex_hdr, colWidths=[W - 40])
    exht.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_PURPLE),
                               ("TOPPADDING",(0,0),(-1,-1),7),("BOTTOMPADDING",(0,0),(-1,-1),7),
                               ("LEFTPADDING",(0,0),(-1,-1),12)]))
    elems.append(exht)

    examples = [
        ('✔ "Have you ever eaten sushi?"  –  "Yes, I have eaten sushi twice."',
         '✔ «Вы когда-нибудь ели суши?» – «Да, я ел(а) суши дважды.»'),
        ('✔ "I have just finished my homework."',
         '✔ «Я только что закончил(а) домашнее задание.»'),
        ('✔ "Did you watch the match last night?"  –  "Yes, I watched it."',
         '✔ «Ты смотрел(а) матч вчера вечером?» – «Да, я смотрел(а).»'),
        ('✔ "She moved to Moscow in 2019."',
         '✔ «Она переехала в Москву в 2019 году.»'),
    ]
    for en, ru in examples:
        ex_data = [[Paragraph(en, S("ex", fontName=BASE, fontSize=10.5, textColor=C_NAVY, leading=14)),
                    Paragraph(ru, S("exr", fontName=ITAL, fontSize=10, textColor=C_TEAL, leading=13))]]
        ext = Table(ex_data, colWidths=[W - 60])
        ext.setStyle(TableStyle([
            ("BACKGROUND",(0,0),(-1,-1),C_LBLUE),
            ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
            ("LEFTPADDING",(0,0),(-1,-1),10),("RIGHTPADDING",(0,0),(-1,-1),10),
            ("BOX",(0,0),(-1,-1),1,C_TEAL),
        ]))
        elems.append(ext)
        elems.append(spacer(4))

    elems.append(spacer(8))

    # Quick practice
    pr_hdr = [[Paragraph("✏  QUICK PRACTICE – Choose the correct form / БЫСТРАЯ ПРАКТИКА – Выберите правильную форму",
                          S("prh", fontName=BOLD, fontSize=10.5, textColor=C_WHITE, leading=14))]]
    prht = Table(pr_hdr, colWidths=[W - 40])
    prht.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LIME),
                               ("TOPPADDING",(0,0),(-1,-1),7),("BOTTOMPADDING",(0,0),(-1,-1),7),
                               ("LEFTPADDING",(0,0),(-1,-1),12)]))
    elems.append(prht)

    practice_qs = [
        ("1.", "I ________ (never / visit) New York.   [a] have never visited   [b] never visited",
                "Я ________ в Нью-Йорке.   [a] никогда не был(а)   [b] никогда не посещал(а)"),
        ("2.", 'She ________ (go) to the cinema last Saturday.   [a] has gone   [b] went',
                'Она ________ в кино в прошлую субботу.   [a] пошла   [b] ходила'),
        ("3.", 'They ________ (already / eat) dinner.   [a] already ate   [b] have already eaten',
                'Они ________ ужин.   [a] уже поели   [b] уже съели'),
        ("4.", '"When ________ you ________ (learn) to drive?"   [a] have… learned   [b] did… learn',
                '"Когда вы ________ водить машину?"   [a] научились   [b] учились')
    ]
    for num, en_q, ru_q in practice_qs:
        pq_data = [
            [Paragraph(num, S("pqn", fontName=BOLD, fontSize=11, textColor=C_ORANGE, leading=14)),
             Paragraph(en_q, S("pqe", fontName=BASE, fontSize=10, textColor=C_NAVY, leading=13))],
            [Paragraph("", sSmall),
             Paragraph(ru_q, S("pqr", fontName=ITAL, fontSize=9.5, textColor=C_TEAL, leading=13))],
        ]
        pqt = Table(pq_data, colWidths=[22, W - 82])
        pqt.setStyle(TableStyle([
            ("BACKGROUND",(0,0),(-1,-1),C_LYELL),
            ("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),3),
            ("LEFTPADDING",(0,0),(-1,-1),6),("RIGHTPADDING",(0,0),(-1,-1),8),
            ("VALIGN",(0,0),(-1,-1),"TOP"),
            ("BOX",(0,0),(-1,-1),0.5,C_GOLD),
        ]))
        elems.append(pqt)
        elems.append(spacer(3))

    ans_data = [[Paragraph("✅  ANSWERS / ОТВЕТЫ:  1–a  |  2–b  |  3–b  |  4–b",
                            S("ans", fontName=BOLD, fontSize=10, textColor=C_WHITE, leading=14))]]
    anst = Table(ans_data, colWidths=[W - 40])
    anst.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LIME),
                               ("TOPPADDING",(0,0),(-1,-1),6),("BOTTOMPADDING",(0,0),(-1,-1),6),
                               ("LEFTPADDING",(0,0),(-1,-1),14)]))
    elems.append(spacer(4))
    elems.append(anst)
    return elems

# ─── Part 3 – Vocabulary ─────────────────────────────────────────────────────
def part3_vocabulary():
    elems = []
    elems.append(PageBreak())
    elems.append(section_header(
        "PART 3  |  VOCABULARY POWER",
        "ЧАСТЬ 3  |  СИЛА СЛОВ",
        C_PURPLE))
    elems.append(spacer(6))

    intro_d = [[Paragraph("Here are key words for today's topics. Read, repeat, and try to use them in sentences!",
                           S("vi", fontName=BASE, fontSize=10.5, textColor=C_NAVY, leading=15)),
                Paragraph("Вот ключевые слова для сегодняшних тем. Читайте, повторяйте и попробуйте использовать их в предложениях!",
                           S("vir", fontName=ITAL, fontSize=10, textColor=C_PURPLE, leading=14))]]
    ivt = Table(intro_d, colWidths=[W - 60])
    ivt.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LPURP),("TOPPADDING",(0,0),(-1,-1),8),
                              ("BOTTOMPADDING",(0,0),(-1,-1),8),("LEFTPADDING",(0,0),(-1,-1),12),
                              ("BOX",(0,0),(-1,-1),1.5,C_PURPLE)]))
    elems.append(ivt)
    elems.append(spacer(8))

    categories = [
        ("🍕  FOOD & EATING / ЕДА", C_ORANGE, C_LORANG, [
            ("to dine out", "есть вне дома / в ресторане", "We dine out every Friday. / Мы едим вне дома каждую пятницу."),
            ("to eat in", "есть дома", "I prefer to eat in when I'm tired. / Я предпочитаю есть дома, когда устала."),
            ("a cosy atmosphere", "уютная атмосфера", "The café has a cosy atmosphere. / В кафе уютная атмосфера."),
            ("a balanced diet", "сбалансированное питание", "A balanced diet keeps you healthy. / Сбалансированное питание сохраняет здоровье."),
            ("to have a bite", "перекусить", "Let's have a bite before the film. / Давай перекусим перед фильмом."),
        ]),
        ("🏃  SPORT & EXERCISE / СПОРТ", C_LIME, C_LGRN, [
            ("competitive", "соревновательный", "Tennis is a competitive sport. / Теннис – соревновательный вид спорта."),
            ("to keep fit", "поддерживать форму", "I go jogging to keep fit. / Я бегаю трусцой, чтобы поддерживать форму."),
            ("exhausted", "измотанный, очень усталый", "After the match I was exhausted. / После матча я был(а) измотан(а)."),
            ("mindful", "осознанный", "Yoga is a mindful exercise. / Йога – осознанная практика."),
            ("to take up a sport", "начать заниматься спортом", "She took up yoga last year. / Она занялась йогой в прошлом году."),
        ]),
        ("✈  TRAVEL & TRANSPORT / ПУТЕШЕСТВИЯ", C_TEAL, C_LBLUE, [
            ("to board", "садиться (в транспорт)", "We need to board the plane now. / Нам нужно сесть в самолёт."),
            ("overcast sky", "облачное, пасмурное небо", "The sky was overcast all morning. / Небо было пасмурным всё утро."),
            ("backpacker", "путешественник с рюкзаком", "As a backpacker, she visited 10 countries. / Как рюкзачный турист, она посетила 10 стран."),
            ("adventurous", "любящий приключения", "He is very adventurous and loves to travel. / Он очень любит приключения."),
            ("a crowded queue", "длинная очередь / толпа", "There was a crowded queue at the airport. / В аэропорту была длинная очередь."),
        ]),
    ]

    for cat_title, accent, bg, words in categories:
        ch_data = [[Paragraph(cat_title, S("ch", fontName=BOLD, fontSize=12, textColor=C_WHITE, leading=15))]]
        cht = Table(ch_data, colWidths=[W - 40])
        cht.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),accent),
                                  ("TOPPADDING",(0,0),(-1,-1),7),("BOTTOMPADDING",(0,0),(-1,-1),7),
                                  ("LEFTPADDING",(0,0),(-1,-1),12)]))
        elems.append(cht)

        word_rows = [
            [Paragraph("Word / Слово", S("wh", fontName=BOLD, fontSize=10, textColor=C_WHITE, leading=13)),
             Paragraph("Russian / Перевод", S("wh", fontName=BOLD, fontSize=10, textColor=C_WHITE, leading=13)),
             Paragraph("Example / Пример", S("wh", fontName=BOLD, fontSize=10, textColor=C_WHITE, leading=13))]
        ]
        for en_w, ru_w, ex in words:
            word_rows.append([
                Paragraph(en_w, S("wen", fontName=BOLD, fontSize=10, textColor=C_NAVY, leading=13)),
                Paragraph(ru_w, S("wru", fontName=ITAL, fontSize=10, textColor=C_PURPLE, leading=13)),
                Paragraph(ex, S("wex", fontName=BASE, fontSize=9, textColor=C_DGREY, leading=12)),
            ])

        wt = Table(word_rows, colWidths=[(W-60)*0.22, (W-60)*0.25, (W-60)*0.53])
        wts = TableStyle([
            ("BACKGROUND",   (0,0),(2,0), accent),
            ("ROWBACKGROUNDS",(0,1),(-1,-1), [C_WHITE, bg]),
            ("TOPPADDING",   (0,0),(-1,-1), 5),
            ("BOTTOMPADDING",(0,0),(-1,-1), 5),
            ("LEFTPADDING",  (0,0),(-1,-1), 7),
            ("RIGHTPADDING", (0,0),(-1,-1), 7),
            ("GRID",         (0,0),(-1,-1), 0.5, colors.HexColor("#BDBDBD")),
            ("VALIGN",       (0,0),(-1,-1), "TOP"),
        ])
        wt.setStyle(wts)
        elems.append(wt)
        elems.append(spacer(8))

    return elems

# ─── Part 4 – Photo Speaking ─────────────────────────────────────────────────
def part4_photos(img_dir):
    elems = []
    elems.append(PageBreak())
    elems.append(section_header(
        "PART 4  |  SPEAKING PRACTICE – DESCRIBE & COMPARE PHOTOS",
        "ЧАСТЬ 4  |  РЕЧЕВАЯ ПРАКТИКА – ОПИСАНИЕ И СРАВНЕНИЕ ФОТО",
        C_PINK))
    elems.append(spacer(6))

    intro_d = [[
        Paragraph("Look at each pair of photos. First DESCRIBE what you see (1 minute). Then COMPARE both photos (1 minute). Use the Useful Language box below each photo pair.",
                  S("pi", fontName=BASE, fontSize=10.5, textColor=C_NAVY, leading=15)),
        Paragraph("Посмотрите на каждую пару фотографий. Сначала ОПИШИТЕ то, что видите (1 минута). Затем СРАВНИТЕ обе фотографии (1 минута). Используйте блок «Полезные выражения» под каждой парой фото.",
                  S("pir", fontName=ITAL, fontSize=10, textColor=C_PURPLE, leading=14)),
    ]]
    pit = Table(intro_d, colWidths=[W - 60])
    pit.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LPURP),
                              ("TOPPADDING",(0,0),(-1,-1),10),("BOTTOMPADDING",(0,0),(-1,-1),10),
                              ("LEFTPADDING",(0,0),(-1,-1),12),("BOX",(0,0),(-1,-1),1.5,C_PINK)]))
    elems.append(pit)
    elems.append(spacer(8))

    # ── Useful language bank ──────────────────────────────────────────────────
    lang_hdr = [[Paragraph("🗣  USEFUL LANGUAGE / ПОЛЕЗНЫЕ ВЫРАЖЕНИЯ",
                            S("lbh", fontName=BOLD, fontSize=11, textColor=C_WHITE, leading=14))]]
    lht = Table(lang_hdr, colWidths=[W - 40])
    lht.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_NAVY),
                              ("TOPPADDING",(0,0),(-1,-1),7),("BOTTOMPADDING",(0,0),(-1,-1),7),
                              ("LEFTPADDING",(0,0),(-1,-1),12)]))
    elems.append(lht)

    phrases = [
        ("DESCRIBING / Описание",
         ["In the left / right photo I can see... – На левом / правом фото я вижу...",
          "It looks like... / It seems that... – Похоже на... / Кажется, что...",
          "In the foreground / background there is... – На переднем / заднем плане...",
          "The person is wearing... and appears to be... – Человек одет в... и, кажется, он(а)..."]),
        ("COMPARING / Сравнение",
         ["Both photos show... – На обоих фото изображено...",
          "While in the first photo..., in the second... – В то время как на первом фото..., на втором...",
          "In contrast to..., the other photo... – В отличие от..., другое фото...",
          "The main difference is that... – Главное отличие заключается в том, что..."]),
        ("EXPRESSING FEELINGS / Выражение чувств",
         ["The atmosphere seems... (lively / relaxed / tense) – Атмосфера кажется... (оживлённой / расслабленной / напряжённой)",
          "The person might feel... (excited / exhausted / happy) – Человек, возможно, чувствует себя... (взволнованным / усталым / счастливым)",
          "I think / I imagine the person is... – Я думаю / представляю, что человек..."]),
    ]
    for ph_cat, ph_list in phrases:
        ph_data = [[Paragraph(ph_cat, S("phc", fontName=BOLD, fontSize=10.5, textColor=C_NAVY, leading=14))]]
        for ph in ph_list:
            ph_data.append([Paragraph(f"  ▶ {ph}", S("phi", fontName=BASE, fontSize=9.5, textColor=C_DGREY, leading=13))])
        pht = Table(ph_data, colWidths=[W - 60])
        pht.setStyle(TableStyle([
            ("BACKGROUND",(0,0),(0,0), C_LYELL),
            ("BACKGROUND",(0,1),(-1,-1), C_WHITE),
            ("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),3),
            ("LEFTPADDING",(0,0),(-1,-1),10),("RIGHTPADDING",(0,0),(-1,-1),10),
            ("BOX",(0,0),(-1,-1),0.8,C_GOLD),
        ]))
        elems.append(pht)
        elems.append(spacer(3))

    elems.append(spacer(10))

    # ── Photo Set 1 – Eating ─────────────────────────────────────────────────
    def photo_header(num, topic_en, topic_ru, accent):
        hd = [[Paragraph(f"📸  PHOTO SET {num}:  {topic_en}",
                          S("psh", fontName=BOLD, fontSize=13, textColor=C_WHITE, leading=16)),
               Paragraph(f"НАБОР ФОТО {num}:  {topic_ru}",
                          S("pshr", fontName=ITAL, fontSize=10, textColor=C_GOLD, leading=14))]]
        ht = Table(hd, colWidths=[W - 40])
        ht.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),accent),
                                 ("TOPPADDING",(0,0),(-1,-1),8),("BOTTOMPADDING",(0,0),(-1,-1),8),
                                 ("LEFTPADDING",(0,0),(-1,-1),12)]))
        return ht

    def fit_image(path, max_w, max_h):
        try:
            img = PILImage.open(path)
            iw, ih = img.size
            scale = min(max_w / iw, max_h / ih)
            return RLImage(path, width=iw*scale, height=ih*scale)
        except Exception as e:
            print(f"Image error {path}: {e}")
            return Spacer(1, 10)

    def task_box(en_text, ru_text, accent):
        td = [[Paragraph(en_text, S("tb", fontName=BOLD, fontSize=10.5, textColor=accent, leading=14)),
               Paragraph(ru_text, S("tbr", fontName=ITAL, fontSize=10, textColor=C_DGREY, leading=13))]]
        tt = Table(td, colWidths=[W - 60])
        tt.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LGREY),
                                 ("TOPPADDING",(0,0),(-1,-1),7),("BOTTOMPADDING",(0,0),(-1,-1),7),
                                 ("LEFTPADDING",(0,0),(-1,-1),12),("BOX",(0,0),(-1,-1),1.5,accent)]))
        return tt

    # SET 1 – Eating
    elems.append(photo_header(1, "EATING – Where & How?", "ЕДА – Где и как?", C_ORANGE))
    elems.append(spacer(6))

    eating_img = fit_image(os.path.join(img_dir, "eating.jpg"), W - 60, 160)
    img_t1 = Table([[eating_img]], colWidths=[W - 40])
    img_t1.setStyle(TableStyle([("ALIGN",(0,0),(-1,-1),"CENTER"),
                                 ("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),4)]))
    elems.append(img_t1)
    elems.append(spacer(4))

    cap1_data = [[
        Paragraph("Photo A: Two women dining out at a restaurant",
                  S("c1", fontName=BOLD, fontSize=10, textColor=C_ORANGE, leading=13)),
        Paragraph("Фото A: Две женщины ужинают в ресторане",
                  S("c1r", fontName=ITAL, fontSize=9.5, textColor=C_TEAL, leading=13)),
        Paragraph("Photo B: A woman eating pasta at home",
                  S("c2", fontName=BOLD, fontSize=10, textColor=C_LIME, leading=13)),
        Paragraph("Фото B: Женщина ест пасту дома",
                  S("c2r", fontName=ITAL, fontSize=9.5, textColor=C_TEAL, leading=13)),
    ]]
    # cap as 2-col
    cap1t = Table([[Paragraph("🅐  Photo A: Two women dining out at a restaurant\n🇷🇺  Фото A: Две женщины ужинают в ресторане",
                               S("cap", fontName=BASE, fontSize=9.5, textColor=C_NAVY, leading=13)),
                    Paragraph("🅑  Photo B: A woman eating pasta alone at home\n🇷🇺  Фото B: Женщина ест пасту дома в одиночестве",
                               S("cap", fontName=BASE, fontSize=9.5, textColor=C_NAVY, leading=13))]],
                  colWidths=[(W-60)/2, (W-60)/2])
    cap1t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LORANG),
                                ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
                                ("LEFTPADDING",(0,0),(-1,-1),8),("GRID",(0,0),(-1,-1),0.5,C_GOLD)]))
    elems.append(cap1t)
    elems.append(spacer(5))

    elems.append(task_box(
        "Your task: Describe each photo (1 min each). Then compare: How is eating out different from eating at home? How might each woman feel?",
        "Ваше задание: Опишите каждое фото (по 1 мин). Затем сравните: Чем отличается питание вне дома от домашней еды? Как может чувствовать себя каждая женщина?",
        C_ORANGE))

    # helper questions
    hq_data_1 = [[
        Paragraph("Helper questions / Вспомогательные вопросы:", S("hqh", fontName=BOLD, fontSize=10, textColor=C_NAVY, leading=13)),
    ],[
        Paragraph("• What is each person eating?  /  Что ест каждый человек?\n"
                  "• What is the atmosphere like?  /  Какова атмосфера?\n"
                  "• Which meal looks more enjoyable and why?  /  Какая еда выглядит приятнее и почему?\n"
                  "• Have you ever eaten at a restaurant alone? How did it feel?  /  Вы когда-нибудь ели в ресторане одни? Как вы себя чувствовали?",
                  S("hqi", fontName=BASE, fontSize=9.5, textColor=C_DGREY, leading=14)),
    ]]
    hqt1 = Table(hq_data_1, colWidths=[W - 60])
    hqt1.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LYELL),
                               ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
                               ("LEFTPADDING",(0,0),(-1,-1),10),("BOX",(0,0),(-1,-1),1,C_GOLD)]))
    elems.append(spacer(4))
    elems.append(hqt1)

    # SET 2 – Exercise
    elems.append(PageBreak())
    elems.append(photo_header(2, "EXERCISE – Sport & Wellbeing", "СПОРТ И ЗДОРОВЬЕ", C_LIME))
    elems.append(spacer(6))

    ex_img = fit_image(os.path.join(img_dir, "exercise.jpg"), W - 60, 170)
    img_t2 = Table([[ex_img]], colWidths=[W - 40])
    img_t2.setStyle(TableStyle([("ALIGN",(0,0),(-1,-1),"CENTER"),
                                 ("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),4)]))
    elems.append(img_t2)
    elems.append(spacer(4))

    cap2t = Table([[Paragraph("🅐  Photo A: A tennis player in action\n🇷🇺  Фото A: Теннисистка во время игры",
                               S("cap", fontName=BASE, fontSize=9.5, textColor=C_NAVY, leading=13)),
                    Paragraph("🅑  Photo B: A woman doing a yoga backbend in nature\n🇷🇺  Фото B: Женщина делает прогиб в йоге на природе",
                               S("cap", fontName=BASE, fontSize=9.5, textColor=C_NAVY, leading=13))]],
                  colWidths=[(W-60)/2, (W-60)/2])
    cap2t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LGRN),
                                ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
                                ("LEFTPADDING",(0,0),(-1,-1),8),("GRID",(0,0),(-1,-1),0.5,C_LIME)]))
    elems.append(cap2t)
    elems.append(spacer(5))

    elems.append(task_box(
        "Your task: Compare the photographs and say how the exercising might make people feel. (1–2 minutes)",
        "Ваше задание: Сравните фотографии и скажите, как занятия спортом могут заставить людей себя чувствовать. (1–2 минуты)",
        C_LIME))

    hq_data_2 = [[
        Paragraph("Helper questions / Вспомогательные вопросы:", S("hqh", fontName=BOLD, fontSize=10, textColor=C_NAVY, leading=13)),
    ],[
        Paragraph("• What sport / exercise is each person doing?  /  Каким спортом / упражнением занимается каждый человек?\n"
                  "• How does each person look – calm, tense, focused?  /  Как выглядит каждый человек – спокойно, напряжённо, сосредоточенно?\n"
                  "• Which type of exercise do you prefer and why?  /  Какой вид упражнений вы предпочитаете и почему?\n"
                  "• Do you think sport is better alone or in a team?  /  Думаете ли вы, что заниматься спортом лучше в одиночку или в команде?",
                  S("hqi", fontName=BASE, fontSize=9.5, textColor=C_DGREY, leading=14)),
    ]]
    hqt2 = Table(hq_data_2, colWidths=[W - 60])
    hqt2.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LGRN),
                               ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
                               ("LEFTPADDING",(0,0),(-1,-1),10),("BOX",(0,0),(-1,-1),1,C_LIME)]))
    elems.append(spacer(4))
    elems.append(hqt2)

    # SET 3 – Transport
    elems.append(PageBreak())
    elems.append(photo_header(3, "TRAVEL – Public Transport", "ПУТЕШЕСТВИЯ – ОБЩЕСТВЕННЫЙ ТРАНСПОРТ", C_TEAL))
    elems.append(spacer(6))

    tr_img = fit_image(os.path.join(img_dir, "transport.jpg"), W - 60, 170)
    img_t3 = Table([[tr_img]], colWidths=[W - 40])
    img_t3.setStyle(TableStyle([("ALIGN",(0,0),(-1,-1),"CENTER"),
                                 ("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),4)]))
    elems.append(img_t3)
    elems.append(spacer(4))

    cap3t = Table([[Paragraph("🅐  Photo A: Passengers boarding a plane at an airport\n🇷🇺  Фото A: Пассажиры садятся в самолёт",
                               S("cap", fontName=BASE, fontSize=9.5, textColor=C_NAVY, leading=13)),
                    Paragraph("🅑  Photo B: Backpackers waiting to board a train\n🇷🇺  Фото B: Туристы ждут поезд на перроне",
                               S("cap", fontName=BASE, fontSize=9.5, textColor=C_NAVY, leading=13))]],
                  colWidths=[(W-60)/2, (W-60)/2])
    cap3t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LBLUE),
                                ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
                                ("LEFTPADDING",(0,0),(-1,-1),8),("GRID",(0,0),(-1,-1),0.5,C_TEAL)]))
    elems.append(cap3t)
    elems.append(spacer(5))

    elems.append(task_box(
        "Your task: Compare the photographs and say how comfortable each type of transport might be. (1–2 minutes)",
        "Ваше задание: Сравните фотографии и скажите, насколько комфортным может быть каждый вид транспорта. (1–2 минуты)",
        C_TEAL))

    hq_data_3 = [[
        Paragraph("Helper questions / Вспомогательные вопросы:", S("hqh", fontName=BOLD, fontSize=10, textColor=C_NAVY, leading=13)),
    ],[
        Paragraph("• What type of transport is in each photo?  /  Какой транспорт изображён на каждом фото?\n"
                  "• What are the passengers doing?  /  Что делают пассажиры?\n"
                  "• Which journey looks more comfortable or adventurous?  /  Какое путешествие выглядит более комфортным или увлекательным?\n"
                  "• Have you ever travelled by plane or long-distance train? Which did you prefer?  /  Вы когда-нибудь путешествовали на самолёте или поезде? Что вам понравилось больше?",
                  S("hqi", fontName=BASE, fontSize=9.5, textColor=C_DGREY, leading=14)),
    ]]
    hqt3 = Table(hq_data_3, colWidths=[W - 60])
    hqt3.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LBLUE),
                               ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
                               ("LEFTPADDING",(0,0),(-1,-1),10),("BOX",(0,0),(-1,-1),1,C_TEAL)]))
    elems.append(spacer(4))
    elems.append(hqt3)

    return elems

# ─── Part 5 – Fun Quiz ────────────────────────────────────────────────────────
def part5_quiz():
    elems = []
    elems.append(PageBreak())
    elems.append(section_header(
        "PART 5  |  FUN QUIZ – WRAP UP!",
        "ЧАСТЬ 5  |  ВЕСЁЛАЯ ВИКТОРИНА – ПОДВОДИМ ИТОГИ!",
        C_GOLD))
    elems.append(spacer(6))

    intro_d = [[
        Paragraph("Let's finish with a quick quiz to see what you remember! Circle the correct answer.",
                  S("qi", fontName=BASE, fontSize=10.5, textColor=C_NAVY, leading=15)),
        Paragraph("Закончим быстрой викториной, чтобы проверить, что вы запомнили! Обведите правильный ответ.",
                  S("qir", fontName=ITAL, fontSize=10, textColor=C_TEAL, leading=14)),
    ]]
    qit = Table(intro_d, colWidths=[W - 60])
    qit.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LYELL),
                              ("TOPPADDING",(0,0),(-1,-1),8),("BOTTOMPADDING",(0,0),(-1,-1),8),
                              ("LEFTPADDING",(0,0),(-1,-1),12),("BOX",(0,0),(-1,-1),1.5,C_GOLD)]))
    elems.append(qit)
    elems.append(spacer(10))

    quiz_qs = [
        (1, C_ORANGE, C_LORANG,
         "Which sentence is correct? / Какое предложение правильное?",
         ["a) I have seen that film yesterday.",
          "b) I saw that film yesterday.  ✅",
          "c) I did see that film yesterday."],
         ["a) Я видел этот фильм вчера. (неправильно)",
          "b) Я видел этот фильм вчера.  ✅",
          "c) Я видел этот фильм вчера. (неправильно)"]),
        (2, C_TEAL, C_LBLUE,
         "What does 'to board a plane' mean? / Что значит 'to board a plane'?",
         ["a) To miss the plane / Опоздать на самолёт",
          "b) To buy a ticket / Купить билет",
          "c) To get on the plane  ✅  /  Сесть в самолёт  ✅"],
         []),
        (3, C_LIME, C_LGRN,
         "Which word means 'very tired'? / Какое слово означает 'очень устал'?",
         ["a) Mindful", "b) Exhausted  ✅", "c) Adventurous"],
         ["a) Осознанный", "b) Измотанный  ✅", "c) Любящий приключения"]),
        (4, C_PURPLE, C_LPURP,
         "Fill in: 'She ________ to Paris twice.'  /  Вставьте: 'Она ________ в Париже дважды.'",
         ["a) went", "b) has been  ✅", "c) was going"],
         ["a) ездила (неправильно здесь)", "b) была / побывала  ✅", "c) ехала (неправильно)"]),
        (5, C_PINK, C_LPURP,
         "Complete: 'In contrast to the first photo, the second photo shows...'  /  Дополните: 'В отличие от первого фото, второе фото показывает...'",
         ["This is an OPEN question – any reasonable answer is correct!",
          "Speak for 30 seconds.  /  Говорите 30 секунд."],
         ["Это ОТКРЫТЫЙ вопрос – любой разумный ответ правильный!",
          "Говорите 30 секунд."]),
    ]

    for qnum, accent, bg, question, options, ru_opts in quiz_qs:
        qhdr = [[Paragraph(f"Question {qnum} / Вопрос {qnum}",
                            S("qhn", fontName=BOLD, fontSize=10, textColor=C_WHITE, leading=13))]]
        qhdrT = Table(qhdr, colWidths=[130])
        qhdrT.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),accent),
                                    ("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),4),
                                    ("LEFTPADDING",(0,0),(-1,-1),10)]))
        elems.append(qhdrT)

        qrows = [[Paragraph(f"❓ {question}", S("qqq", fontName=BOLD, fontSize=11, textColor=accent, leading=15))]]
        for i, opt in enumerate(options):
            qrows.append([Paragraph(f"   {opt}", S("qo", fontName=BASE, fontSize=10, textColor=C_NAVY, leading=13))])
            if ru_opts and i < len(ru_opts):
                qrows.append([Paragraph(f"      {ru_opts[i]}", S("qor", fontName=ITAL, fontSize=9.5, textColor=C_TEAL, leading=13))])

        qt = Table(qrows, colWidths=[W - 60])
        qt.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),bg),
                                 ("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),4),
                                 ("LEFTPADDING",(0,0),(-1,-1),12),("BOX",(0,0),(-1,-1),1.5,accent)]))
        elems.append(qt)
        elems.append(spacer(6))

    # Score guide
    sc_data = [
        [Paragraph("🏆  SCORE GUIDE / ШКАЛА ОЦЕНОК", S("sgh", fontName=BOLD, fontSize=11, textColor=C_WHITE, leading=14))],
        [Table([
            ["5/5 ⭐⭐⭐⭐⭐", "Excellent! / Отлично! – You are ready for B2 challenges!"],
            ["4/5 ⭐⭐⭐⭐", "Very Good! / Очень хорошо! – Keep it up!"],
            ["3/5 ⭐⭐⭐", "Good! / Хорошо! – Solid B1 level. Let's build from here."],
            ["1–2/5 ⭐", "Good start! / Хорошее начало! – We will practise more together."],
        ], colWidths=[120, W - 200],
        style=[
            ("FONTNAME",(0,0),(-1,-1),BASE),("FONTSIZE",(0,0),(-1,-1),10),
            ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),4),
            ("LEFTPADDING",(0,0),(-1,-1),6),("ROWBACKGROUNDS",(0,0),(-1,-1),[C_WHITE, C_LYELL]),
            ("TEXTCOLOR",(0,0),(0,-1),C_ORANGE),("FONTNAME",(0,0),(0,-1),BOLD),
        ])],
    ]
    sct = Table(sc_data, colWidths=[W - 40])
    sct.setStyle(TableStyle([
        ("BACKGROUND",(0,0),(0,0),C_NAVY),
        ("BACKGROUND",(0,1),(0,1),C_WHITE),
        ("TOPPADDING",(0,0),(-1,-1),7),("BOTTOMPADDING",(0,0),(-1,-1),7),
        ("LEFTPADDING",(0,0),(-1,-1),10),("BOX",(0,0),(-1,-1),2,C_NAVY),
    ]))
    elems.append(spacer(4))
    elems.append(sct)
    return elems

# ─── Back Cover ───────────────────────────────────────────────────────────────
def back_cover():
    elems = []
    elems.append(PageBreak())

    bc_data = [[
        Paragraph("🌟  GREAT WORK TODAY!", S("bcm", fontName=BOLD, fontSize=22, textColor=C_GOLD, alignment=TA_CENTER, leading=28)),
        Paragraph("ОТЛИЧНАЯ РАБОТА СЕГОДНЯ!", S("bcmr", fontName=ITAL, fontSize=14, textColor=C_WHITE, alignment=TA_CENTER, leading=18)),
        Spacer(1, 10),
        Paragraph("You have completed your first B1/B1+ English lesson. Keep practising every day – even 15 minutes makes a big difference!",
                  S("bcb", fontName=BASE, fontSize=11, textColor=C_WHITE, alignment=TA_CENTER, leading=16)),
        Paragraph("Вы завершили свой первый урок английского уровня B1/B1+. Продолжайте заниматься каждый день – даже 15 минут имеют большое значение!",
                  S("bcbr", fontName=ITAL, fontSize=10, textColor=C_GOLD, alignment=TA_CENTER, leading=14)),
        Spacer(1, 14),
        Paragraph("📌  MY NOTES / МОИ ЗАМЕТКИ", S("bcn", fontName=BOLD, fontSize=12, textColor=C_GOLD, alignment=TA_CENTER, leading=16)),
        Spacer(1, 8),
    ]]
    bct = Table(bc_data, colWidths=[W - 40])
    bct.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_NAVY),
                              ("TOPPADDING",(0,0),(-1,-1),20),("BOTTOMPADDING",(0,0),(-1,-1),14),
                              ("LEFTPADDING",(0,0),(-1,-1),20),("RIGHTPADDING",(0,0),(-1,-1),20)]))
    elems.append(bct)

    # Note lines
    for _ in range(10):
        elems.append(HRFlowable(width="90%", thickness=0.8, color=colors.HexColor("#BDBDBD"),
                                spaceAfter=12, spaceBefore=2))
        elems.append(Spacer(1, 2))

    elems.append(spacer(8))
    footer_data = [[
        Paragraph("See you at the next lesson! / До встречи на следующем уроке! 👋",
                  S("ft", fontName=BOLD, fontSize=12, textColor=C_TEAL, alignment=TA_CENTER, leading=16)),
    ]]
    ft = Table(footer_data, colWidths=[W - 40])
    ft.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LYELL),
                             ("TOPPADDING",(0,0),(-1,-1),10),("BOTTOMPADDING",(0,0),(-1,-1),10),
                             ("BOX",(0,0),(-1,-1),2,C_TEAL)]))
    elems.append(ft)
    return elems

# ─── Build PDF ────────────────────────────────────────────────────────────────
def build():
    img_dir = "/tmp/workspace/esl-handbook"
    out_path = "/tmp/workspace/esl-handbook/ESL_B1_Trial_Lesson_Handbook.pdf"

    doc = SimpleDocTemplate(
        out_path,
        pagesize=A4,
        leftMargin=20*mm,
        rightMargin=20*mm,
        topMargin=15*mm,
        bottomMargin=15*mm,
        title="ESL B1/B1+ Trial Lesson Handbook",
        author="English Language Academy",
    )

    story = []
    story += cover_page()
    story += part1_level_check()
    story += part2_grammar()
    story += part3_vocabulary()
    story += part4_photos(img_dir)
    story += part5_quiz()
    story += back_cover()

    doc.build(story)
    print(f"PDF created: {out_path}")
    import os
    print(f"File size: {os.path.getsize(out_path):,} bytes")

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

cd /tmp/workspace/esl-handbook && python3 build_handbook.py 2>&1

Running Command

find /usr/share/fonts -name "*.ttf" | grep -i -E "(dejavu|noto|liberation|freemono|ubuntu|open)" | head -20

Running Command

# Check if Noto Sans VF supports Cyrillic, and also find bold/italic variants python3 -c " from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont import os fonts = { 'NotoSans': '/usr/share/fonts/google-noto-vf/NotoSans[wght].ttf', 'NotoSansItal': '/usr/share/fonts/google-noto-vf/NotoSans-Italic[wght].ttf', } for name, path in fonts.items(): if os.path.exists(path): try: pdfmetrics.registerFont(TTFont(name, path)) print(f'OK: {name}') except Exception as e: print(f'FAIL {name}: {e}') else: print(f'NOT FOUND: {path}') " # Also look for any bold variant find /usr/share/fonts -name "*.ttf" 2>/dev/null | head -30

Running Command

# Download Noto Sans Cyrillic fonts for proper Russian rendering python3 -c " import urllib.request, os fonts_to_get = { 'NotoSans-Regular.ttf': 'https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoSans/NotoSans-Regular.ttf', 'NotoSans-Bold.ttf': 'https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoSans/NotoSans-Bold.ttf', 'NotoSans-Italic.ttf': 'https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoSans/NotoSans-Italic.ttf', } for name, url in fonts_to_get.items(): target = f'/tmp/workspace/esl-handbook/{name}' if not os.path.exists(target): try: req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) with urllib.request.urlopen(req, timeout=25) as r, open(target, 'wb') as f: f.write(r.read()) print(f'Got {name}: {os.path.getsize(target)} bytes') except Exception as e: print(f'Failed {name}: {e}') else: print(f'Already have {name}') "

Editing File

~/esl-handbook/build_handbook.py

Successfully replaced 1 instance(s) of the string in '/tmp/workspace/esl-handbook/build_handbook.py'
Running Command

cd /tmp/workspace/esl-handbook && python3 build_handbook.py 2>&1

Running Command

python3 -c " import pdfplumber with pdfplumber.open('/tmp/workspace/esl-handbook/ESL_B1_Trial_Lesson_Handbook.pdf') as pdf: print(f'Pages: {len(pdf.pages)}') for i, page in enumerate(pdf.pages[:3]): text = page.extract_text() or '' snippet = text[:200].replace('\n',' ') print(f'Page {i+1}: {snippet}...') "

Running Command

python3 -c " import pdfplumber with pdfplumber.open('/tmp/workspace/esl-handbook/ESL_B1_Trial_Lesson_Handbook.pdf') as pdf: print(f'Total pages: {len(pdf.pages)}') for i, page in enumerate(pdf.pages): text = (page.extract_text() or '')[:80].replace('\n',' ') print(f' Page {i+1:2d}: {text}') "

Your handbook is ready! Here is your download link:

ESL B1 Trial Lesson Handbook

PDF Document · PDF


What's inside (13 pages, colourful PPT-style design)

Every English sentence has its Russian translation directly underneath it. All section titles appear first in English, then in Russian below.

Page 1 - Cover / Обложка

  • Bold navy + gold welcome banner
  • Bilingual lesson overview table (5 activities, timed)
  • "This handbook belongs to YOU" motivational box

Pages 2-3 - PART 1: Level Check / Проверка уровня (0-10 min)

  • 3 colour-coded question blocks (orange, green, purple) the teacher asks aloud
  • Topics: personal info, free time/travel, learning goals
  • "What the teacher is listening for" grid (vocabulary, grammar, fluency, confidence)

Pages 4-5 - PART 2: Grammar Spotlight / Грамматика (10-20 min)

  • Present Perfect vs. Past Simple - side-by-side comparison table (teal vs. orange columns)
  • Signal words, usage rules, 4 contextual examples
  • 4-question quick practice exercise with answer key

Pages 5-6 - PART 3: Vocabulary Power / Сила слов (20-30 min)

  • 3 themed vocabulary tables (Food, Sport, Travel)
  • 5 words per theme - each with Russian translation + bilingual example sentence

Pages 7-12 - PART 4: Speaking Practice / Речевая практика (30-50 min)

  • Useful language bank (describing, comparing, expressing feelings)
  • All 3 photos you provided, each labelled with captions A and B
    • Photo Set 1: Eating (restaurant vs. at home)
    • Photo Set 2: Exercise (tennis vs. yoga)
    • Photo Set 3: Transport (plane vs. train)
  • Each set has a task instruction + 4 helper discussion questions

Pages 11-12 - PART 5: Fun Quiz / Викторина (50-60 min)

  • 5 engaging questions (grammar, vocabulary, speaking)
  • Score guide with star ratings

Page 13 - Back Cover / Финал

  • Motivational send-off in gold on navy
  • 10 blank note lines for the student to write in

I want you to do some improvemens. I want you to do some improvements , firstly don't write/ mention anywhere in this above mentioned handbook pdf that it's the first lesson or something similar. Secondly the pdf which is already created by you some text of the pdf seems cut from left side and not visible when you open the pdf , please fix that problem. Thirdly,the pdf contains the exercise where you need to find which options are grammatically correct or not , please add some more interesting exercises of course along with Russian translation under every English sentences. The most important is provide answers for all exercises in the pdf at last as answer key. Also please mention the answers of "describing the photos" exercise and most important please mention step by step process how to describe a photo completely by looking at it during Cambridge exam intensive speaking practice part. Please mention and add answers of those 3 intensive speaking practice describing photos in the end in answer key

Reading File
Writing File

~/esl-handbook/build_handbook_v2.py

#!/usr/bin/env python3
"""
ESL B1/B1+ Intermediate Handbook – Improved v2
Bilingual English + Russian, PPT-style colourful PDF
Fixes: wider margins, no 'first lesson' references, more exercises, Cambridge photo guide, full answer key
"""

from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm
from reportlab.lib.styles import ParagraphStyle
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
    HRFlowable, PageBreak, Image as RLImage,
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from PIL import Image as PILImage
import os

# ─── Page geometry ──────────────────────────────────────────────────────────
LM = 22 * mm   # left margin
RM = 22 * mm   # right margin
TM = 14 * mm
BM = 14 * mm
W, H = A4      # 595 x 842 pts
CONTENT_W = W - LM - RM   # usable content width ≈ 507 pts

# ─── Colour palette ──────────────────────────────────────────────────────────
C_NAVY   = colors.HexColor("#1B2A4A")
C_TEAL   = colors.HexColor("#0D8B8B")
C_ORANGE = colors.HexColor("#FF6B35")
C_GOLD   = colors.HexColor("#F5A623")
C_LIME   = colors.HexColor("#4CAF50")
C_PURPLE = colors.HexColor("#7B52AB")
C_PINK   = colors.HexColor("#E91E8C")
C_LBLUE  = colors.HexColor("#E3F4FB")
C_LYELL  = colors.HexColor("#FFF8E1")
C_LGRN   = colors.HexColor("#E8F5E9")
C_LPURP  = colors.HexColor("#F3E5F5")
C_LORANG = colors.HexColor("#FFF3E0")
C_WHITE  = colors.white
C_LGREY  = colors.HexColor("#F5F5F5")
C_DGREY  = colors.HexColor("#424242")
C_RED    = colors.HexColor("#D32F2F")

# ─── Fonts ───────────────────────────────────────────────────────────────────
FONT_DIR = "/tmp/workspace/esl-handbook"
_fl = {}

def _tl(name, path):
    if os.path.exists(path):
        try:
            pdfmetrics.registerFont(TTFont(name, path))
            _fl[name] = True
        except Exception as e:
            print(f"Font error {name}: {e}")

_tl("NS",  os.path.join(FONT_DIR, "NotoSans-Regular.ttf"))
_tl("NSB", os.path.join(FONT_DIR, "NotoSans-Bold.ttf"))
_tl("NSI", os.path.join(FONT_DIR, "NotoSans-Italic.ttf"))

BASE = "NS"  if "NS"  in _fl else "Helvetica"
BOLD = "NSB" if "NSB" in _fl else "Helvetica-Bold"
ITAL = "NSI" if "NSI" in _fl else "Helvetica-Oblique"
print(f"Fonts: {BASE} / {BOLD} / {ITAL}")

# ─── Style factory ───────────────────────────────────────────────────────────
def S(name, font=None, size=10, color=C_NAVY, align=TA_LEFT, lead=None, **kw):
    return ParagraphStyle(
        name,
        fontName=font or BASE,
        fontSize=size,
        textColor=color,
        alignment=align,
        leading=lead or size * 1.4,
        **kw,
    )

# ─── Shortcuts ───────────────────────────────────────────────────────────────
def sp(h=6):  return Spacer(1, h)
def hr(c=C_TEAL, t=1.2): return HRFlowable(width="100%", thickness=t, color=c, spaceAfter=3, spaceBefore=3)

def band(text_en, text_ru, bg, text_color=C_WHITE):
    """Full-width coloured header band."""
    rows = [
        [Paragraph(text_en, S("bhe", font=BOLD, size=15, color=text_color, align=TA_CENTER, lead=20))],
        [Paragraph(text_ru, S("bhr", font=ITAL, size=10, color=text_color, align=TA_CENTER, lead=14))],
    ]
    t = Table(rows, colWidths=[CONTENT_W])
    t.setStyle(TableStyle([
        ("BACKGROUND",   (0,0),(-1,-1), bg),
        ("TOPPADDING",   (0,0),(-1,-1), 9),
        ("BOTTOMPADDING",(0,0),(-1,-1), 9),
        ("LEFTPADDING",  (0,0),(-1,-1), 14),
        ("RIGHTPADDING", (0,0),(-1,-1), 14),
    ]))
    return t

def box(rows_data, bg, border=None, col_w=None):
    """Coloured content box."""
    cw = col_w or [CONTENT_W]
    t = Table(rows_data, colWidths=cw)
    cmds = [
        ("BACKGROUND",   (0,0),(-1,-1), bg),
        ("TOPPADDING",   (0,0),(-1,-1), 6),
        ("BOTTOMPADDING",(0,0),(-1,-1), 5),
        ("LEFTPADDING",  (0,0),(-1,-1), 11),
        ("RIGHTPADDING", (0,0),(-1,-1), 11),
        ("VALIGN",       (0,0),(-1,-1), "TOP"),
    ]
    if border:
        cmds.append(("BOX",(0,0),(-1,-1), 1.5, border))
    t.setStyle(TableStyle(cmds))
    return t

def en_ru_box(en, ru, bg=C_LBLUE, border=C_TEAL):
    return box([[Paragraph(en, S("er_e", font=BASE, size=10.5, color=C_NAVY, lead=15)),
                 Paragraph(ru, S("er_r", font=ITAL, size=10,   color=C_TEAL,  lead=14))]], bg, border)

def chip(label, bg, text_w=120):
    t = Table([[Paragraph(label, S("chip", font=BOLD, size=10, color=C_WHITE, lead=13))]], colWidths=[text_w])
    t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),bg),
                            ("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),4),
                            ("LEFTPADDING",(0,0),(-1,-1),10)]))
    return t

def fit_image(path, max_w, max_h):
    try:
        img = PILImage.open(path)
        iw, ih = img.size
        scale = min(max_w / iw, max_h / ih)
        return RLImage(path, width=iw*scale, height=ih*scale)
    except Exception as e:
        print(f"Image error {path}: {e}")
        return sp(10)

# ═══════════════════════════════════════════════════════════════════════════════
#  COVER
# ═══════════════════════════════════════════════════════════════════════════════
def cover_page():
    E = []
    cv = Table([
        [Paragraph("WELCOME TO YOUR ENGLISH LESSON!",
                   S("ct", font=BOLD, size=26, color=C_WHITE, align=TA_CENTER, lead=32))],
        [Paragraph("Добро пожаловать на урок английского языка!",
                   S("ctr", font=ITAL, size=13, color=C_GOLD, align=TA_CENTER, lead=17))],
        [sp(6)],
        [Paragraph("B1 / B1+  Intermediate  |  60-Minute Lesson",
                   S("cs", font=BOLD, size=12, color=C_GOLD, align=TA_CENTER, lead=16))],
        [Paragraph("B1 / B1+  Средний уровень  |  60-минутный урок",
                   S("csr", font=ITAL, size=10, color=C_WHITE, align=TA_CENTER, lead=14))],
    ], colWidths=[CONTENT_W])
    cv.setStyle(TableStyle([
        ("BACKGROUND",(0,0),(-1,-1),C_NAVY),
        ("TOPPADDING",(0,0),(-1,-1),22),("BOTTOMPADDING",(0,0),(-1,-1),22),
        ("LEFTPADDING",(0,0),(-1,-1),16),("RIGHTPADDING",(0,0),(-1,-1),16),
    ]))
    E.append(cv); E.append(sp(12))

    wm = box([
        [Paragraph("Hello! Today you will practise your English skills through conversation, grammar, vocabulary and an exciting speaking activity. Enjoy the lesson!",
                   S("wme", font=BASE, size=11, color=C_NAVY, lead=16))],
        [Paragraph("Привет! Сегодня вы будете совершенствовать свои навыки английского языка через разговор, грамматику, лексику и увлекательное задание на говорение. Приятного урока!",
                   S("wmr", font=ITAL, size=10, color=C_TEAL, lead=14))],
    ], C_LBLUE, C_TEAL)
    E.append(wm); E.append(sp(10))

    oh = Table([[Paragraph("LESSON OVERVIEW / ПЛАН УРОКА",
                            S("ohh", font=BOLD, size=13, color=C_WHITE, align=TA_CENTER, lead=16))]],
               colWidths=[CONTENT_W])
    oh.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_ORANGE),
                             ("TOPPADDING",(0,0),(-1,-1),7),("BOTTOMPADDING",(0,0),(-1,-1),7)]))
    E.append(oh)

    ov_rows = [
        [Paragraph("Time / Время", S("ovh", font=BOLD, size=10, color=C_WHITE, lead=13)),
         Paragraph("Activity / Деятельность", S("ovh2", font=BOLD, size=10, color=C_WHITE, lead=13))],
        ["0-10 min",  "Level Check – Warm-up Interview\nПроверка уровня – Разминочное интервью"],
        ["10-20 min", "Grammar Spotlight: Present Perfect vs. Past Simple\nГрамматика: Present Perfect и Past Simple"],
        ["20-30 min", "Vocabulary Power: Food, Sport & Travel\nСловарный запас: Еда, спорт и путешествия"],
        ["30-50 min", "Speaking Practice: Describe & Compare Photos\nРечевая практика: Описание и сравнение фото"],
        ["50-60 min", "Exercises + Wrap-Up\nУпражнения + Подведение итогов"],
    ]
    ot = Table(ov_rows, colWidths=[CONTENT_W*0.22, CONTENT_W*0.78])
    ot.setStyle(TableStyle([
        ("BACKGROUND",    (0,0),(1,0),  C_NAVY),
        ("TEXTCOLOR",     (0,0),(1,0),  C_WHITE),
        ("FONTNAME",      (0,0),(1,0),  BOLD),
        ("FONTSIZE",      (0,0),(1,0),  10),
        ("ROWBACKGROUNDS",(0,1),(-1,-1),[C_LYELL, C_LGRN, C_LYELL, C_LGRN, C_LYELL]),
        ("FONTNAME",      (0,1),(0,-1), BOLD),
        ("FONTSIZE",      (0,1),(-1,-1),10),
        ("TEXTCOLOR",     (0,1),(0,-1), C_ORANGE),
        ("TEXTCOLOR",     (1,1),(1,-1), C_NAVY),
        ("TOPPADDING",    (0,0),(-1,-1),6),("BOTTOMPADDING",(0,0),(-1,-1),6),
        ("LEFTPADDING",   (0,0),(-1,-1),8),("RIGHTPADDING",(0,0),(-1,-1),8),
        ("GRID",          (0,0),(-1,-1),0.5, colors.HexColor("#BDBDBD")),
        ("VALIGN",        (0,0),(-1,-1),"MIDDLE"),
        ("ALIGN",         (0,0),(0,-1),"CENTER"),
    ]))
    E.append(ot); E.append(sp(12))

    tip = box([
        [Paragraph("This handbook is yours. Write notes in it and bring it to every lesson!",
                   S("t1", font=ITAL, size=10, color=C_WHITE, lead=14))],
        [Paragraph("Этот справочник ваш. Делайте в нём заметки и берите его на каждый урок!",
                   S("t2", font=ITAL, size=10, color=C_GOLD, lead=13))],
    ], C_PURPLE)
    E.append(tip)
    return E

# ═══════════════════════════════════════════════════════════════════════════════
#  PART 1 – LEVEL CHECK
# ═══════════════════════════════════════════════════════════════════════════════
def part1():
    E = [PageBreak()]
    E.append(band("PART 1  |  LEVEL CHECK – WHO ARE YOU?",
                  "ЧАСТЬ 1  |  ПРОВЕРКА УРОВНЯ – КТО ВЫ?", C_TEAL))
    E.append(sp(8))
    E.append(box([
        [Paragraph("Your teacher will ask you these questions. Speak naturally – there are no wrong answers!",
                   S("p1i", font=BASE, size=10.5, color=C_NAVY, lead=15))],
        [Paragraph("Учитель задаст вам эти вопросы. Говорите естественно – неправильных ответов нет!",
                   S("p1ir", font=ITAL, size=10, color=C_PURPLE, lead=14))],
    ], C_LPURP, C_PURPLE))
    E.append(sp(10))

    qs = [
        ("A", C_ORANGE, C_LORANG,
         "Tell me about yourself.",
         "Расскажите о себе.",
         ["What is your name? Where are you from?",
          "What do you do? (job / studies)",
          "How long have you been learning English?"],
         ["Как вас зовут? Откуда вы?",
          "Чем вы занимаетесь? (работа / учёба)",
          "Как давно вы изучаете английский язык?"]),
        ("B", C_LIME, C_LGRN,
         "Tell me about your free time and interests.",
         "Расскажите о своём свободном времени и интересах.",
         ["What do you usually do at weekends?",
          "What hobbies or sports do you enjoy?",
          "Have you ever travelled abroad? Where did you go?"],
         ["Что вы обычно делаете по выходным?",
          "Какие у вас увлечения или каким спортом вы занимаетесь?",
          "Вы когда-нибудь путешествовали за границу? Куда вы ездили?"]),
        ("C", C_PURPLE, C_LPURP,
         "What do you want from your English lessons?",
         "Чего вы хотите от уроков английского?",
         ["Why are you learning English?",
          "What is difficult for you in English?",
          "What is your goal – business, travel or general communication?"],
         ["Почему вы учите английский?",
          "Что вам трудно даётся в английском?",
          "Какова ваша цель – бизнес, путешествия или общение?"]),
    ]

    for letter, accent, bg, eq, rq, esubs, rsubs in qs:
        E.append(chip(f"Question {letter} / Вопрос {letter}", accent, 170))
        rows = [
            [Paragraph(f"  {eq}", S(f"qq{letter}", font=BOLD, size=11.5, color=accent, lead=15))],
            [Paragraph(f"  {rq}", S(f"qqr{letter}", font=ITAL, size=10, color=C_DGREY, lead=14))],
        ]
        for i in range(len(esubs)):
            rows.append([Paragraph(f"     • {esubs[i]}", S(f"qs{letter}{i}", font=BASE, size=10, color=C_NAVY, lead=13))])
            rows.append([Paragraph(f"        {rsubs[i]}", S(f"qsr{letter}{i}", font=ITAL, size=9.5, color=C_TEAL, lead=13))])
        t = Table(rows, colWidths=[CONTENT_W])
        t.setStyle(TableStyle([
            ("BACKGROUND",(0,0),(-1,-1),bg),
            ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),4),
            ("LEFTPADDING",(0,0),(-1,-1),10),("RIGHTPADDING",(0,0),(-1,-1),10),
            ("BOX",(0,0),(-1,-1),1.5,accent),
        ]))
        E.append(t); E.append(sp(8))

    lt = Table([
        [Paragraph("What is your teacher listening for? / Что проверяет учитель?",
                   S("lh", font=BOLD, size=11, color=C_WHITE, lead=14))],
        [Table([
            [Paragraph("Vocabulary range / Словарный запас", S("lv1", font=BASE, size=10, color=C_NAVY, lead=13)),
             Paragraph("Sentence structure / Структура предложений", S("lv2", font=BASE, size=10, color=C_NAVY, lead=13))],
            [Paragraph("Grammar accuracy / Точность грамматики", S("lv3", font=BASE, size=10, color=C_NAVY, lead=13)),
             Paragraph("Confidence & fluency / Уверенность и беглость", S("lv4", font=BASE, size=10, color=C_NAVY, lead=13))],
        ], colWidths=[CONTENT_W/2 - 12, CONTENT_W/2 - 12],
        style=[("BACKGROUND",(0,0),(-1,-1),C_LGREY),
               ("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),3),
               ("LEFTPADDING",(0,0),(-1,-1),5)])],
    ], colWidths=[CONTENT_W])
    lt.setStyle(TableStyle([
        ("BACKGROUND",(0,0),(0,0),C_TEAL),
        ("BACKGROUND",(0,1),(0,1),C_LGREY),
        ("TOPPADDING",(0,0),(-1,-1),8),("BOTTOMPADDING",(0,0),(-1,-1),8),
        ("LEFTPADDING",(0,0),(-1,-1),12),("BOX",(0,0),(-1,-1),1.5,C_TEAL),
    ]))
    E.append(lt)
    return E

# ═══════════════════════════════════════════════════════════════════════════════
#  PART 2 – GRAMMAR SPOTLIGHT
# ═══════════════════════════════════════════════════════════════════════════════
def part2():
    E = [PageBreak()]
    E.append(band("PART 2  |  GRAMMAR SPOTLIGHT",
                  "ЧАСТЬ 2  |  ГРАММАТИКА: В ЦЕНТРЕ ВНИМАНИЯ", C_ORANGE))
    E.append(sp(6))

    # Title
    th = Table([
        [Paragraph("Present Perfect  vs  Past Simple",
                   S("th", font=BOLD, size=16, color=C_NAVY, align=TA_CENTER, lead=20))],
        [Paragraph("Настоящее совершённое время  vs  Прошедшее простое время",
                   S("thr", font=ITAL, size=11, color=C_TEAL, align=TA_CENTER, lead=14))],
    ], colWidths=[CONTENT_W])
    th.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LYELL),
                             ("TOPPADDING",(0,0),(-1,-1),8),("BOTTOMPADDING",(0,0),(-1,-1),8),
                             ("BOX",(0,0),(-1,-1),2,C_ORANGE)]))
    E.append(th); E.append(sp(8))

    # Comparison table
    half = (CONTENT_W - 2) / 2
    pp = Table([
        [Paragraph("PRESENT PERFECT", S("pph", font=BOLD, size=12, color=C_WHITE, align=TA_CENTER, lead=16)),
         Paragraph("PAST SIMPLE",     S("psh", font=BOLD, size=12, color=C_WHITE, align=TA_CENTER, lead=16))],
        [Paragraph("Настоящее совершённое", S("pphr", font=ITAL, size=9.5, color=C_GOLD, align=TA_CENTER, lead=13)),
         Paragraph("Прошедшее простое",     S("pshr", font=ITAL, size=9.5, color=C_GOLD, align=TA_CENTER, lead=13))],
        [Paragraph("have / has + past participle\nI have visited Paris.\nЯ посещал(а) Париж.",
                   S("ppf", font=BASE, size=10, color=C_NAVY, lead=14)),
         Paragraph("verb + -ed / irregular\nI visited Paris last year.\nЯ посетил(а) Париж в прошлом году.",
                   S("psf", font=BASE, size=10, color=C_NAVY, lead=14))],
        [Paragraph("Use when: No specific time / experience / result matters\nКогда: Нет точного времени / опыт / результат важен",
                   S("ppu", font=ITAL, size=9.5, color=C_TEAL, lead=13)),
         Paragraph("Use when: Specific finished time in the past\nКогда: Конкретное завершённое время в прошлом",
                   S("psu", font=ITAL, size=9.5, color=C_TEAL, lead=13))],
        [Paragraph("Signal words:\nalready, yet, ever, never, just, recently, since, for\nСлова-маркеры: уже, ещё не, когда-либо, никогда, только что, недавно, с тех пор как, в течение",
                   S("ppk", font=BASE, size=9.5, color=C_NAVY, lead=13)),
         Paragraph("Signal words:\nyesterday, last week/year, ago, in 2010, when I was...\nСлова-маркеры: вчера, на прошлой неделе/году, назад, в 2010, когда я был(а)...",
                   S("psk", font=BASE, size=9.5, color=C_NAVY, lead=13))],
    ], colWidths=[half, half])
    pp.setStyle(TableStyle([
        ("BACKGROUND",(0,0),(0,1),C_TEAL),("BACKGROUND",(1,0),(1,1),C_ORANGE),
        ("ROWBACKGROUNDS",(0,2),(-1,-1),[C_LGRN, C_LGREY, C_LYELL]),
        ("TOPPADDING",(0,0),(-1,-1),7),("BOTTOMPADDING",(0,0),(-1,-1),7),
        ("LEFTPADDING",(0,0),(-1,-1),8),("RIGHTPADDING",(0,0),(-1,-1),8),
        ("GRID",(0,0),(-1,-1),1,colors.HexColor("#BDBDBD")),("VALIGN",(0,0),(-1,-1),"TOP"),
    ]))
    E.append(pp); E.append(sp(10))

    # Examples
    E.append(chip("Examples in Context / Примеры в контексте", C_PURPLE, 280))
    for en, ru in [
        ('"Have you ever eaten sushi?"  →  "Yes, I have eaten sushi twice."',
         '«Вы когда-нибудь ели суши?» → «Да, я ел(а) суши дважды.»'),
        ('"I have just finished my homework."',
         '«Я только что закончил(а) домашнее задание.»'),
        ('"Did you watch the match last night?"  →  "Yes, I watched it."',
         '«Ты смотрел(а) матч вчера вечером?» → «Да, я смотрел(а).»'),
        ('"She moved to Moscow in 2019."',
         '«Она переехала в Москву в 2019 году.»'),
    ]:
        E.append(en_ru_box(f"  {en}", f"  {ru}", C_LBLUE, C_TEAL))
        E.append(sp(3))
    return E

# ═══════════════════════════════════════════════════════════════════════════════
#  PART 3 – VOCABULARY
# ═══════════════════════════════════════════════════════════════════════════════
def part3():
    E = [PageBreak()]
    E.append(band("PART 3  |  VOCABULARY POWER",
                  "ЧАСТЬ 3  |  СИЛА СЛОВ", C_PURPLE))
    E.append(sp(6))
    E.append(box([
        [Paragraph("Read, repeat and try to use each word in a sentence!",
                   S("vci", font=BASE, size=10.5, color=C_NAVY, lead=15))],
        [Paragraph("Читайте, повторяйте и попробуйте использовать каждое слово в предложении!",
                   S("vcir", font=ITAL, size=10, color=C_PURPLE, lead=14))],
    ], C_LPURP, C_PURPLE))
    E.append(sp(8))

    categories = [
        ("  FOOD & EATING / ЕДА", C_ORANGE, C_LORANG, [
            ("to dine out",       "есть вне дома / в ресторане",     "We dine out every Friday.\nМы едим вне дома каждую пятницу."),
            ("to eat in",         "есть дома",                        "I prefer to eat in when I'm tired.\nЯ предпочитаю есть дома, когда устала."),
            ("a cosy atmosphere", "уютная атмосфера",                 "The café has a cosy atmosphere.\nВ кафе уютная атмосфера."),
            ("a balanced diet",   "сбалансированное питание",         "A balanced diet keeps you healthy.\nСбалансированное питание сохраняет здоровье."),
            ("to have a bite",    "перекусить",                       "Let's have a bite before the film.\nДавай перекусим перед фильмом."),
        ]),
        ("  SPORT & EXERCISE / СПОРТ", C_LIME, C_LGRN, [
            ("competitive",       "соревновательный",                 "Tennis is a competitive sport.\nТеннис – соревновательный вид спорта."),
            ("to keep fit",       "поддерживать форму",               "I go jogging to keep fit.\nЯ бегаю трусцой, чтобы поддерживать форму."),
            ("exhausted",         "измотанный / очень усталый",        "After the match I was exhausted.\nПосле матча я был(а) измотан(а)."),
            ("mindful",           "осознанный",                       "Yoga is a mindful exercise.\nЙога – осознанная практика."),
            ("to take up a sport","начать заниматься спортом",         "She took up yoga last year.\nОна занялась йогой в прошлом году."),
        ]),
        ("  TRAVEL & TRANSPORT / ПУТЕШЕСТВИЯ", C_TEAL, C_LBLUE, [
            ("to board",          "садиться (в транспорт)",            "We need to board the plane now.\nНам нужно сесть в самолёт сейчас."),
            ("overcast",          "облачный / пасмурный",              "The sky was overcast all morning.\nНебо было пасмурным всё утро."),
            ("backpacker",        "путешественник с рюкзаком",         "As a backpacker she visited 10 countries.\nКак рюкзачный турист она посетила 10 стран."),
            ("adventurous",       "любящий приключения",               "He is very adventurous and loves to travel.\nОн очень любит приключения."),
            ("a crowded queue",   "длинная очередь / толпа",           "There was a crowded queue at the airport.\nВ аэропорту была длинная очередь."),
        ]),
    ]

    for title, accent, bg, words in categories:
        ch = Table([[Paragraph(title, S("ch", font=BOLD, size=12, color=C_WHITE, lead=15))]],
                   colWidths=[CONTENT_W])
        ch.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),accent),
                                 ("TOPPADDING",(0,0),(-1,-1),7),("BOTTOMPADDING",(0,0),(-1,-1),7),
                                 ("LEFTPADDING",(0,0),(-1,-1),12)]))
        E.append(ch)

        c1 = CONTENT_W * 0.22; c2 = CONTENT_W * 0.25; c3 = CONTENT_W * 0.53
        hrow = [
            Paragraph("Word / Слово",    S("wh1", font=BOLD, size=10, color=C_WHITE, lead=13)),
            Paragraph("Russian / Перевод",S("wh2", font=BOLD, size=10, color=C_WHITE, lead=13)),
            Paragraph("Example / Пример",S("wh3", font=BOLD, size=10, color=C_WHITE, lead=13)),
        ]
        wrows = [hrow]
        for en_w, ru_w, ex in words:
            wrows.append([
                Paragraph(en_w, S(f"we{en_w}", font=BOLD, size=10, color=C_NAVY, lead=13)),
                Paragraph(ru_w, S(f"wr{en_w}", font=ITAL, size=10, color=C_PURPLE, lead=13)),
                Paragraph(ex,   S(f"wx{en_w}", font=BASE, size=9,   color=C_DGREY, lead=12)),
            ])
        wt = Table(wrows, colWidths=[c1, c2, c3])
        wt.setStyle(TableStyle([
            ("BACKGROUND",(0,0),(2,0),accent),
            ("ROWBACKGROUNDS",(0,1),(-1,-1),[C_WHITE, bg]),
            ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
            ("LEFTPADDING",(0,0),(-1,-1),7),("RIGHTPADDING",(0,0),(-1,-1),7),
            ("GRID",(0,0),(-1,-1),0.5,colors.HexColor("#BDBDBD")),
            ("VALIGN",(0,0),(-1,-1),"TOP"),
        ]))
        E.append(wt); E.append(sp(8))

    return E

# ═══════════════════════════════════════════════════════════════════════════════
#  PART 4 – CAMBRIDGE PHOTO SPEAKING
# ═══════════════════════════════════════════════════════════════════════════════
def part4(img_dir):
    E = [PageBreak()]
    E.append(band("PART 4  |  SPEAKING PRACTICE – DESCRIBE & COMPARE PHOTOS",
                  "ЧАСТЬ 4  |  РЕЧЕВАЯ ПРАКТИКА – ОПИСАНИЕ И СРАВНЕНИЕ ФОТО", C_PINK))
    E.append(sp(6))

    # ── Cambridge Step-by-Step Guide ─────────────────────────────────────────
    sg_hdr = Table([[Paragraph("HOW TO DESCRIBE A PHOTO – Cambridge Exam Step-by-Step Guide",
                                S("sgh", font=BOLD, size=13, color=C_WHITE, align=TA_CENTER, lead=17))],
                    [Paragraph("КАК ОПИСАТЬ ФОТОГРАФИЮ – Пошаговое руководство для экзамена Cambridge",
                                S("sghr", font=ITAL, size=10, color=C_GOLD, align=TA_CENTER, lead=14))]],
                   colWidths=[CONTENT_W])
    sg_hdr.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_NAVY),
                                 ("TOPPADDING",(0,0),(-1,-1),9),("BOTTOMPADDING",(0,0),(-1,-1),9),
                                 ("LEFTPADDING",(0,0),(-1,-1),12)]))
    E.append(sg_hdr); E.append(sp(4))

    steps = [
        ("STEP 1", C_TEAL,
         "OVERVIEW – What is the general topic?",
         "ШАГ 1: ОБЩИЙ ПЛАН – Какова общая тема?",
         "This photo shows... / In this photograph I can see...\nStart with one sentence that names the main topic.",
         "«Это фото показывает...» / «На этой фотографии я вижу...»\nНачните с одного предложения, называющего главную тему."),
        ("STEP 2", C_ORANGE,
         "SETTING – Where and when?",
         "ШАГ 2: МЕСТО ДЕЙСТВИЯ – Где и когда?",
         "It looks like the photo was taken... (indoors/outdoors, in a restaurant/park, etc.)\nDescribe the background and location.",
         "«Похоже, фото было сделано...» (в помещении / на улице, в ресторане / парке и т.д.)\nОпишите фон и место."),
        ("STEP 3", C_LIME,
         "PEOPLE – Who is in the photo?",
         "ШАГ 3: ЛЮДИ – Кто на фото?",
         "In the foreground/background there is/are... They appear to be... (age, clothing, expression).",
         "«На переднем/заднем плане есть...» Они выглядят... (возраст, одежда, выражение лица)."),
        ("STEP 4", C_PURPLE,
         "ACTION – What is happening?",
         "ШАГ 4: ДЕЙСТВИЕ – Что происходит?",
         "They are + -ing... / It seems like they are about to... / It looks as if...",
         "«Они + глагол-ing...» / «Кажется, они собираются...» / «Похоже, что...»"),
        ("STEP 5", C_PINK,
         "ATMOSPHERE & FEELINGS – How does it feel?",
         "ШАГ 5: АТМОСФЕРА И ЧУВСТВА – Какова атмосфера?",
         "The atmosphere seems... (lively/relaxed/tense). The person looks... (happy/excited/tired).",
         "«Атмосфера кажется...» (оживлённой/расслабленной/напряжённой). «Человек выглядит...» (счастливым/взволнованным/усталым)."),
        ("STEP 6", C_GOLD,
         "COMPARE – How are the two photos different/similar?",
         "ШАГ 6: СРАВНЕНИЕ – Чем фото похожи или отличаются?",
         "Both photos show... / While in the first photo..., in the second... / The main difference is that... / In contrast to...",
         "«Оба фото показывают...» / «В то время как на первом фото..., на втором...» / «Главное отличие состоит в том, что...» / «В отличие от...»"),
        ("STEP 7", C_RED,
         "PERSONAL OPINION – What do you think?",
         "ШАГ 7: ЛИЧНОЕ МНЕНИЕ – Что вы думаете?",
         "Personally, I think... / In my opinion... / I believe... / I would prefer... because...",
         "«Лично я думаю...» / «По моему мнению...» / «Я считаю...» / «Я бы предпочёл(а)... потому что...»"),
    ]

    for step_lbl, accent, en_title, ru_title, en_body, ru_body in steps:
        step_row = Table([
            [chip(step_lbl, accent, 75),
             Paragraph(en_title, S(f"st_e_{step_lbl}", font=BOLD, size=11, color=accent, lead=14)),
             Paragraph(ru_title, S(f"st_r_{step_lbl}", font=ITAL, size=10, color=C_DGREY, lead=13))],
        ], colWidths=[80, CONTENT_W*0.45, CONTENT_W*0.42])
        step_row.setStyle(TableStyle([
            ("BACKGROUND",(0,0),(-1,-1),C_LGREY),
            ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
            ("LEFTPADDING",(0,0),(-1,-1),3),("VALIGN",(0,0),(-1,-1),"MIDDLE"),
        ]))
        body_row = Table([
            [Paragraph(f"  {en_body}", S(f"sb_e_{step_lbl}", font=BASE, size=10, color=C_NAVY, lead=14)),
             Paragraph(f"  {ru_body}", S(f"sb_r_{step_lbl}", font=ITAL, size=9.5, color=C_TEAL, lead=13))],
        ], colWidths=[CONTENT_W*0.5, CONTENT_W*0.5])
        body_row.setStyle(TableStyle([
            ("BACKGROUND",(0,0),(-1,-1),C_WHITE),
            ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
            ("LEFTPADDING",(0,0),(-1,-1),5),("RIGHTPADDING",(0,0),(-1,-1),5),
            ("BOX",(0,0),(-1,-1),0.8,accent),
            ("VALIGN",(0,0),(-1,-1),"TOP"),
        ]))
        E.append(step_row); E.append(body_row); E.append(sp(4))

    E.append(sp(8))

    # ── Useful Language Bank ─────────────────────────────────────────────────
    E.append(chip("USEFUL LANGUAGE / ПОЛЕЗНЫЕ ВЫРАЖЕНИЯ", C_NAVY, CONTENT_W))
    phrase_groups = [
        ("Describing / Описание", [
            "In the left/right photo I can see... – На левом/правом фото я вижу...",
            "It looks like... / It seems that... – Похоже на... / Кажется, что...",
            "In the foreground/background there is/are... – На переднем/заднем плане...",
        ]),
        ("Comparing / Сравнение", [
            "Both photos show... – На обоих фото изображено...",
            "While in the first photo..., in the second... – В то время как на первом фото..., на втором...",
            "The main difference is that... – Главное отличие состоит в том, что...",
        ]),
        ("Expressing feelings / Выражение чувств", [
            "The atmosphere seems... (lively / relaxed / tense) – Атмосфера кажется... (оживлённой/расслабленной/напряжённой)",
            "The person might feel... (excited / exhausted / happy) – Человек, возможно, чувствует... (взволнованным/усталым/счастливым)",
        ]),
    ]
    for pg_title, phrases in phrase_groups:
        pg_rows = [[Paragraph(pg_title, S(f"pg_{pg_title}", font=BOLD, size=10.5, color=C_NAVY, lead=14))]]
        for ph in phrases:
            pg_rows.append([Paragraph(f"  {ph}", S(f"ph_{ph[:10]}", font=BASE, size=9.5, color=C_DGREY, lead=13))])
        pgt = Table(pg_rows, colWidths=[CONTENT_W])
        pgt.setStyle(TableStyle([
            ("BACKGROUND",(0,0),(0,0),C_LYELL),
            ("BACKGROUND",(0,1),(-1,-1),C_WHITE),
            ("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),3),
            ("LEFTPADDING",(0,0),(-1,-1),10),
            ("BOX",(0,0),(-1,-1),0.8,C_GOLD),
        ]))
        E.append(pgt); E.append(sp(3))

    E.append(sp(10))

    # ── Photo Sets ────────────────────────────────────────────────────────────
    def photo_section(num, topic_en, topic_ru, img_path, cap_a_en, cap_a_ru, cap_b_en, cap_b_ru,
                      task_en, task_ru, helper_en, helper_ru, accent, bg):
        elems = []
        ph_hdr = Table([
            [Paragraph(f"PHOTO SET {num}:  {topic_en}",
                       S(f"psh{num}", font=BOLD, size=13, color=C_WHITE, lead=16))],
            [Paragraph(f"НАБОР ФОТО {num}:  {topic_ru}",
                       S(f"pshr{num}", font=ITAL, size=10, color=C_GOLD, lead=14))],
        ], colWidths=[CONTENT_W])
        ph_hdr.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),accent),
                                     ("TOPPADDING",(0,0),(-1,-1),8),("BOTTOMPADDING",(0,0),(-1,-1),8),
                                     ("LEFTPADDING",(0,0),(-1,-1),12)]))
        elems.append(ph_hdr); elems.append(sp(6))

        img = fit_image(img_path, CONTENT_W, 168)
        it = Table([[img]], colWidths=[CONTENT_W])
        it.setStyle(TableStyle([("ALIGN",(0,0),(-1,-1),"CENTER"),
                                 ("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),4)]))
        elems.append(it); elems.append(sp(4))

        cap = Table([[
            Paragraph(f"Photo A: {cap_a_en}\n{cap_a_ru}",
                      S(f"ca{num}", font=BASE, size=9.5, color=C_NAVY, lead=13)),
            Paragraph(f"Photo B: {cap_b_en}\n{cap_b_ru}",
                      S(f"cb{num}", font=BASE, size=9.5, color=C_NAVY, lead=13)),
        ]], colWidths=[CONTENT_W/2, CONTENT_W/2])
        cap.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),bg),
                                   ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
                                   ("LEFTPADDING",(0,0),(-1,-1),8),("GRID",(0,0),(-1,-1),0.5,accent)]))
        elems.append(cap); elems.append(sp(5))

        task_t = Table([
            [Paragraph(task_en, S(f"te{num}", font=BOLD, size=10.5, color=accent, lead=14))],
            [Paragraph(task_ru, S(f"tr{num}", font=ITAL, size=10,   color=C_DGREY, lead=13))],
        ], colWidths=[CONTENT_W])
        task_t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LGREY),
                                     ("TOPPADDING",(0,0),(-1,-1),7),("BOTTOMPADDING",(0,0),(-1,-1),7),
                                     ("LEFTPADDING",(0,0),(-1,-1),12),("BOX",(0,0),(-1,-1),1.5,accent)]))
        elems.append(task_t); elems.append(sp(4))

        hq = Table([
            [Paragraph("Helper questions / Вспомогательные вопросы:",
                       S(f"hqh{num}", font=BOLD, size=10, color=C_NAVY, lead=13))],
            [Paragraph(helper_en + "\n" + helper_ru,
                       S(f"hqi{num}", font=BASE, size=9.5, color=C_DGREY, lead=14))],
        ], colWidths=[CONTENT_W])
        hq.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),bg),
                                  ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
                                  ("LEFTPADDING",(0,0),(-1,-1),10),("BOX",(0,0),(-1,-1),1,accent)]))
        elems.append(hq)
        return elems

    E += photo_section(
        1, "EATING – Where & How?", "ЕДА – Где и как?",
        os.path.join(img_dir, "eating.jpg"),
        "Two women dining at a restaurant", "Две женщины ужинают в ресторане",
        "A woman eating pasta alone at home", "Женщина ест пасту дома в одиночестве",
        "Describe each photo (1 min each). Then compare: How is eating out different from eating at home? How might each woman feel?",
        "Опишите каждое фото (по 1 мин). Затем сравните: Чем отличается еда вне дома от домашней? Как может чувствовать себя каждая женщина?",
        "• What is each person eating?\n• What is the atmosphere like?\n• Which meal looks more enjoyable and why?\n• Have you ever eaten at a restaurant with friends?",
        "• Что ест каждый человек?\n• Какова атмосфера?\n• Какая еда выглядит приятнее и почему?\n• Вы когда-нибудь ели в ресторане с друзьями?",
        C_ORANGE, C_LORANG)

    E.append(PageBreak())
    E += photo_section(
        2, "EXERCISE – Sport & Wellbeing", "СПОРТ И ЗДОРОВЬЕ",
        os.path.join(img_dir, "exercise.jpg"),
        "A woman playing tennis", "Женщина играет в теннис",
        "A woman doing yoga outdoors", "Женщина занимается йогой на природе",
        "Compare the photographs and say how the exercising might make people feel. (1-2 minutes)",
        "Сравните фотографии и скажите, как занятия спортом могут заставить людей себя чувствовать. (1–2 минуты)",
        "• What sport/exercise is each person doing?\n• How does each person look – calm, tense, focused?\n• Which type of exercise do you prefer and why?\n• Do you think sport is better alone or with others?",
        "• Каким спортом/упражнением занимается каждый человек?\n• Как выглядит каждый человек – спокойно, напряжённо, сосредоточенно?\n• Какой вид упражнений вы предпочитаете и почему?\n• Думаете ли вы, что заниматься спортом лучше в одиночку или с другими?",
        C_LIME, C_LGRN)

    E.append(PageBreak())
    E += photo_section(
        3, "TRAVEL – Public Transport", "ПУТЕШЕСТВИЯ – ОБЩЕСТВЕННЫЙ ТРАНСПОРТ",
        os.path.join(img_dir, "transport.jpg"),
        "Passengers boarding a plane at an airport", "Пассажиры садятся в самолёт в аэропорту",
        "Backpackers waiting to board a train", "Туристы ждут поезда на перроне",
        "Compare the photographs and say how comfortable each type of transport might be. (1-2 minutes)",
        "Сравните фотографии и скажите, насколько комфортным может быть каждый вид транспорта. (1–2 минуты)",
        "• What type of transport is in each photo?\n• What are the passengers doing?\n• Which journey looks more comfortable or adventurous?\n• Have you ever travelled by plane or long-distance train?",
        "• Какой транспорт изображён на каждом фото?\n• Что делают пассажиры?\n• Какое путешествие выглядит комфортнее или увлекательнее?\n• Вы когда-нибудь путешествовали на самолёте или поезде дальнего следования?",
        C_TEAL, C_LBLUE)

    return E

# ═══════════════════════════════════════════════════════════════════════════════
#  PART 5 – EXERCISES
# ═══════════════════════════════════════════════════════════════════════════════
def part5():
    E = [PageBreak()]
    E.append(band("PART 5  |  EXERCISES",
                  "ЧАСТЬ 5  |  УПРАЖНЕНИЯ", C_TEAL))
    E.append(sp(6))
    E.append(box([
        [Paragraph("Complete all exercises. Write your answers in the spaces. Check your answers in the Answer Key at the end!",
                   S("ex_i", font=BASE, size=10.5, color=C_NAVY, lead=15))],
        [Paragraph("Выполните все упражнения. Запишите ответы в пустые строки. Проверьте ответы в ключе в конце!",
                   S("ex_ir", font=ITAL, size=10, color=C_TEAL, lead=14))],
    ], C_LBLUE, C_TEAL))
    E.append(sp(10))

    # ── Exercise 1: Choose the correct tense ──────────────────────────────────
    E.append(chip("EXERCISE 1 – Choose the Correct Tense / Выберите правильное время", C_ORANGE, CONTENT_W))
    instr1 = Table([
        [Paragraph("Circle the correct option (a or b). / Обведите правильный вариант (a или b).",
                   S("e1i", font=ITAL, size=10, color=C_NAVY, lead=14))],
    ], colWidths=[CONTENT_W])
    instr1.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LORANG),
                                 ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
                                 ("LEFTPADDING",(0,0),(-1,-1),10)]))
    E.append(instr1)

    q1_items = [
        ("1.", "I ________ (never / visit) New York.\na)  have never visited     b)  never visited",
               "Я ________ в Нью-Йорке.\na)  никогда не был(а)     b)  никогда не посещал(а)"),
        ("2.", "She ________ (go) to the cinema last Saturday.\na)  has gone     b)  went",
               "Она ________ в кино в прошлую субботу.\na)  пошла     b)  ходила"),
        ("3.", "They ________ (already / eat) dinner.\na)  already ate     b)  have already eaten",
               "Они ________ ужин.\na)  уже поели     b)  уже съели"),
        ("4.", "When ________ you ________ (learn) to drive?\na)  have … learned     b)  did … learn",
               "Когда вы ________ водить машину?\na)  научились     b)  учились"),
        ("5.", "I ________ (not / see) that film yet.\na)  didn't see     b)  haven't seen",
               "Я ещё ________ этот фильм.\na)  не видел(а)     b)  не посмотрел(а)"),
        ("6.", "We ________ in London for three years now.\na)  lived     b)  have lived",
               "Мы ________ в Лондоне уже три года.\na)  жили     b)  живём"),
    ]
    for num, en_q, ru_q in q1_items:
        qr = Table([
            [Paragraph(num, S(f"q1n{num}", font=BOLD, size=11, color=C_ORANGE, lead=14)),
             Paragraph(en_q, S(f"q1e{num}", font=BASE, size=10, color=C_NAVY, lead=13))],
            [Paragraph("", S("_")),
             Paragraph(ru_q, S(f"q1r{num}", font=ITAL, size=9.5, color=C_TEAL, lead=13))],
        ], colWidths=[22, CONTENT_W - 22])
        qr.setStyle(TableStyle([
            ("BACKGROUND",(0,0),(-1,-1),C_LYELL),
            ("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),3),
            ("LEFTPADDING",(0,0),(-1,-1),6),("RIGHTPADDING",(0,0),(-1,-1),8),
            ("VALIGN",(0,0),(-1,-1),"TOP"),("BOX",(0,0),(-1,-1),0.5,C_GOLD),
        ]))
        E.append(qr); E.append(sp(3))

    E.append(sp(8))

    # ── Exercise 2: Fill in the Blank ─────────────────────────────────────────
    E.append(chip("EXERCISE 2 – Fill in the Blank / Вставьте пропущенное слово", C_TEAL, CONTENT_W))
    instr2 = Table([
        [Paragraph("Use the correct word from the box to complete each sentence.",
                   S("e2ie", font=ITAL, size=10, color=C_NAVY, lead=14))],
        [Paragraph("Используйте правильное слово из рамки для завершения каждого предложения.",
                   S("e2ir", font=ITAL, size=10, color=C_TEAL, lead=14))],
    ], colWidths=[CONTENT_W])
    instr2.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LBLUE),
                                  ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
                                  ("LEFTPADDING",(0,0),(-1,-1),10)]))
    E.append(instr2)

    word_box = Table([[Paragraph(
        "Word Box:   exhausted   |   board   |   competitive   |   balanced   |   adventurous   |   overcast\n"
        "Слова:        измотанный  |  садиться  |  соревновательный  |  сбалансированный  |  любящий приключения  |  пасмурный",
        S("wb", font=BOLD, size=10, color=C_NAVY, lead=14))]],
        colWidths=[CONTENT_W])
    word_box.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LPURP),
                                    ("TOPPADDING",(0,0),(-1,-1),7),("BOTTOMPADDING",(0,0),(-1,-1),7),
                                    ("LEFTPADDING",(0,0),(-1,-1),10),("BOX",(0,0),(-1,-1),1.5,C_PURPLE)]))
    E.append(word_box); E.append(sp(4))

    fill_items = [
        ("1.", "After running the marathon, she felt completely __________.",
               "После марафона она чувствовала себя полностью __________."),
        ("2.", "Passengers, please __________ the aircraft through Gate 5.",
               "Пассажиры, пожалуйста, __________ в самолёт через выход 5."),
        ("3.", "Tennis is a very __________ sport – everyone wants to win!",
               "Теннис – очень __________ вид спорта – все хотят выиграть!"),
        ("4.", "Doctors recommend eating a __________ diet with lots of vegetables.",
               "Врачи рекомендуют соблюдать __________ диету с большим количеством овощей."),
        ("5.", "He is very __________ – he loves travelling to unknown places.",
               "Он очень __________ – он любит путешествовать в неизвестные места."),
        ("6.", "The sky was grey and __________ all day.",
               "Небо было серым и __________ весь день."),
    ]
    for num, en_s, ru_s in fill_items:
        fr = Table([
            [Paragraph(num, S(f"f2n{num}", font=BOLD, size=11, color=C_TEAL, lead=14)),
             Paragraph(en_s, S(f"f2e{num}", font=BASE, size=10, color=C_NAVY, lead=13))],
            [Paragraph("", S("_2")),
             Paragraph(ru_s, S(f"f2r{num}", font=ITAL, size=9.5, color=C_TEAL, lead=13))],
        ], colWidths=[22, CONTENT_W - 22])
        fr.setStyle(TableStyle([
            ("BACKGROUND",(0,0),(-1,-1),C_LGREY),
            ("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),3),
            ("LEFTPADDING",(0,0),(-1,-1),6),("RIGHTPADDING",(0,0),(-1,-1),8),
            ("VALIGN",(0,0),(-1,-1),"TOP"),("BOX",(0,0),(-1,-1),0.5,C_TEAL),
        ]))
        E.append(fr); E.append(sp(3))

    E.append(sp(8))

    # ── Exercise 3: Error Correction ─────────────────────────────────────────
    E.append(chip("EXERCISE 3 – Find & Correct the Mistake / Найдите и исправьте ошибку", C_PURPLE, CONTENT_W))
    instr3 = Table([
        [Paragraph("Each sentence has ONE grammar mistake. Find it and write the correct sentence on the line below.",
                   S("e3ie", font=ITAL, size=10, color=C_NAVY, lead=14))],
        [Paragraph("В каждом предложении есть ОДНА грамматическая ошибка. Найдите её и запишите правильное предложение на строке ниже.",
                   S("e3ir", font=ITAL, size=10, color=C_TEAL, lead=14))],
    ], colWidths=[CONTENT_W])
    instr3.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LPURP),
                                  ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
                                  ("LEFTPADDING",(0,0),(-1,-1),10),("BOX",(0,0),(-1,-1),1.5,C_PURPLE)]))
    E.append(instr3); E.append(sp(4))

    error_items = [
        ("1.", "I have seen that film yesterday.",   "Я видел(а) этот фильм вчера."),
        ("2.", "She has lived in Paris since two years.", "Она жила в Париже два года."),
        ("3.", "Did you ever try Japanese food?",    "Ты когда-нибудь пробовал(а) японскую еду?"),
        ("4.", "He didn't finish his homework yet.", "Он ещё не закончил домашнее задание."),
        ("5.", "We was exhausted after the hike.",   "Мы были измотаны после похода."),
    ]
    for num, wrong_en, wrong_ru in error_items:
        er = Table([
            [Paragraph(num, S(f"er3n{num}", font=BOLD, size=11, color=C_PURPLE, lead=14)),
             Paragraph(f"WRONG: {wrong_en}", S(f"er3e{num}", font=BASE, size=10, color=C_RED, lead=13))],
            [Paragraph("", S("_3")),
             Paragraph(f"Неверно: {wrong_ru}", S(f"er3r{num}", font=ITAL, size=9.5, color=C_TEAL, lead=13))],
            [Paragraph("", S("_3b")),
             Paragraph("CORRECT: _________________________________________________",
                        S(f"er3c{num}", font=BASE, size=10, color=C_NAVY, lead=14))],
        ], colWidths=[22, CONTENT_W - 22])
        er.setStyle(TableStyle([
            ("BACKGROUND",(0,0),(-1,-1),C_LGRN),
            ("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),3),
            ("LEFTPADDING",(0,0),(-1,-1),6),("RIGHTPADDING",(0,0),(-1,-1),8),
            ("VALIGN",(0,0),(-1,-1),"TOP"),("BOX",(0,0),(-1,-1),0.5,C_PURPLE),
        ]))
        E.append(er); E.append(sp(3))

    E.append(sp(8))

    # ── Exercise 4: Match the Sentence Halves ─────────────────────────────────
    E.append(chip("EXERCISE 4 – Match the Sentence Halves / Соедините части предложений", C_GOLD, CONTENT_W))
    instr4 = Table([
        [Paragraph("Draw a line to match the beginning (1-5) with the correct ending (A-E).",
                   S("e4ie", font=ITAL, size=10, color=C_NAVY, lead=14))],
        [Paragraph("Проведите линию, соединяя начало (1–5) с правильным окончанием (A–E).",
                   S("e4ir", font=ITAL, size=10, color=C_TEAL, lead=14))],
    ], colWidths=[CONTENT_W])
    instr4.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LYELL),
                                  ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
                                  ("LEFTPADDING",(0,0),(-1,-1),10),("BOX",(0,0),(-1,-1),1.5,C_GOLD)]))
    E.append(instr4); E.append(sp(4))

    left_col = [
        "1.  I have lived in this city\n    Я живу в этом городе",
        "2.  She went to the doctor\n    Она ходила к врачу",
        "3.  Have you ever eaten\n    Вы когда-нибудь ели",
        "4.  We boarded the plane\n    Мы сели в самолёт",
        "5.  He has been learning\n    Он изучает",
    ]
    right_col = [
        "A.  Japanese food?\n    японскую еду?",
        "B.  just in time for departure.\n    как раз перед вылетом.",
        "C.  for over ten years.\n    уже более десяти лет.",
        "D.  English since he was a child.\n    английский с детства.",
        "E.  because she felt ill.\n    потому что она чувствовала себя плохо.",
    ]
    match_rows = []
    for i in range(5):
        match_rows.append([
            Paragraph(left_col[i],  S(f"ml{i}", font=BASE, size=10, color=C_NAVY, lead=13)),
            Paragraph("→",          S(f"ma{i}", font=BOLD, size=14, color=C_GOLD, align=TA_CENTER)),
            Paragraph(right_col[i], S(f"mr{i}", font=BASE, size=10, color=C_NAVY, lead=13)),
        ])
    mt = Table(match_rows, colWidths=[CONTENT_W*0.45, CONTENT_W*0.08, CONTENT_W*0.47])
    mt.setStyle(TableStyle([
        ("ROWBACKGROUNDS",(0,0),(-1,-1),[C_LYELL, C_WHITE]),
        ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
        ("LEFTPADDING",(0,0),(-1,-1),8),("RIGHTPADDING",(0,0),(-1,-1),8),
        ("VALIGN",(0,0),(-1,-1),"TOP"),("GRID",(0,0),(-1,-1),0.5,colors.HexColor("#BDBDBD")),
    ]))
    E.append(mt); E.append(sp(8))

    # ── Exercise 5: Make a sentence ──────────────────────────────────────────
    E.append(chip("EXERCISE 5 – Put the Words in Order / Расставьте слова в правильном порядке", C_PINK, CONTENT_W))
    instr5 = Table([
        [Paragraph("Rearrange the words to make a correct sentence. Write your answer on the line.",
                   S("e5ie", font=ITAL, size=10, color=C_NAVY, lead=14))],
        [Paragraph("Составьте из слов правильное предложение. Запишите ответ на строке.",
                   S("e5ir", font=ITAL, size=10, color=C_TEAL, lead=14))],
    ], colWidths=[CONTENT_W])
    instr5.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LPURP),
                                  ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
                                  ("LEFTPADDING",(0,0),(-1,-1),10),("BOX",(0,0),(-1,-1),1.5,C_PINK)]))
    E.append(instr5); E.append(sp(4))

    order_items = [
        ("1.", "[ ever / you / tried / have / sushi / ? ]",
               "[ когда-нибудь / вы / пробовали / суши / ? ]"),
        ("2.", "[ I / three / for / Tokyo / years / lived / have / in ]",
               "[ Я / три / в течение / Токио / лет / жил(а) / в ]"),
        ("3.", "[ she / finished / already / her / has / homework ]",
               "[ она / закончила / уже / своё / домашнее задание ]"),
        ("4.", "[ did / go / you / where / last / summer / ? ]",
               "[ куда / поехал(а) / ты / прошлым / летом / ? ]"),
        ("5.", "[ been / have / I / exhausted / all / feeling / week ]",
               "[ Я / чувствовал(а) / себя / измотанным / всю / неделю ]"),
    ]
    for num, scrambled, ru_hint in order_items:
        or_r = Table([
            [Paragraph(num, S(f"or5n{num}", font=BOLD, size=11, color=C_PINK, lead=14)),
             Paragraph(scrambled, S(f"or5e{num}", font=BOLD, size=10, color=C_PURPLE, lead=13))],
            [Paragraph("", S("_5")),
             Paragraph(f"Подсказка: {ru_hint}", S(f"or5r{num}", font=ITAL, size=9.5, color=C_TEAL, lead=13))],
            [Paragraph("", S("_5b")),
             Paragraph("Answer: _________________________________________________",
                        S(f"or5c{num}", font=BASE, size=10, color=C_NAVY, lead=14))],
        ], colWidths=[22, CONTENT_W - 22])
        or_r.setStyle(TableStyle([
            ("BACKGROUND",(0,0),(-1,-1),C_LGRN),
            ("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),3),
            ("LEFTPADDING",(0,0),(-1,-1),6),("RIGHTPADDING",(0,0),(-1,-1),8),
            ("VALIGN",(0,0),(-1,-1),"TOP"),("BOX",(0,0),(-1,-1),0.5,C_PINK),
        ]))
        E.append(or_r); E.append(sp(3))

    return E

# ═══════════════════════════════════════════════════════════════════════════════
#  ANSWER KEY  (Exercises + Model Photo Answers)
# ═══════════════════════════════════════════════════════════════════════════════
def answer_key():
    E = [PageBreak()]
    E.append(band("ANSWER KEY",
                  "КЛЮЧ ОТВЕТОВ", C_NAVY))
    E.append(sp(8))

    # ── Exercise Answers ─────────────────────────────────────────────────────
    def ans_section(title_en, title_ru, accent, bg, items):
        h = Table([
            [Paragraph(f"  {title_en}", S(f"ah_{title_en[:8]}", font=BOLD, size=12, color=C_WHITE, lead=15))],
            [Paragraph(f"  {title_ru}", S(f"ahr_{title_en[:8]}", font=ITAL, size=10, color=C_GOLD, lead=13))],
        ], colWidths=[CONTENT_W])
        h.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),accent),
                                ("TOPPADDING",(0,0),(-1,-1),7),("BOTTOMPADDING",(0,0),(-1,-1),7)]))
        rows = []
        for num_label, answer_en, answer_ru in items:
            rows.append([
                Paragraph(num_label, S(f"akn{num_label}", font=BOLD, size=11, color=accent, lead=14)),
                Paragraph(answer_en, S(f"ake{num_label}", font=BOLD, size=10.5, color=C_NAVY, lead=14)),
                Paragraph(answer_ru, S(f"akr{num_label}", font=ITAL, size=10, color=C_TEAL, lead=13)),
            ])
        t = Table(rows, colWidths=[30, CONTENT_W*0.52, CONTENT_W*0.42])
        t.setStyle(TableStyle([
            ("ROWBACKGROUNDS",(0,0),(-1,-1),[C_WHITE, bg]),
            ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
            ("LEFTPADDING",(0,0),(-1,-1),7),("RIGHTPADDING",(0,0),(-1,-1),7),
            ("GRID",(0,0),(-1,-1),0.5,colors.HexColor("#BDBDBD")),
            ("VALIGN",(0,0),(-1,-1),"TOP"),
        ]))
        return [h, t, sp(8)]

    ex1_ans = [
        ("1.", "a) have never visited",   "никогда не был(а) – Present Perfect, no specific time"),
        ("2.", "b) went",                 "пошла – Past Simple, specific time: last Saturday"),
        ("3.", "b) have already eaten",   "уже съели – Present Perfect + 'already'"),
        ("4.", "b) did … learn",          "научились – Past Simple question"),
        ("5.", "b) haven't seen",         "не посмотрел(а) – Present Perfect + 'yet'"),
        ("6.", "b) have lived",           "живём – Present Perfect, duration up to now: 'for three years'"),
    ]
    for seg in ans_section("Exercise 1 – Choose the Correct Tense",
                           "Упражнение 1 – Выберите правильное время",
                           C_ORANGE, C_LORANG, ex1_ans):
        E.append(seg)

    ex2_ans = [
        ("1.", "exhausted",    "измотанный"),
        ("2.", "board",        "садиться"),
        ("3.", "competitive",  "соревновательный"),
        ("4.", "balanced",     "сбалансированный"),
        ("5.", "adventurous",  "любящий приключения"),
        ("6.", "overcast",     "пасмурный"),
    ]
    for seg in ans_section("Exercise 2 – Fill in the Blank",
                           "Упражнение 2 – Вставьте пропущенное слово",
                           C_TEAL, C_LBLUE, ex2_ans):
        E.append(seg)

    ex3_ans = [
        ("1.", "I saw that film yesterday.",             "Я видел(а) этот фильм вчера.  (saw not have seen – 'yesterday' = specific past time)"),
        ("2.", "She has lived in Paris for two years.",  "Она прожила в Париже два года.  (for not since)"),
        ("3.", "Have you ever tried Japanese food?",     "Вы когда-нибудь пробовали японскую еду?  (Have ... tried, not Did ... ever)"),
        ("4.", "He hasn't finished his homework yet.",   "Он ещё не закончил домашнее задание.  (hasn't = Present Perfect + yet)"),
        ("5.", "We were exhausted after the hike.",      "Мы были измотаны после похода.  (were not was)"),
    ]
    for seg in ans_section("Exercise 3 – Error Correction",
                           "Упражнение 3 – Исправление ошибок",
                           C_PURPLE, C_LPURP, ex3_ans):
        E.append(seg)

    ex4_ans = [
        ("1 → C", "I have lived in this city for over ten years.",  "Я живу в этом городе уже более десяти лет."),
        ("2 → E", "She went to the doctor because she felt ill.",    "Она ходила к врачу, потому что плохо себя чувствовала."),
        ("3 → A", "Have you ever eaten Japanese food?",              "Вы когда-нибудь ели японскую еду?"),
        ("4 → B", "We boarded the plane just in time for departure.","Мы сели в самолёт как раз перед вылетом."),
        ("5 → D", "He has been learning English since he was a child.","Он изучает английский с детства."),
    ]
    for seg in ans_section("Exercise 4 – Match the Sentence Halves",
                           "Упражнение 4 – Соедините части предложений",
                           C_GOLD, C_LYELL, ex4_ans):
        E.append(seg)

    ex5_ans = [
        ("1.", "Have you ever tried sushi?",                     "Вы когда-нибудь пробовали суши?"),
        ("2.", "I have lived in Tokyo for three years.",          "Я живу в Токио уже три года."),
        ("3.", "She has already finished her homework.",          "Она уже закончила домашнее задание."),
        ("4.", "Where did you go last summer?",                   "Куда ты ездил(а) прошлым летом?"),
        ("5.", "I have been feeling exhausted all week.",         "Я чувствовал(а) себя измотанным(ой) всю неделю."),
    ]
    for seg in ans_section("Exercise 5 – Put the Words in Order",
                           "Упражнение 5 – Расставьте слова в правильном порядке",
                           C_PINK, C_LPURP, ex5_ans):
        E.append(seg)

    # ── Model Answers – Photo Descriptions ──────────────────────────────────
    E.append(PageBreak())
    E.append(band("MODEL ANSWERS – SPEAKING PRACTICE PHOTOS",
                  "ОБРАЗЦОВЫЕ ОТВЕТЫ – РЕЧЕВАЯ ПРАКТИКА (ОПИСАНИЕ ФОТО)", C_PINK))
    E.append(sp(6))
    E.append(box([
        [Paragraph("These are model answers showing what a good B1/B1+ response looks like. Yours does not need to be identical – as long as you cover the key points you will do very well!",
                   S("maint", font=BASE, size=10.5, color=C_NAVY, lead=15))],
        [Paragraph("Это образцовые ответы, показывающие, как должен выглядеть хороший ответ уровня B1/B1+. Ваш ответ не обязан быть идентичным – главное охватить ключевые моменты!",
                   S("maintr", font=ITAL, size=10, color=C_TEAL, lead=14))],
    ], C_LBLUE, C_TEAL))
    E.append(sp(8))

    photo_models = [
        ("PHOTO SET 1 – EATING",
         "НАБОР ФОТО 1 – ЕДА",
         C_ORANGE,
         C_LORANG,
         [
           ("DESCRIBING Photo A (Restaurant) / Описание Фото A (Ресторан)",
            "This photo shows a lively social scene. In the foreground, two young women are sitting at a restaurant table, enjoying a meal together. The woman on the left has dark curly hair and is smiling warmly, while the woman on the right has blonde hair and is eating bruschetta – toasted bread with tomatoes. On the table there is a glass of white wine and a plate of food. The background is softly blurred, showing warm golden lights and other diners, which suggests a popular, busy venue. The atmosphere seems very warm, cheerful and social. The women look happy and relaxed, as if they are having a great time out together.",
            "Это фото показывает оживлённую социальную сцену. На переднем плане две молодые женщины сидят за столом в ресторане и вместе наслаждаются едой. Женщина слева с тёмными вьющимися волосами тепло улыбается, а женщина справа со светлыми волосами ест брускетту – поджаренный хлеб с помидорами. На столе есть бокал белого вина и тарелка с едой. Фон мягко размыт, видны тёплые золотистые огни и другие посетители. Атмосфера кажется очень тёплой, радостной и общительной. Женщины выглядят счастливыми и расслабленными."),
           ("DESCRIBING Photo B (Home) / Описание Фото B (Дома)",
            "The second photo shows a much more casual and private scene. A young woman is sitting on a soft, fluffy white blanket – most likely on her bed or sofa at home. She has her hair up in a messy bun and is wearing a black and white striped long-sleeved top. She is eating spaghetti directly from a black frying pan, which is quite informal and suggests she is very relaxed or simply too tired to use a proper plate. In the background there is a bright window and a small plant on the windowsill, giving a cosy apartment feel. The atmosphere seems peaceful, private and comfortable.",
            "На втором фото изображена гораздо более непринуждённая и личная сцена. Молодая женщина сидит на мягком белом пушистом одеяле – скорее всего, на кровати или диване дома. Её волосы собраны в небрежный пучок, на ней чёрно-белая полосатая кофта с длинными рукавами. Она ест спагетти прямо из чёрной сковороды, что весьма неформально. На заднем плане яркое окно и небольшое растение на подоконнике. Атмосфера кажется спокойной, личной и уютной."),
           ("COMPARING Both Photos / Сравнение двух фото",
            "Both photos show women eating, but the situations are very different. While Photo A shows a social occasion in a restaurant with a lively, cheerful atmosphere, Photo B shows a solitary, informal moment at home. In Photo A the women are dressed nicely and sharing food together with wine, which suggests a special or celebratory occasion. In contrast, Photo B shows someone who is relaxed at home, eating for convenience rather than enjoyment. The main difference is the social versus solitary experience. I think eating out with friends is more enjoyable because you can talk and share experiences, but eating at home is more convenient and relaxing when you are tired.",
            "Оба фото показывают женщин за едой, но ситуации очень разные. В то время как Фото A показывает социальное событие в ресторане с оживлённой, радостной атмосферой, Фото B изображает одинокий, неформальный момент дома. На Фото A женщины красиво одеты и вместе делят еду с вином, что говорит о праздничном поводе. Напротив, Фото B показывает кого-то, кто расслаблен дома и ест ради удобства. Главное отличие – социальный опыт против уединённого. Лично я думаю, что есть с друзьями приятнее, потому что можно общаться."),
         ]),
        ("PHOTO SET 2 – EXERCISE",
         "НАБОР ФОТО 2 – СПОРТ",
         C_LIME,
         C_LGRN,
         [
           ("DESCRIBING Photo A (Tennis) / Описание Фото A (Теннис)",
            "This photo shows a woman playing tennis. She appears to be of East Asian descent and is wearing professional sportswear – a sleeveless striped grey and white tank top, black shorts, white wristbands and a white Adidas visor. She is holding a tennis racket tightly and her facial expression is intense and determined, almost a grimace. This tells us she is in the middle of a competitive game and is exerting a lot of physical effort. The background is a solid blue-green wall, typical of a professional tennis court. The atmosphere feels energetic, intense and competitive.",
            "Это фото показывает женщину, играющую в теннис. Она в профессиональной спортивной одежде – безрукавка с серыми и белыми полосами, чёрные шорты, белые напульсники и белый козырёк Adidas. Она крепко держит теннисную ракетку, а выражение её лица напряжённое и решительное. Это говорит нам о том, что она находится в разгаре соревновательной игры. Атмосфера кажется энергичной, интенсивной и соревновательной."),
           ("DESCRIBING Photo B (Yoga) / Описание Фото B (Йога)",
            "The second photo shows a young woman practising yoga outdoors in a beautiful natural setting. She is performing a deep backbend, arching her spine backwards with her arms stretched and her hands clasped together behind her. She is wearing simple black activewear and is barefoot, which helps her connect with the ground. The setting is stunning – she is standing on a reddish-brown rock next to a calm river or lake, with lush green trees in the background. The overall atmosphere is peaceful, serene and connected to nature. The exercise looks challenging but the woman seems calm and in control.",
            "На втором фото изображена молодая женщина, занимающаяся йогой на открытом воздухе в красивой природной обстановке. Она выполняет глубокий прогиб, выгибая позвоночник назад, с вытянутыми руками и сцепленными ладонями. Она одета в простую чёрную спортивную одежду и босиком. Обстановка потрясающая – она стоит на красновато-коричневой скале рядом со спокойной рекой, на фоне пышных зелёных деревьев. Общая атмосфера мирная, безмятежная и близкая к природе."),
           ("COMPARING Both Photos / Сравнение двух фото",
            "Both photos show women exercising, but the types of exercise and the feelings they create are very different. The tennis player in Photo A looks tense, competitive and physically exhausted. This kind of exercise is high-intensity and fast-paced, and it might make people feel exhilarated and full of adrenaline during the game, but tired and sore afterwards. In contrast, the yoga practitioner in Photo B looks calm, focused and at peace with her surroundings. Yoga is slow, mindful and connected to breathing, so it is likely to make people feel relaxed, centred and refreshed. Personally, I think both types of exercise are valuable – tennis is great for excitement and competition, while yoga is perfect for stress relief and flexibility.",
            "Оба фото показывают занимающихся спортом женщин, но виды упражнений и создаваемые ощущения очень разные. Теннисистка на Фото A выглядит напряжённой, соревновательной и физически изнурённой. Этот вид упражнений высокоинтенсивный, и он может вызывать возбуждение и прилив адреналина во время игры, но усталость после. Напротив, практикующая йогу на Фото B выглядит спокойной и сосредоточенной. Йога медленная и осознанная, поэтому она, скорее всего, заставляет людей чувствовать себя расслабленными и свежими. Лично я думаю, что оба вида упражнений ценны."),
         ]),
        ("PHOTO SET 3 – TRANSPORT",
         "НАБОР ФОТО 3 – ТРАНСПОРТ",
         C_TEAL,
         C_LBLUE,
         [
           ("DESCRIBING Photo A (Aeroplane) / Описание Фото A (Самолёт)",
            "This photo shows a group of passengers boarding a large commercial aeroplane at an airport. The plane is white with blue stripes and Chinese characters on the side. Passengers are queuing up on the tarmac and walking up a covered set of boarding stairs to enter the aircraft. The crowd is diverse and everyone appears to be carrying hand luggage or small backpacks. The sky in the background is overcast and grey. The atmosphere feels routine and slightly crowded – this is the typical experience of air travel, with lots of waiting involved. The passengers look patient but perhaps a little impatient to get on board.",
            "Это фото показывает группу пассажиров, садящихся в большой коммерческий самолёт в аэропорту. Самолёт белый с синими полосами и китайскими иероглифами на борту. Пассажиры выстроились в очередь на взлётно-посадочной полосе и идут по крытому трапу для посадки в самолёт. Небо на заднем плане пасмурное и серое. Атмосфера кажется обыденной и немного переполненной – это типичный опыт воздушного путешествия."),
           ("DESCRIBING Photo B (Train) / Описание Фото B (Поезд)",
            "The second photo shows two young backpackers standing on a railway platform, waiting to board a train. The train carriage behind them is colourful – white on top with a bright yellow stripe and a vivid magenta bottom. A yellow safety line is painted on the edge of the platform. The young man is carrying a very large bright yellow backpack and wearing a red plaid shirt with jeans. The young woman next to him has a grey backpack. Their body language suggests they are relaxed and excited about their journey. The atmosphere feels adventurous, youthful and full of anticipation – this looks like a backpacking holiday rather than a business trip.",
            "На втором фото изображены два молодых рюкзачных туриста, стоящих на железнодорожной платформе в ожидании поезда. Вагон позади них красочный – белый сверху с ярко-жёлтой полосой и насыщенным малиновым низом. Молодой человек несёт очень большой ярко-жёлтый рюкзак и одет в красную клетчатую рубашку с джинсами. Их язык тела говорит о том, что они расслаблены и взволнованы предстоящим путешествием. Атмосфера кажется авантюрной, молодёжной и полной предвкушения."),
           ("COMPARING Both Photos / Сравнение двух фото",
            "Both photos show people using public transport, but the experiences feel very different. Photo A shows air travel, which tends to be faster but also more stressful – with crowded queues, strict security checks and a grey, cloudy atmosphere. In contrast, Photo B shows train travel, which seems much more relaxed and flexible. The backpackers in Photo B appear to be on a leisure trip and are enjoying the experience, whereas the airport passengers in Photo A look like they are simply trying to get to their destination efficiently. In terms of comfort, I think train travel can be more comfortable for shorter distances because you can move around freely, but for long international distances, flying is generally faster and more convenient.",
            "Оба фото показывают людей, пользующихся общественным транспортом, но ощущения очень разные. Фото A показывает авиаперелёт, который, как правило, быстрее, но и более напряжённый – с переполненными очередями и пасмурной атмосферой. Напротив, Фото B показывает поездку на поезде, которая кажется гораздо более расслабленной и гибкой. Рюкзачные туристы на Фото B, судя по всему, путешествуют с удовольствием, тогда как пассажиры аэропорта на Фото A просто стараются добраться до пункта назначения. С точки зрения комфорта, я думаю, что поездка на поезде может быть более комфортной на небольшие расстояния, а для длинных международных перелётов самолёт обычно быстрее."),
         ]),
    ]

    for title_en, title_ru, accent, bg, sub_items in photo_models:
        ph_hdr = Table([
            [Paragraph(f"  {title_en}", S(f"pma_{title_en[:8]}", font=BOLD, size=13, color=C_WHITE, lead=16))],
            [Paragraph(f"  {title_ru}", S(f"pmar_{title_en[:8]}", font=ITAL, size=10, color=C_GOLD, lead=14))],
        ], colWidths=[CONTENT_W])
        ph_hdr.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),accent),
                                     ("TOPPADDING",(0,0),(-1,-1),8),("BOTTOMPADDING",(0,0),(-1,-1),8),
                                     ("LEFTPADDING",(0,0),(-1,-1),12)]))
        E.append(ph_hdr); E.append(sp(4))

        for sub_title, en_ans, ru_ans in sub_items:
            st = Table([[Paragraph(sub_title, S(f"st_{sub_title[:8]}", font=BOLD, size=10.5, color=accent, lead=14))]],
                       colWidths=[CONTENT_W])
            st.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),bg),
                                     ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
                                     ("LEFTPADDING",(0,0),(-1,-1),10)]))
            E.append(st)

            ans_t = Table([
                [Paragraph(en_ans, S(f"pa_e_{sub_title[:6]}", font=BASE, size=10, color=C_NAVY, lead=15))],
                [Paragraph(ru_ans, S(f"pa_r_{sub_title[:6]}", font=ITAL, size=9.5, color=C_TEAL, lead=14))],
            ], colWidths=[CONTENT_W])
            ans_t.setStyle(TableStyle([
                ("BACKGROUND",(0,0),(-1,-1),C_WHITE),
                ("TOPPADDING",(0,0),(-1,-1),6),("BOTTOMPADDING",(0,0),(-1,-1),6),
                ("LEFTPADDING",(0,0),(-1,-1),12),("RIGHTPADDING",(0,0),(-1,-1),12),
                ("BOX",(0,0),(-1,-1),1,accent),
            ]))
            E.append(ans_t); E.append(sp(5))

        E.append(sp(6))

    return E

# ═══════════════════════════════════════════════════════════════════════════════
#  BACK COVER
# ═══════════════════════════════════════════════════════════════════════════════
def back_cover():
    E = [PageBreak()]
    bc = Table([
        [Paragraph("GREAT WORK TODAY!",
                   S("bcm", font=BOLD, size=22, color=C_GOLD, align=TA_CENTER, lead=28))],
        [Paragraph("ОТЛИЧНАЯ РАБОТА СЕГОДНЯ!",
                   S("bcmr", font=ITAL, size=14, color=C_WHITE, align=TA_CENTER, lead=18))],
        [sp(10)],
        [Paragraph("Keep practising every day – even 15 minutes makes a big difference!",
                   S("bcb", font=BASE, size=11, color=C_WHITE, align=TA_CENTER, lead=16))],
        [Paragraph("Продолжайте заниматься каждый день – даже 15 минут имеют большое значение!",
                   S("bcbr", font=ITAL, size=10, color=C_GOLD, align=TA_CENTER, lead=14))],
        [sp(14)],
        [Paragraph("MY NOTES / МОИ ЗАМЕТКИ",
                   S("bcn", font=BOLD, size=12, color=C_GOLD, align=TA_CENTER, lead=16))],
        [sp(8)],
    ], colWidths=[CONTENT_W])
    bc.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_NAVY),
                             ("TOPPADDING",(0,0),(-1,-1),20),("BOTTOMPADDING",(0,0),(-1,-1),14),
                             ("LEFTPADDING",(0,0),(-1,-1),20),("RIGHTPADDING",(0,0),(-1,-1),20)]))
    E.append(bc)
    for _ in range(12):
        E.append(HRFlowable(width="90%", thickness=0.8, color=colors.HexColor("#BDBDBD"),
                            spaceAfter=14, spaceBefore=2))
        E.append(sp(2))
    E.append(sp(8))
    ft = Table([[Paragraph("See you at the next lesson! / До встречи на следующем уроке!",
                            S("ft", font=BOLD, size=12, color=C_TEAL, align=TA_CENTER, lead=16))]],
               colWidths=[CONTENT_W])
    ft.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LYELL),
                             ("TOPPADDING",(0,0),(-1,-1),10),("BOTTOMPADDING",(0,0),(-1,-1),10),
                             ("BOX",(0,0),(-1,-1),2,C_TEAL)]))
    E.append(ft)
    return E

# ═══════════════════════════════════════════════════════════════════════════════
#  BUILD
# ═══════════════════════════════════════════════════════════════════════════════
def build():
    img_dir = "/tmp/workspace/esl-handbook"
    out = "/tmp/workspace/esl-handbook/ESL_B1_Handbook_v2.pdf"

    doc = SimpleDocTemplate(
        out,
        pagesize=A4,
        leftMargin=LM,
        rightMargin=RM,
        topMargin=TM,
        bottomMargin=BM,
        title="ESL B1/B1+ English Lesson Handbook",
        author="English Language Academy",
    )

    story = (cover_page()
             + part1()
             + part2()
             + part3()
             + part4(img_dir)
             + part5()
             + answer_key()
             + back_cover())

    doc.build(story)
    size = os.path.getsize(out)
    print(f"PDF saved: {out}  ({size:,} bytes)")

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

cd /tmp/workspace/esl-handbook && python3 build_handbook_v2.py 2>&1

Running Command

python3 -c " import pdfplumber with pdfplumber.open('/tmp/workspace/esl-handbook/ESL_B1_Handbook_v2.pdf') as pdf: print(f'Total pages: {len(pdf.pages)}') for i, page in enumerate(pdf.pages): text = (page.extract_text() or '')[:90].replace('\n',' ') print(f' Page {i+1:2d}: {text}') "

Running Command

python3 -c " import pdfplumber with pdfplumber.open('/tmp/workspace/esl-handbook/ESL_B1_Handbook_v2.pdf') as pdf: full = ' '.join((p.extract_text() or '') for p in pdf.pages).lower() checks = ['first lesson', 'trial', 'first class', 'today is your first'] for phrase in checks: count = full.count(phrase) print(f'{repr(phrase):35s} occurrences: {count}') print() # confirm key new sections exist keys = ['answer key', 'exercise 1', 'exercise 5', 'step 1', 'step 7', 'model answer', 'photo set 3'] for k in keys: print(f'{repr(k):35s} found: {k in full}') "

Your improved handbook is ready! Here is your download link:

ESL B1 Handbook v2

PDF Document · PDF


Summary of every change made

1. No more "first lesson" references

Every mention of "first lesson", "trial class", "trial lesson", or similar has been completely removed. The handbook now reads as a general B1/B1+ English lesson guide with no indication of the lesson number.

2. Left-side text cutoff - fixed

Left and right margins were increased from 20 mm to 22 mm each, and all table column widths are now calculated from CONTENT_W (the actual usable page width after margins). Nothing is clipped.

3. Five full exercises added (Part 5)

All with bilingual instructions and Russian translations under every sentence:
  • Exercise 1 - Choose the Correct Tense (6 questions, Present Perfect vs Past Simple)
  • Exercise 2 - Fill in the Blank (6 sentences, using vocabulary words from Part 3)
  • Exercise 3 - Find & Correct the Mistake (5 error-correction sentences with a write-in line)
  • Exercise 4 - Match the Sentence Halves (5 matching pairs)
  • Exercise 5 - Put the Words in Order (5 scrambled sentences with a write-in line)

4. Full Answer Key at the end (pages 16-21)

  • All 5 exercises answered with explanations in both English and Russian
  • Model answers for all 3 photo sets - each including:
    • A full description of Photo A (English + Russian)
    • A full description of Photo B (English + Russian)
    • A full comparison of both photos (English + Russian)

5. Cambridge Exam Step-by-Step Photo Guide added (Part 4)

A 7-step visual guide shows exactly how to approach any Cambridge speaking photo task:
  • Step 1: Overview (general topic)
  • Step 2: Setting (where/when)
  • Step 3: People (who)
  • Step 4: Action (what is happening)
  • Step 5: Atmosphere & feelings
  • Step 6: Compare (similarities/differences)
  • Step 7: Personal opinion
Each step has the English instruction and its Russian translation side by side.
This is a shared conversation. Sign in to Orris to start your own chat.