Files
Documents---Re/CONTEXT.md
2026-06-02 18:48:14 +04:00

24 KiB
Raw Blame History

Basic Web Development Series — Project Context

Drop this file into your project folder and tell Claude Code: "Read CONTEXT.md and continue the project."


What This Project Is

A complete programming education product catalogue being built to sell on Gumroad and Etsy. It covers HTML, CSS, and JavaScript across three learning tiers: reference notes, beginner micro-exercises, and intermediate exercises — each with matching answer sheets. Two strategy documents accompany the catalogue.

All documents are Word (.docx) files generated using Python + python-docx. The Node.js docx library was tried first but produced files Word couldn't open. python-docx 1.2.0 is confirmed working and must be used for all future documents.


Project Folder Structure

Place files like this inside C:\projects\Docs Re\:

C:\projects\Docs Re\
│
├── CONTEXT.md                        ← this file
│
├── scripts\
│   ├── dochelper.py                  ← shared helper library (all generators import this)
│   ├── gen_notes.py                  ← generates 3 fundamentals reference notes
│   ├── gen_strategy.py               ← generates 2 strategy documents
│   ├── gen_exercises.py              ← generates 6 intermediate exercise docs
│   ├── gen_beginner.py               ← generates 6 beginner exercise docs
│   ├── gen_micro_images.py           ← generates 6 PIL preview images for HTML beginner exercises
│   └── gen_images.py                 ← generates 3 PIL preview images for HTML intermediate exercises
│
├── images\
│   ├── bex1.png through bex6.png     ← HTML beginner exercise browser-window previews
│   └── html_ex1_preview.png          ← HTML intermediate exercise previews
│       html_ex2_preview.png
│       html_ex3_preview.png
│
└── output\
    ├── HTML_Fundamentals.docx
    ├── CSS_Fundamentals.docx
    ├── JavaScript_Fundamentals.docx
    ├── HTML_Beginner_Exercises.docx
    ├── HTML_Beginner_Answer_Sheet.docx
    ├── CSS_Beginner_Exercises.docx
    ├── CSS_Beginner_Answer_Sheet.docx
    ├── JavaScript_Beginner_Exercises.docx
    ├── JavaScript_Beginner_Answer_Sheet.docx
    ├── HTML_Exercises.docx
    ├── HTML_Answer_Sheet.docx
    ├── CSS_Exercises.docx
    ├── CSS_Answer_Sheet.docx
    ├── JavaScript_Exercises.docx
    ├── JavaScript_Answer_Sheet.docx
    ├── Monetising_Your_Programming_Knowledge.docx
    └── Gumroad_Sales_Strategy.docx

Note: Update the output paths at the top of each generator script to match wherever you want files saved on your machine.


All 17 Completed Documents

Tier 1 — Reference Notes (3 docs)

File Accent Colour Sections
HTML_Fundamentals.docx Orange #E44D26 What is HTML, Tags & Elements, Lists, Tables, Forms, Semantic HTML, Attributes, Best Practices, Cheat Sheet
CSS_Fundamentals.docx Blue #264DE4 What is CSS, Selectors, Box Model, Typography & Colour, Flexbox, CSS Grid, Positioning, Media Queries, Variables & Animations, Cheat Sheet
JavaScript_Fundamentals.docx Yellow #B8A800 What is JS, Variables & Data Types, Operators, Control Flow, Loops, Functions, Arrays, Objects, DOM Manipulation, Fetch API, Cheat Sheet

Tier 2 — Beginner Micro-Exercises (6 docs)

Each language has an exercises doc (6 exercises × 5 pts = 30 pts) and a matching green answer sheet.

Language Exercises Cover Difficulty Spread
HTML Headings & Paragraphs, Images, Lists, Tables, Hyperlinks, Semantic Layout 3 × First Steps / 3 × Getting There
CSS Colours & Fonts, Box Model, Backgrounds & Links, Flexbox Centring, Hover & Transitions, CSS Grid 3 × First Steps / 3 × Getting There
JavaScript Variables & console.log, if/else, Arrays & for Loop, Functions, DOM Manipulation, Objects 3 × First Steps / 3 × Getting There

HTML beginner exercises include real browser-window preview images (generated by PIL). CSS exercises use inline text preview boxes. JS exercises show expected Console output in green-on-dark code blocks.

Tier 3 — Intermediate Exercises (6 docs)

Each language has 3 exercises worth 10 pts each = 30 pts total, with a matching answer sheet.

Language Exercises
HTML Personal Bio Page, Recipe Page with Table, Contact Form
CSS Blog Post Card Styling, Flexbox Navigation Bar, Responsive CSS Grid
JavaScript Grade Calculator (functions & conditionals), DOM Array Builder, Interactive Quiz App

HTML intermediate exercises include real PIL preview images.

Strategy Documents (2 docs)

File Contents
Monetising_Your_Programming_Knowledge.docx Online courses, tutoring, YouTube/newsletter, selling digital docs, corporate training, recommended starting path, strategy comparison table
Gumroad_Sales_Strategy.docx Full pricing table, bundle structure, Gumroad setup steps, Etsy guidance, product description templates, 8-week launch plan

Pricing Strategy (Finalised)

Individual Products

Product Price
Fundamentals notes (each) £7.00
Beginner exercises + answers (per language) £5.00
Intermediate exercises + answers (per language) £8.00
Monetising guide Free lead magnet or £4.00

Bundles

Bundle Includes Price
Complete HTML Pack All 5 HTML docs £22.00
Complete CSS Pack All 5 CSS docs £22.00
Complete JavaScript Pack All 5 JS docs £22.00
Notes Bundle All 3 fundamentals guides £16.00
Beginner Exercises Bundle All 6 beginner docs (3 languages) £18.00
Intermediate Exercises Bundle All 6 intermediate docs (3 languages) £20.00
Complete Series All 16 teaching docs £45.00

After the first 2030 reviews, raise prices 2030%. Early buyers got a founder's discount; this is standard and well-accepted on Gumroad.


Design Decisions (Important — Keep These Consistent)

Brand Colours

  • HTML → Orange #E44D26
  • CSS → Blue #264DE4
  • JavaScript → Yellow #B8A800
  • Answer sheets → Green #1A7A3C (all languages)
  • Strategy docs → Teal #2E86AB

Document Design Rules

  • Font: Arial throughout (body, headings, tables)
  • Code blocks: dark background #1E1E1E, monospace Courier New
    • HTML code → amber #F8C555
    • CSS code → blue #9CDCFE
    • JavaScript code → yellow #F0DB4F
    • Console output → green #98FB98 on #2D2D2D
  • Exercise badges: 4-column table (title | section ref | time | score)
  • Answer headers: full-width green banner with tick mark
  • Info boxes: 2-column table (coloured label | tinted text area)
  • Reference tables: coloured header row, alternating white/#F8F8F8 rows

Scoring

  • Beginner exercises: 5 pts each × 6 = 30 pts per language
  • Intermediate exercises: 10 pts each × 3 = 30 pts per language

Technical Setup

Requirements

python-docx==1.2.0    pip install python-docx
Pillow                pip install Pillow

Do NOT use the Node.js docx library (v9.6.1 was tried — it produces structurally valid XML but Word rejects the files on open).

How to Run

# From your project folder, run scripts in this order:
python scripts/gen_micro_images.py   # generates bex1-6.png
python scripts/gen_images.py         # generates html_ex1-3_preview.png
python scripts/gen_notes.py          # HTML/CSS/JS Fundamentals
python scripts/gen_strategy.py       # Monetising guide + Gumroad Strategy
python scripts/gen_exercises.py      # 6 intermediate exercise docs
python scripts/gen_beginner.py       # 6 beginner exercise docs

Output Path Variable

Each generator script has OUT = "/mnt/user-data/outputs" near the top. Change this to your local output folder, e.g.:

OUT = r"C:\projects\Docs Re\output"

Validating Files

Run this to confirm all generated files are Word-compatible:

import zipfile, xml.etree.ElementTree as ET, os

folder = r"C:\projects\Docs Re\output"
for f in sorted(os.listdir(folder)):
    if f.endswith('.docx'):
        path = os.path.join(folder, f)
        try:
            with zipfile.ZipFile(path) as z:
                ET.fromstring(z.read('word/document.xml'))
            print(f"OK  {f}")
        except Exception as e:
            print(f"FAIL  {f}  ->  {e}")

Shared Helper Library (dochelper.py)

This file must exist before any generator runs. All generators do import dochelper at the top.

"""
Shared helpers for generating Word documents with python-docx.
Produces clean, Word-compatible .docx files.
"""
from docx import Document
from docx.shared import Pt, RGBColor, Inches, Cm, Emu
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT, WD_ALIGN_VERTICAL
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
import copy

# ── Colour palette ─────────────────────────────────────────────────────────
def rgb(hex6):
    h = hex6.lstrip('#')
    return RGBColor(int(h[0:2],16), int(h[2:4],16), int(h[4:6],16))

WHITE    = rgb("FFFFFF")
BLACK    = rgb("1A1A2E")
GRAY     = rgb("666666")
LGRAY    = rgb("F5F5F7")
DGRAY    = rgb("333333")
ORANGE   = rgb("E44D26")
ORANGE_L = rgb("FDF0EC")
BLUE     = rgb("264DE4")
BLUE_L   = rgb("EEF2FD")
YELLOW   = rgb("B8A800")
YELLOW_L = rgb("FFFCE0")
GREEN    = rgb("1A7A3C")
GREEN_L  = rgb("E8F8EE")

# ── Document setup ──────────────────────────────────────────────────────────
def new_doc():
    doc = Document()
    for section in doc.sections:
        section.top_margin    = Cm(2.5)
        section.bottom_margin = Cm(2.5)
        section.left_margin   = Cm(2.5)
        section.right_margin  = Cm(2.5)
    for p in doc.paragraphs:
        p._element.getparent().remove(p._element)
    return doc

# ── Paragraph helpers ───────────────────────────────────────────────────────
def add_para(doc_or_cell, text="", bold=False, italic=False, size=11,
             color=None, align=WD_ALIGN_PARAGRAPH.LEFT,
             space_before=0, space_after=6, font_name="Arial"):
    if hasattr(doc_or_cell, 'add_paragraph'):
        p = doc_or_cell.add_paragraph()
    else:
        p = doc_or_cell.paragraphs[0] if doc_or_cell.paragraphs else doc_or_cell.add_paragraph()
        p.clear()
    p.alignment = align
    pf = p.paragraph_format
    pf.space_before = Pt(space_before)
    pf.space_after  = Pt(space_after)
    if text:
        run = p.add_run(text)
        run.bold = bold; run.italic = italic
        run.font.name = font_name; run.font.size = Pt(size)
        run.font.color.rgb = color if color else BLACK
    return p

def add_heading(doc, text, level=1, color=None, size=None, border_color=None,
                space_before=14, space_after=4):
    sizes = {1: 18, 2: 14, 3: 12}
    p = doc.add_paragraph()
    p.alignment = WD_ALIGN_PARAGRAPH.LEFT
    p.paragraph_format.space_before = Pt(space_before)
    p.paragraph_format.space_after  = Pt(space_after)
    run = p.add_run(text)
    run.bold = True; run.font.name = "Arial"
    run.font.size = Pt(size or sizes.get(level, 11))
    run.font.color.rgb = color or BLACK
    if border_color:
        set_para_border(p, bottom=border_color)
    return p

def add_code_block(doc, lines, bg="1E1E1E", fg="F0DB4F", left_bar=None):
    for line in lines:
        p = doc.add_paragraph()
        p.paragraph_format.space_before = Pt(0)
        p.paragraph_format.space_after  = Pt(0)
        p.paragraph_format.left_indent  = Cm(0.5)
        set_para_shading(p, bg)
        if left_bar:
            set_para_border(p, left=left_bar)
        run = p.add_run(line if line else " ")
        run.font.name = "Courier New"; run.font.size = Pt(9.5)
        run.font.color.rgb = rgb(fg)
    sp = doc.add_paragraph(" ")
    sp.paragraph_format.space_before = Pt(0)
    sp.paragraph_format.space_after  = Pt(4)

def add_bullet(doc, text, size=11, color=None):
    p = doc.add_paragraph(style='List Bullet')
    p.paragraph_format.space_before = Pt(2)
    p.paragraph_format.space_after  = Pt(2)
    run = p.add_run(text)
    run.font.name = "Arial"; run.font.size = Pt(size)
    run.font.color.rgb = color or BLACK
    return p

def add_numbered(doc, text, size=11, color=None):
    p = doc.add_paragraph(style='List Number')
    p.paragraph_format.space_before = Pt(2)
    p.paragraph_format.space_after  = Pt(2)
    run = p.add_run(text)
    run.font.name = "Arial"; run.font.size = Pt(size)
    run.font.color.rgb = color or BLACK
    return p

def add_checkbox(doc, text):
    p = doc.add_paragraph(style='List Bullet')
    p.paragraph_format.space_before = Pt(2)
    p.paragraph_format.space_after  = Pt(2)
    run = p.add_run("☐  " + text)
    run.font.name = "Arial"; run.font.size = Pt(10.5)
    run.font.color.rgb = BLACK
    return p

def add_tick(doc, text):
    p = doc.add_paragraph(style='List Bullet')
    p.paragraph_format.space_before = Pt(2)
    p.paragraph_format.space_after  = Pt(2)
    run = p.add_run("✓  " + text)
    run.font.name = "Arial"; run.font.size = Pt(10.5)
    run.font.color.rgb = GREEN
    return p

def add_spacer(doc, pts=8):
    p = doc.add_paragraph(" ")
    p.paragraph_format.space_before = Pt(0)
    p.paragraph_format.space_after  = Pt(pts)
    for run in p.runs:
        run.font.size = Pt(2)

def add_page_break(doc):
    doc.add_page_break()

# ── Table helpers ───────────────────────────────────────────────────────────
def cell_text(cell, text, bold=False, italic=False, size=10, color=None,
              align=WD_ALIGN_PARAGRAPH.LEFT, bg=None, font="Arial"):
    cell.text = ""
    p = cell.paragraphs[0]
    p.alignment = align
    p.paragraph_format.space_before = Pt(3)
    p.paragraph_format.space_after  = Pt(3)
    run = p.add_run(text)
    run.bold = bold; run.italic = italic
    run.font.name = font; run.font.size = Pt(size)
    run.font.color.rgb = color or BLACK
    if bg:
        set_cell_shading(cell, bg)
    return cell

def set_cell_shading(cell, hex_color):
    tc = cell._tc; tcPr = tc.get_or_add_tcPr()
    shd = OxmlElement('w:shd')
    shd.set(qn('w:val'), 'clear'); shd.set(qn('w:color'), 'auto')
    shd.set(qn('w:fill'), hex_color.lstrip('#'))
    tcPr.append(shd)

def set_cell_margins(cell, top=60, bottom=60, left=100, right=100):
    tc = cell._tc; tcPr = tc.get_or_add_tcPr()
    tcMar = OxmlElement('w:tcMar')
    for side, val in [('top',top),('bottom',bottom),('left',left),('right',right)]:
        el = OxmlElement(f'w:{side}')
        el.set(qn('w:w'), str(val)); el.set(qn('w:type'), 'dxa')
        tcMar.append(el)
    tcPr.append(tcMar)

# ── XML styling helpers ─────────────────────────────────────────────────────
def set_para_shading(para, hex_color):
    pPr = para._p.get_or_add_pPr()
    shd = OxmlElement('w:shd')
    shd.set(qn('w:val'), 'clear'); shd.set(qn('w:color'), 'auto')
    shd.set(qn('w:fill'), hex_color.lstrip('#'))
    pPr.append(shd)

def set_para_border(para, bottom=None, left=None, top=None):
    pPr = para._p.get_or_add_pPr()
    pBdr = OxmlElement('w:pBdr')
    for side, color in [('bottom',bottom),('left',left),('top',top)]:
        if color:
            el = OxmlElement(f'w:{side}')
            el.set(qn('w:val'), 'single')
            el.set(qn('w:sz'), '6' if side in ('bottom','top') else '18')
            el.set(qn('w:space'), '4')
            el.set(qn('w:color'), color.lstrip('#') if isinstance(color,str)
                   else '{:02X}{:02X}{:02X}'.format(*color))
            pBdr.append(el)
    pPr.append(pBdr)

# ── Compound elements ───────────────────────────────────────────────────────
def add_info_box(doc, label, text, bg_hex, accent_hex, label_color_hex="FFFFFF"):
    table = doc.add_table(rows=1, cols=2)
    table.style = 'Table Grid'
    lc = table.rows[0].cells[0]
    lc.width = Cm(1.5)
    set_cell_shading(lc, accent_hex); set_cell_margins(lc)
    p = lc.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    p.paragraph_format.space_before = Pt(4); p.paragraph_format.space_after = Pt(4)
    run = p.add_run(label); run.bold = True; run.font.name = "Arial"
    run.font.size = Pt(9); run.font.color.rgb = rgb(label_color_hex)
    tc = table.rows[0].cells[1]
    set_cell_shading(tc, bg_hex); set_cell_margins(tc, left=120)
    p2 = tc.paragraphs[0]
    p2.paragraph_format.space_before = Pt(4); p2.paragraph_format.space_after = Pt(4)
    run2 = p2.add_run(text); run2.font.name = "Arial"; run2.font.size = Pt(10)
    run2.font.color.rgb = BLACK
    add_spacer(doc, 6)
    return table

def add_cover(doc, title_top, title_main, title_sub, subtitle, part_note, accent_hex):
    add_spacer(doc, 40)
    if title_top:
        add_para(doc, title_top, bold=True, size=14, color=GRAY,
                 align=WD_ALIGN_PARAGRAPH.CENTER, space_after=2)
    add_para(doc, title_main, bold=True, size=36, color=rgb(accent_hex),
             align=WD_ALIGN_PARAGRAPH.CENTER, space_after=4)
    if title_sub:
        add_para(doc, title_sub, bold=True, size=20, color=BLACK,
                 align=WD_ALIGN_PARAGRAPH.CENTER, space_after=6)
    add_para(doc, subtitle, size=12, color=GRAY,
             align=WD_ALIGN_PARAGRAPH.CENTER, space_after=4)
    if part_note:
        p = add_para(doc, part_note, size=10, italic=True, color=GRAY,
                     align=WD_ALIGN_PARAGRAPH.CENTER, space_after=8)
        set_para_border(p, bottom=accent_hex)
    add_spacer(doc, 20)

def add_answer_header(doc, num, title, accent_hex="1A7A3C"):
    table = doc.add_table(rows=1, cols=1)
    table.style = 'Table Grid'
    cell = table.rows[0].cells[0]
    set_cell_shading(cell, accent_hex); set_cell_margins(cell, top=80, bottom=80, left=150)
    p = cell.paragraphs[0]
    r1 = p.add_run(f"✓  Answer — Exercise {num}:  ")
    r1.bold = True; r1.font.name = "Arial"; r1.font.size = Pt(13); r1.font.color.rgb = WHITE
    r2 = p.add_run(title)
    r2.font.name = "Arial"; r2.font.size = Pt(11); r2.font.color.rgb = rgb("C8F0D4")
    add_spacer(doc, 6)

def add_exercise_badge(doc, num, title, section_ref, difficulty, minutes, accent_hex, score_denom=5):
    table = doc.add_table(rows=1, cols=4)
    table.style = 'Table Grid'
    c0 = table.rows[0].cells[0]; c0.width = Cm(8)
    set_cell_shading(c0, accent_hex); set_cell_margins(c0, top=80, bottom=80, left=150)
    p = c0.paragraphs[0]
    r1 = p.add_run(f"Ex {num}  "); r1.bold=True; r1.font.name="Arial"
    r1.font.size=Pt(13); r1.font.color.rgb=WHITE
    r2 = p.add_run(title); r2.font.name="Arial"; r2.font.size=Pt(11); r2.font.color.rgb=rgb("E0E0E0")
    c1 = table.rows[0].cells[1]; set_cell_shading(c1, "F5F5F7"); set_cell_margins(c1)
    p1a = c1.paragraphs[0]; p1a.alignment = WD_ALIGN_PARAGRAPH.CENTER
    r = p1a.add_run(f"After Section {section_ref}"); r.font.name="Arial"; r.font.size=Pt(9); r.font.color.rgb=GRAY
    p1b = c1.add_paragraph(); p1b.alignment = WD_ALIGN_PARAGRAPH.CENTER
    r2 = p1b.add_run(difficulty); r2.bold=True; r2.font.name="Arial"; r2.font.size=Pt(9); r2.font.color.rgb=rgb(accent_hex)
    c2 = table.rows[0].cells[2]; set_cell_shading(c2, "F5F5F7"); set_cell_margins(c2)
    p2a = c2.paragraphs[0]; p2a.alignment = WD_ALIGN_PARAGRAPH.CENTER
    p2b = c2.add_paragraph(); p2b.alignment = WD_ALIGN_PARAGRAPH.CENTER
    r = p2b.add_run(f"{minutes} min"); r.bold=True; r.font.name="Arial"; r.font.size=Pt(9); r.font.color.rgb=BLACK
    c3 = table.rows[0].cells[3]; set_cell_shading(c3, "F5F5F7"); set_cell_margins(c3)
    p3a = c3.paragraphs[0]; p3a.alignment = WD_ALIGN_PARAGRAPH.CENTER
    r = p3a.add_run("Score"); r.font.name="Arial"; r.font.size=Pt(9); r.font.color.rgb=GRAY
    p3b = c3.add_paragraph(); p3b.alignment = WD_ALIGN_PARAGRAPH.CENTER
    r = p3b.add_run(f"__ / {score_denom}"); r.bold=True; r.font.name="Arial"; r.font.size=Pt(10); r.font.color.rgb=BLACK
    add_spacer(doc, 6)

def add_ref_table(doc, headers, rows, accent_hex, col_widths_cm=None):
    table = doc.add_table(rows=1+len(rows), cols=len(headers))
    table.style = 'Table Grid'
    if col_widths_cm:
        for row in table.rows:
            for j, cell in enumerate(row.cells):
                if j < len(col_widths_cm):
                    cell.width = Cm(col_widths_cm[j])
    for j, h in enumerate(headers):
        c = table.rows[0].cells[j]
        set_cell_shading(c, accent_hex); set_cell_margins(c)
        cell_text(c, h, bold=True, size=10, color=WHITE)
    for i, row in enumerate(rows):
        bg = "FFFFFF" if i % 2 == 0 else "F8F8F8"
        for j, val in enumerate(row):
            c = table.rows[i+1].cells[j]
            set_cell_shading(c, bg); set_cell_margins(c)
            font = "Courier New" if j < 2 else "Arial"
            clr = rgb("C0392B") if j == 0 else (GRAY if j == 1 else BLACK)
            cell_text(c, val, size=9, color=clr, font=font)
    add_spacer(doc, 8)

print("dochelper loaded OK")

Ideas for What to Build Next

These were discussed but not yet created. Claude Code can pick any of these up:

Additional Documents

  • Advanced Exercise Tier — 3 more challenging exercises per language (e.g. building a full responsive landing page in HTML/CSS, a full CRUD app in JS). Worth £1215 each.
  • jQuery Basics — a notes + exercise pack for the legacy library still widely used in older codebases
  • Git & GitHub Guide — version control reference doc, highly searched on Etsy
  • CSS Animations Deep Dive — keyframes, transitions, scroll animations
  • JavaScript Interview Prep — Q&A format covering closures, hoisting, event loop, promises

Product Improvements

  • Add a table of contents page to each fundamentals notes doc
  • Add page numbers in the footer of each document
  • Create PDF versions of all documents (some buyers prefer PDFs for printing)
  • Add a "Common Mistakes" callout box to every exercise (not just answer sheets)

Marketing Assets

  • A free sample PDF — 2-page teaser combining one page from each fundamentals guide
  • Gumroad cover images — 1280×720px branded thumbnails for each product listing
  • A product comparison table one-pager showing all bundles side by side

Platform Expansion

  • Reformat notes as Notion templates (sold separately on Gumroad)
  • Create a Teachable or Udemy course outline based on the existing document structure
  • Package documents as a Payhip bundle for a second sales channel

Key Decisions Made (Do Not Change Without Reason)

Decision What Was Decided
File format Word (.docx) via python-docx 1.2.0 — NOT Node.js docx library
Image generation PIL (Pillow) for browser-window preview images
Fonts Arial body, Courier New for all code
Code block style Dark background #1E1E1E, language-specific fg colours
Answer sheet colour Green #1A7A3C for all languages
Scoring Beginner: 5 pts × 6 = 30
Pricing anchor Complete Series £45 is the hero product
Lead magnet Monetising guide offered free to build email list
Launch platform Gumroad first, then Etsy in parallel

This context file was generated from a claude.ai chat session. Last updated: June 2026.