Zonqor Refurbishment — project index

Everything in this folder, in one place. Built 17 August 2026

Mon 17 Aug 2026start on site (delayed from 13 Aug)
Tue 17 Nov 2026finish · 67 working days
81work items across 13 areas
12open questions, none answered
133 m²internal floor, measured
123 m²less the wet rooms — cf. 125 m²

What this is

Kurt's refurbishment of the Zonqor apartment. Roderick (main contractor, with his son) does gypsum, electrical, plumbing and tiling; ULF does carpentry; Christine walks the site each evening, signs items off and agrees prices on paper. Print is the primary medium — everything here is built to be printed.

The two deliverables

Open questions — all still unanswered

OQ-01Outdoor wall finish — PVC sheet or tiles?
OQ-02Outdoor kitchen — design / equipment spec
OQ-03New consumer unit — location and capacity
OQ-04Study/Office plugs — quantity
OQ-05Ensuite tiles — confirm 60×30 dimensions
OQ-06Living/Dining TV wall — PVC sheet spec (material, size, fixing)
OQ-07Sofa-area TV unit — electrical point spec
OQ-08Intercom — reuse or replace? audio or video?
OQ-09Who books and pays for the skips?
OQ-10Tiling scope — site photos show the ensuite, main bathroom and front balcony are already tiled. Does ~145 sqm still stand, and what is in it?
OQ-11Fireplace — the photo shows it installed with the wall already opened beneath. Remove, or keep and rework around it?
OQ-12Two tile formats on site: Fanal 120 and Fanal 31.6×90. What is the 31.6×90 for?

The three things to settle first

  1. OQ-10 tiling scope — before any price is agreed. The base plan shows the 125/145 m² gap is the two balconies; what is still open is how much is actually being tiled (~22 m² of argument).
  2. OQ-11 fireplace — remove or keep? Needed by plan day 4, Thu 20 Aug.
  3. OQ-03 consumer unit and OQ-08 intercom — both change first-fix cable, week one.

Site Checklist (PDF)

23 pages. The live document — this is what Christine prints and works from. · open the real PDF (print from that, not from these previews). Click a page to enlarge.

page 1 of 23
page 2 of 23
page 3 of 23
page 4 of 23
page 5 of 23
page 6 of 23
page 7 of 23
page 8 of 23
page 9 of 23
page 10 of 23
page 11 of 23
page 12 of 23
page 13 of 23
page 14 of 23
page 15 of 23
page 16 of 23
page 17 of 23
page 18 of 23
page 19 of 23
page 20 of 23
page 21 of 23
page 22 of 23
page 23 of 23

Base Plan (PDF)

3 pages. Reconstructed floor plan, room schedule, and the measure-on-site list. · open the real PDF (print from that, not from these previews). Click a page to enlarge.

page 1 of 3
page 2 of 3
page 3 of 3

Photo Sort Sheet (PDF)

4 pages. Contact sheet used to try to place the 54 unassigned site photos. · open the real PDF (print from that, not from these previews). Click a page to enlarge.

page 1 of 4
page 2 of 4
page 3 of 4
page 4 of 4

Site Checklist (HTML)

Same document, with live-totalling price boxes. Open full screen to use it.

CLAUDE.md — master context

Zonqor Apartment Refurbishment — Project Context

Read this file first — or open index.html in the project root, which browses this file and everything else in the folder offline. It is the complete context for the project, carried over from a long Cowork session. Everything the previous session knew is either in this file or in the files it points at. Where something is uncertain, it says so — do not treat inference as fact.


1. What this project is

Kurt is refurbishing his apartment at Zonqor (Malta). A contractor starts on Monday 17 August 2026 (delayed from Thursday 13 August). The work is turnkey: one main contractor plus a carpenter.

The artefact built so far is a printed A4 site checklist and price sheet — 81 work items across 13 areas, each with an ID, trade, assignee, dependency, phase, and space to record status, date, agreed price, initials and comments on paper.

Who does what

PersonRole
RoderickMain contractor. Gypsum & structural, electrical, plumbing & gas, tiling. Works with his son — a crew of 2.
ULFCarpenter. Wardrobes, cabinets, TV unit, mirrors, fittings. (ULF is a person's name, not an acronym.)
ChristineKurt's wife. Does the daily on-site walk-through, signs off items and agrees prices per item, in the evening, on paper.
KurtOwner. Answers open questions, confirms specs, makes ordering decisions.

The paper-first design exists because Christine works from a printed copy on site at night. Any future deliverable must keep that property: print is the primary medium, screen is secondary.


2. Why this handoff exists — what to build next

The checklist is essentially done. What is missing and is the reason for moving to Claude Code:

  1. A full architecture / floor plan. DONE, 16 Aug 2026output/Zonqor Base Plan.pdf.

Reconstructed by tracing the designer's 1:100 layout, calibrated on a printed scale bar (two independent routes agreeing to 0.08%). 12 rooms with floor areas, per-room confidence grades, and a measure-on-site list. Method and caveats: docs/09-base-plan.md. Geometry lives in build/plan_data.py and nowhere else. It also answered half of OQ-10 — the 125 / 145 sqm gap is the two balconies. ⚠ Still a reconstruction, not a survey: the measure-on-site list is outstanding, and no point positions should be committed until it is done.

  1. A proper electrical plan. The designer supplied renders but no electrical drawings. The

checklist currently carries only an indicative point count per room (how many light points, switches, power points) — explicitly labelled "not a wiring diagram". What is needed: a circuit schedule, a consumer-unit / distribution board schedule, point layout per room, cable routes, and the ethernet runs back to the entrance hub.

  1. Elevations. The designer gave "limited elevations" (one PDF). Room elevations showing switch

and socket heights, wardrobe internals, and the feature-wall build-up do not exist.

  1. Anything else missing — see docs/08-whats-missing.md.

Honest constraints on doing this

Any plan produced from this material is approximate and must be labelled as such until someone measures the flat. Do not present a reconstructed plan as a survey drawing.

("2 bedside plugs", "4 ceiling lights"), not from positions. Placement on any drawing is a proposal to be confirmed on site with Roderick, not a record of fact.

discipline — a confidently-wrong electrical drawing on a live site is dangerous.


3. Current state of the deliverable

output/Zonqor Site Checklist.pdf — 23 A4 pages, the live document. Structure:

  1. Cover — roles, how-to-use, sign-off block
  2. Work Plan — 11 phases, with two flagged warnings (see §5)
  3. Day-by-Day Plan — 67 working days, Mon 17 Aug → Tue 17 Nov 2026
  4. Open Questions — OQ-01…OQ-12, each with the plan-day deadline by which it is needed
  5. 13 room sections in walk-through order, each with inline photo thumbnails, trade sub-groups, and per-item form fields
  6. Price Summary — per-room totals, grand total, signatures
  7. Daily Site Log — 16 rows
  8. Appendix — 49 pictures at full size, IDs A01A50

output/Zonqor Site Checklist.html — same document; the price boxes live-total per room and into the summary table. Built to be self-contained (inline CSS/JS, base64 images, no localStorage).

Conventions that must not drift

⚠ Trade grouping reorders items, so an ID is only stable if the data file order and the group order both stay fixed. A previous bug had cross-references pointing at the wrong item because of exactly this — build/verify_refs.py exists to catch it. Run it after every rebuild.

Roderick; fittings/carpentry → ULF.


4. Rooms — walk-through order

Kurt specified this order explicitly; it follows a physical walk through the flat. Keep it.

#CodeRoomItemsNotes
1WHSWhole House5Cross-room items only, priced once
2FBLFront Balcony10Outdoor kitchen lives here (Kurt confirmed)
3OPNOpen Plan — Kitchen → Dining → Living14One space; items carry zone tags
4ENTEntrance2Electrical central unit + intercom
5MCRMain Corridor6
6SCRSmall Corridor1
7MBRMain Bedroom12
8OFFStudy / Office9Sits under/off the Main Bedroom (Kurt)
9ENSEnsuite5
10BTHMain Bathroom6
11CYBCy's Bedroom (with window)5
12RAFRafe's Bedroom (with back balcony door)5
13BBLBack Balcony1Scope not yet established

Full item list: data/rooms.json (structured) and data/items.csv (flat).


5. The work plan, and the two things worth arguing about

Roderick proposed this order: clean up old parquet / demolish the dining feature wall → plumbing / core electricity + electrical unit / ethernet → cleanup with skip → tiling → gypsum amendments → "let's see what's missing after this".

Six stages were missing and were added: painting, second-fix electrical (fitting and terminating every point, hanging fittings, testing and certification), carpentry, fittings and sanitary, the outdoor works, and snagging/handover.

One sequencing change was proposed and is NOT yet agreed with Roderick. He put tiling before gypsum. That is right for patching, but wrong for three full walls — the study partition (OFF-01), the small corridor extension (SCR-01) and the sofa wall (OPN-02). Tiling under a wall that is then built on top wastes 120×120 tile and seats the wall on tile rather than slab. The plan therefore splits it: Phase 4 builds partitions before tiling; Phase 6 keeps patching after. ⚠ Flag this to Roderick on day one. He may have a reason.

PhPhaseDays
1Strip-out & demolition5
2First fix — plumbing, electrical, ethernet, gas16
3Clear site — skip1
4Structural gypsum (partitions) — moved before tiling6
5Tiling16
6Gypsum amendments & making good2
7Painting5
8Second fix electrical6
9Carpentry — ULF (+ Roderick cut-ins)3
10Fittings, sanitary & outdoor5
11Snagging & handover2

Duration assumptions (all stated in the document, all unconfirmed): 2 people, Mon–Fri, no public holidays deducted; painting assumed to be in Roderick's price — if it is not, Phase 7 is a separate trade and every date after it shifts; ULF's carpentry shown sequentially but could start earlier and pull the finish in by ~1 week; balcony tiling assumes dry weather.


6. Open questions — all still unanswered

None of these have been decided. Deadlines are plan-days from the 17 Aug start.

IDQuestionRoomNeeded by
OQ-01Outdoor wall finish — PVC sheet or tiles? (one or the other, not both)FBLday 63
OQ-02Outdoor kitchen — design / equipment specFBLday 18
OQ-03New consumer unit — location and capacityENTday 6
OQ-04Study/Office plugs — quantityOFFday 14
OQ-05Ensuite tiles — confirm 60×30 dimensionsENSday 42
OQ-06Living/Dining TV wall — PVC sheet specOPNday 64
OQ-07Sofa-area TV unit — electrical point specOPNday 10
OQ-08Intercom — reuse or replace? audio or video? (changes first-fix cable)ENTday 8
OQ-09Who books and pays for the skips?WHSday 5
OQ-10Tiling scope — photos show ensuite, main bathroom and front balcony are already tiled. Does ~145 sqm still stand, and what is in it?WHSbefore pricing
OQ-11Fireplace — photo shows it installed with the wall already opened beneath. Remove, or keep and rework around it?OPNday 4
OQ-12Two tile formats on site: Fanal 120 and Fanal 31.6×90. What is the 31.6×90 for?WHSday 29

OQ-10 is the commercially significant one and should be settled before any price is agreed.


7. Source material — what exists and how good it is

images/measurements/ — the dimensional evidence (full resolution, read these to build a plan)

plan that exists. Roderick's quote says 145 sqm; Kurt chose 145. The discrepancy is unexplained and is now folded into OQ-10.

*"Sound proof gypsum partition to consist of: 1. Inside of study to have 2 layers of boards.

  1. Middle part to have a profile of 75 mm filled with rockwool insulation. 3. Outer part of study

to have a single board." Also notes "wardrobe around ensuite door for maximum storage"*.

Copy_of_kids_bedroom_measurement_opt4_p1.jpg

images/design/ — designer renders (1400px, visual intent only, no dimensions)

19 renders: main bedroom (4), study (3), Cy (2), Rafe (2), corridors (2), balcony (2), dining feature wall, dining+TV unit, dining+kitchen, sofa.

images/site/ — 75 photos Kurt took on site, Aug 2026

IMG_9826IMG_9896 are photos; IMG_9897IMG_9900_frame are stills pulled from 4 videos. Originals were HEIC (135 MB) and were converted to 1100px JPEG. The originals are still on Kurt's Mac at ~/Desktop/HomeWorks&ReDesign/Site Photos/_Unsorted/Zonqor Works/ if higher resolution is ever needed.

Only 21 of the 75 are placed into the document. See docs/05-photo-index.md for the full inventory including which are placed, which are identified but unplaced, and which are unidentified. 54 remain unassigned — mostly close-ups of switch plates and sockets that look identical across rooms. Kurt was asked for the order he walked the flat so they could be assigned by number range; he has not yet answered. That question is still open and is the fastest way to place them.

data/

(126 rows × 20 cols): # / NEW-EXIST / ACTION / ROOM / LOCATION / DESCRIPTION / FOR / Gypsum-paint works / Furniture Setup / Electrical Points / Light Points / Switch Points / Water Points / Gas Points / IMAGE. This is the origin of the whole checklist.

like the ethernet behind the sofa, the LED bar near the office door, the emergency-light short circuit in the back corridor, and "unit for salva vita"


8. What the site photos revealed — and why it matters

The photos changed the picture materially, and these findings are not yet resolved:

  1. The flat is in far better condition than "refurbishment" implies. The ensuite and main

bathroom are finished in large-format travertine and grey marble, with shower glass, tray and sanitary ware installed. The front balcony is already tiled. This is a refresh, not a strip-out.

  1. Therefore the tiling quantity is probably wrong. "120×120 full house including balconies,

~145 sqm" almost certainly applies only to the parquet areas — living/dining, corridors, bedrooms. → OQ-10, and it moves both price and the 16-day tiling phase.

  1. The fireplace appears newly installed (IMG_9893), yet the wall beneath it is already broken

open. The item says "Dining Feature Wall + Fireplace Removal". → OQ-11.

  1. Two tile formats are on site: Fanal 120 boxes in the open plan, Fanal 31.6×90 stacked on the

balcony. The work list only knows about 120×120 and the ensuite 60×30. → OQ-12.

  1. IMG_9845 shows the existing consumer unit, its position by the entrance door, and an

adjacent thermostat panel — directly useful for OQ-03 and for any electrical plan.

  1. Materials already delivered: Fanal tiles (both formats), Isomat AK22 tile adhesive, gypsum board.

9. Files in this package

The handoff bundle has been unpacked to the working-folder root, so this file loads automatically when Claude Code starts in ~/Desktop/ZonqorHome-RefurbWorks. Every relative path below is unchanged from the bundle.

ZonqorHome-RefurbWorks/          ← project root, run claude here
├── CLAUDE.md                     ← you are here
├── README.md                     ← how to pick this up
├── docs/
│   ├── 01-decisions-log.md       every decision Kurt made, verbatim, with what it changed
│   ├── 02-open-questions.md      OQ-01..12 in full, with why each matters
│   ├── 03-work-plan.md           phases, durations, assumptions, the sequencing argument
│   ├── 04-day-by-day.md          all 67 days with dates and item IDs
│   ├── 05-photo-index.md         all 75 site photos: what each shows, placed or not
│   ├── 06-source-data.md         what is in each source file and how reliable it is
│   ├── 07-site-observations.md   the discrepancies found in the photos, in detail
│   ├── 08-whats-missing.md       the brief for the architecture + electrical plan work
│   ├── 09-base-plan.md           how the base plan was reconstructed, and how far to trust it
│   └── 10-project-index.md       what index.html is, and the three constraints on it
├── data/      rooms.json, items.csv, works_*.csv, lights_plugs_*.csv, pdf_text/
├── images/    measurements/ (dimensional, full-res) · design/ · site/
├── index.html ← LOCAL PROJECT BROWSER. Double-click it. Built by build/build_index.py
│           also hosted: https://zonqor-refurb-c7cc97.pages.dev (public link, no login)
├── build/     checklist: build_v3.py, rooms_data2.py, verify_refs.py, pdfcheck.py, gen_day_plan.py
│           base plan: plan_data.py, build_plan.py, pdfplan.py
│           index:     build_index.py, deploy.sh (Cloudflare Pages — the only online step)
├── output/    Zonqor Site Checklist.pdf + .html, Zonqor Base Plan.pdf, Photo Sort Sheet.pdf
│           preview/  96 dpi page JPEGs, generated for index.html — safe to delete
└── _archive/  zonqor-refurb-handoff.zip (the original bundle) + an earlier PDF export

_archive/ is the untouched original. Nothing reads from it — it exists so the flattening is reversible and so the pre-handoff PDF export is not lost.

10. Rebuilding the checklist

cd build
python3 build_v3.py        # writes ../output/renovation_checklist.html
python3 verify_refs.py     # MUST pass: no broken refs, no duplicate IDs
python3 gen_day_plan.py    # rewrites ../docs/04-day-by-day.md from the built HTML
python3 pdfcheck.py        # renders ../output/Zonqor Site Checklist.pdf via Playwright

The schedule has exactly one source of truth: START and DAYS in build_v3.py. Change the start date there and nowhere else — the cover, the phase table, the day plan and docs/04-day-by-day.md all derive from it. (Before 16 Aug 2026 the start date was also hardcoded as a literal string in three places in the HTML; those are now derived, and the markdown day plan was hand-maintained until gen_day_plan.py replaced it.) Dates stated in proseCLAUDE.md, docs/03-work-plan.md, docs/07-site-observations.md — are still manual; grep for the old date after any change.

Two filenames, one document. build_v3.py writes output/renovation_checklist.html; the file delivered to Kurt is output/Zonqor Site Checklist.html — a renamed copy. pdfcheck.py reads the build name and writes the delivered PDF name. After a rebuild you must copy the HTML across yourself if the HTML is being handed over:

cp ../output/renovation_checklist.html "../output/Zonqor Site Checklist.html"

index.html must be rebuilt after any of this — it embeds the docs and data at build time:

cd build && python3 build_index.py

The base plan is a separate, smaller pipeline (added 16 Aug 2026):

cd build
python3 build_plan.py     # geometry from plan_data.py -> ../output/base_plan.html
python3 pdfplan.py        # renders ../output/Zonqor Base Plan.pdf

It shares the honesty discipline and the print-first CSS but nothing else — it has its own data file and no cross-reference verifier, because it has no IDs to break. See docs/09-base-plan.md.

Then look at the pages. pdf2image is not installed on this Mac, but poppler is, so render directly and read the PNGs — never assume the CSS worked:

pdftoppm -png -r 90 -f 1 -l 4 "../output/Zonqor Site Checklist.pdf" /tmp/pg

Dependencies — what is actually needed. requirements.txt lists the tools used to extract data/ from Kurt's spreadsheets and PDFs. The rebuild itself needs far less:

Needed to rebuildStatus on this Mac
build_v3.py — Python stdlib only✅ Python 3.14.4 at /opt/homebrew/bin/python3
verify_refs.pybeautifulsoup4✅ installed
pdfcheck.pyplaywright + chromium✅ installed (~/Library/Caches/ms-playwright/chromium-1208)
visual check — poppler pdftoppm/opt/homebrew/bin/pdftoppm
pandas, pdfplumber, pdf2image❌ not installed — only needed to re-extract source data

Verified 16 Aug 2026: a clean rebuild on this Mac reproduces Zonqor Site Checklist.html byte-for-byte (same MD5), and the PDF renders to the same 23 pages, laid out correctly. The HTML generator is deterministic — so any HTML diff you see later is a real content change, not build noise. If a rebuild ever stops matching unexpectedly, that is a signal, not a nuisance.

The PDF is not byte-reproducible. pdfcheck.py overwrites Zonqor Site Checklist.pdf, and this Mac's Chromium emits a different byte stream (~2.9 MB) than the sandbox Chromium that made the shipped file (~2.4 MB) — same 23 pages, same layout. So compare PDFs by looking at pages, not by MD5, and be aware that running pdfcheck.py replaces the file Christine may be printing from. The original shipped PDF is preserved inside _archive/zonqor-refurb-handoff.zip.

build_v3.py reads rooms_data2.py (the item data) and emits one self-contained HTML file. Room content lives in rooms_data2.py; layout, the day plan and the narrative warnings live in build_v3.py.


11. Working notes for whoever picks this up

wrong, change exactly that and re-verify — do not rebuild around it.

tiling-scope discrepancy were both welcomed. Flag problems rather than quietly complying.

exists, so this is indicative only" — that framing is in the document and should stay.

assuming the CSS worked. pdfcheck.py + pdf2image is the loop that has been used throughout.

README.md

Zonqor Refurbishment — Claude Code handoff

Everything from the Cowork session that produced the site checklist, packaged so a fresh Claude Code session can carry on without losing anything.

Start here

  1. Open index.html (double-click it). A local browser for everything in this folder —

deliverables, docs, data, all 109 images. No internet needed. See docs/10-project-index.md. Also hosted at https://zonqor-refurb-c7cc97.pages.dev (public to anyone with the link — redeploy with ./build/deploy.sh).

  1. Read CLAUDE.md — full project context, conventions, constraints and current state.
  2. Read docs/08-whats-missing.md — the brief for what still needs building

(architecture plan, electrical plan, elevations).

  1. Look at images/measurements/ before designing anything. Those are the only

dimensioned drawings that exist. tiles_sq_m.jpg is the closest thing to a floor plan.

The one-paragraph version

Kurt is refurbishing his Zonqor apartment. Roderick (+ son) start Mon 17 Aug 2026; ULF does carpentry. A printed A4 checklist of 81 work items across 13 rooms exists and works — each item has an ID, trade, assignee, dependency, phase, and paper fields for status/date/agreed price. What does not exist: a floor plan, an electrical plan, or elevations. The designer supplied renders but no technical drawings. Twelve open questions are unanswered, two of them urgent (fireplace: remove or keep, by day 4; tiling scope: the bathrooms and balcony turn out to be already tiled, which likely makes the 145 sqm quote wrong).

Ground rules carried over

drawn is a proposal to confirm on site, and must be labelled that way.

silently breaks cross-references. This has already happened once.

Layout

PathWhat
index.htmlLocal browser for the whole project — start here. Rebuild with build/build_index.py
CLAUDE.mdMaster context. Read first.
docs/Decisions log, open questions, work plan, day-by-day, photo index, source notes, site observations, the brief for what's missing, and how the base plan was reconstructed
data/rooms.json + items.csv (81 items), original spreadsheets as CSV, PDF text
images/measurements/Dimensioned drawings, full resolution — the design source of truth
images/design/19 designer renders (visual intent only)
images/site/75 site photos, Aug 2026
build/Generator scripts
output/The deliverables: Zonqor Site Checklist (PDF + HTML) and Zonqor Base Plan (PDF)

Rebuild

cd build && python3 build_v3.py && python3 verify_refs.py && python3 pdfcheck.py
pdftoppm -png -r 90 -f 1 -l 4 "../output/Zonqor Site Checklist.pdf" /tmp/pg   # then look at the pages

Verified working on Kurt's Mac (16 Aug 2026) — a clean rebuild reproduces the shipped HTML byte-for-byte. Note the build writes output/renovation_checklist.html; the delivered HTML is a renamed copy. See CLAUDE.md §10.

01-decisions-log.md

Decisions log

Every decision Kurt made during the Cowork session, in order, with what it changed. Quoted text is his own wording. Anything not listed here is not decided.

Scope and content

#DecisionWhat it changed
1Tiles: 120×120 full house including balconies, except bathrooms; ensuite 60×30 (dimensions still to confirm)WHS-05, ENS-04. The 60×30 confirmation is still open as OQ-05.
2Known fault: "Kitchen Appliances Oven when switched cuts salvavita"OPN-05, tagged as a known issue and made dependent on the new consumer unit
3Consumer unit: "Full refresh to unit / panel" (not a repair)ENT-01
4Tiling area: 145 sq m (Roderick's quote), chosen over the 125 sqm marked on the designer's floor planWHS-05. ⚠ Now in question — see OQ-10.
5Outdoor wall: "this will be PVC / TILES - same thing - one of them to be done - still to be decided"Merged two duplicate items into one FBL-10, and created the first Open Question
6The dining item is to be called "Dining Feature Wall + Fireplace Removal"OPN-01
7"Building Wardrobe is on ULF"MBR-11 moved from Roderick to ULF

Structure and format

#DecisionWhat it changed
8"put into sections (electrical / tile fitting / plumbing) per room"Every room's items grouped by trade in a fixed order: Gypsum → Electrical → Plumbing → Tiling → Fittings
9"Add an Assigned Section and Dependency in every item. Gypsum / Tiles / Electrical / Plumbing = Roderick. Carpentry = ULF."The assignment rule. Derived from trade, never set per item.
10"make the what (title / type / text) to the left and project management part to the right … and make it expandable"Two-column item layout
11"Can you give them an ID Number for Easy Reference - Ideally Code-ID by room"The {ROOM}-{NN} ID scheme
12Print-first rebuild: "this will be printed from a PDF and my wife Christine will check-in with the worker and update the checklist per item and comments during the night + agree on prices per item"The whole document was rebuilt around paper: status/date/price/initials/comments fields, price summary, daily log, sign-off blocks
13"make it one document and compact. we will have appendix for the pictures and reference them with an ID"34 pages → 15. Pictures moved to an ID-referenced appendix (A01…)
14"still have thumbnails inline per room. just larger same photos in the appendix"Small inline thumbnails restored, full size kept in the appendix

Room order and layout (asked explicitly, answered explicitly)

#Question askedKurt's answer
15Room orderFront Balcony → Open Plan (Kitchen→Dining→Living) → Entrance → Main Corridor → Small Corridor → Main Bedroom → Ensuite → Main Bathroom → Cy Bedroom (with Window) → Rafe Bedroom (with Back Balcony Door) → Back Balcony
16Where does the Study/Office sit?"Do it under Main Bedroom" → placed immediately after Main Bedroom
17Which balcony gets the outdoor kitchen?Front Balcony → all BBQ/gas/waterproof-plug items to FBL
18What is in the Entrance?"Electrical Central Unit / Intercom" → the consumer unit was moved from the General section to ENT, and an intercom item was added
19How should the day plan be dated?Real dates, 5-day week
20Start dateThursday 13 August 2026 (sent mid-turn)

Work order

#DecisionStatus
21Roderick's proposed order: parquet clean-up / demolish feature wall → plumbing + core electricity + electrical unit + ethernet → cleanup with skip → tiling → gypsum amendments → "lets try to see whats missing after this"Adopted as phases 1–6, with six further phases added
22Kurt asked for a day-by-day turnkey plan for Roderick + sonBuilt: 67 working days, originally Thu 13 Aug → Fri 13 Nov 2026
23Start delayed — the worker was not available on 13 Aug. New start Mon 17 Aug 2026 (Kurt, 16 Aug)Whole plan re-dated: 67 working days, Mon 17 Aug → Tue 17 Nov 2026. Durations, phase order and plan-day numbers unchanged; every calendar date moved two working days later. Open-question deadlines are expressed as plan-days, so they still hold — but they now fall two working days later.

Decisions NOT made

02-open-questions.md

Open questions

None of these are answered. Deadlines are plan-days counted from the 17 Aug 2026 start (delayed from 13 Aug — the plan-day numbers are unchanged, the calendar dates moved). Any work item blocked by one names it in its depends_on field.

OQ-01 — Outdoor wall finish — PVC sheet or tiles?

Room: Front Balcony · Needed by: day 63

One or the other, never both. Kurt merged two duplicate items over this. Blocks FBL-10.

OQ-02 — Outdoor kitchen — design / equipment spec

Room: Front Balcony · Needed by: day 18

Blocks five FBL items including the gas line. Nothing outdoor can be ordered until this lands.

OQ-03 — New consumer unit — location and capacity

Room: Entrance · Needed by: day 6

ENT-01 is a full board replacement and everything electrical downstream waits on it. IMG_9845 shows the existing unit and its position.

OQ-04 — Study/Office plugs — quantity

Room: Study / Office · Needed by: day 14

Quantity was never specified. Blocks OFF-04.

OQ-05 — Ensuite tiles — confirm 60×30 dimensions

Room: Ensuite · Needed by: day 42

Now entangled with OQ-10: photos show the ensuite is already tiled, so the prior question is whether it is re-tiled at all.

OQ-06 — Living/Dining TV wall — PVC sheet spec (material, size, fixing)

Room: Open Plan · Needed by: day 64

Was a dangling reference with no question behind it; formalised.

OQ-07 — Sofa-area TV unit — electrical point spec

Room: Open Plan · Needed by: day 10

Needed before first fix reaches the living zone.

OQ-08 — Intercom — reuse or replace? audio or video?

Room: Entrance · Needed by: day 8

Changes the cable pulled at first fix, so it cannot wait. Added when Kurt defined the Entrance as 'Electrical Central Unit / Intercom'.

OQ-09 — Who books and pays for the skips?

Room: Whole House · Needed by: day 5

Two skips assumed: after demolition, and after first fix.

OQ-10 — Tiling scope — site photos show the ensuite, main bathroom and front balcony are already tiled. Does ~145 sqm still stand, and what is in it?

Room: Whole House · Needed by: before pricing

The commercially significant one. Affects the price and the 16-day tiling phase.

Partly answered, 16 Aug 2026 — see 09-base-plan.md. Measuring the reconstructed base plan shows the 125 vs 145 sqm gap is the two balconies: internal rooms less the two wet rooms come to 122.7 sqm against the designer's 125, and adding the balconies (21.6 sqm) lands on Roderick's

  1. The two figures were never in conflict — 125 is the internal floor excluding wet rooms, 145

is everything including balconies.

What is still open is the part that costs money: how much is actually being tiled? The photos show the ensuite, main bathroom and front balcony are already finished. If none of those are re-tiled the new 120×120 area is nearer 123 sqm than 145 — roughly 22 sqm of difference to settle with Roderick before agreeing a price.

OQ-11 — Fireplace — the photo shows it installed with the wall already opened beneath. Remove, or keep and rework around it?

Room: Open Plan · Needed by: day 4

OPN-01 says 'removal' but IMG_9893 shows a new-looking fireplace. Demolition day depends on the answer.

OQ-12 — Two tile formats on site: Fanal 120 and Fanal 31.6×90. What is the 31.6×90 for?

Room: Whole House · Needed by: day 29

Not in the work list at all. Seen in IMG_9894.

Priority

  1. OQ-10 — settle before agreeing any price.
  2. OQ-11 — needed by day 4, changes what happens on demolition day.
  3. OQ-03 and OQ-08 — week one, both affect first fix.
  4. OQ-09 — day 5.
  5. The rest follow the plan dates.

03-work-plan.md

Work plan

Start Mon 17 Aug 2026 · 67 working days, Mon–Fri · finish Tue 17 Nov 2026 · Roderick + son (2 people).

Delayed from the original Thu 13 Aug start (contractor unavailable). Durations and phase order are unchanged; every date shifted by two working days.

Origin

Roderick proposed: clean up old parquet / demolish the dining feature wall → plumbing / core electricity + electrical unit / ethernet → cleanup with skip → tiling → gypsum amendments → "lets try to see whats missing after this".

Six stages were missing and were added: painting, second fix electrical, carpentry, fittings and sanitary, the outdoor works, and snagging and handover.

Four work items were also missing and were added: WHS-01 strip out the parquet, WHS-04 level the screed once it is up, WHS-02 skip hire, and ENS-03 tank the ensuite wet area before tiling.

⚠ The one open argument — gypsum before tiling, not after

Roderick's order puts tiling before gypsum. That is correct for patching and making good, but wrong for three full walls: the study partition (OFF-01), the small corridor extension (SCR-01) and the sofa wall (OPN-02). Tiling under a wall that is then built on top wastes 120×120 tile and seats the wall on tile instead of slab.

The plan therefore splits it — Phase 4 builds partitions before tiling, Phase 6 keeps patching after. This has not been put to Roderick. Raise it on day one; he may have a reason.

Phases

PhPhaseDaysNotes
1Strip-out & demolition5Parquet up, feature wall and fireplace down, old fittings removed.
2First fix — plumbing, electrical, ethernet, gas16Consumer unit, chasing, all cable and pipe runs, gas line. Nothing gets closed up until this is signed off.
3Clear site — skip1Rubble and chasing debris out before tiling.
4Structural gypsum (partitions)6MOVED BEFORE TILING — see the argument below.
5Tiling16Screed levelled, wet areas tanked, 120x120 through the flat, ensuite 60x30.
6Gypsum amendments & making good2Patching at tile/wall junctions.
7Painting5Ceilings and walls, including the ensuite ceiling.
8Second fix electrical6Fit and terminate every point, hang fittings, LED strips and sensors, then test and certify.
9Carpentry — ULF (+ Roderick cut-ins)3Wardrobes, cabinets, TV unit. Roderick returns to cut in gyser/pump and ensuite switches.
10Fittings, sanitary & outdoor5Mirrors, shower glass and tray, sanitary ware, vent covers, PVC/tile outdoor finish.
11Snagging & handover2Walk the flat against the checklist, fix defects, final clean, hand over certificates.

Assumptions — all stated, none confirmed

04-day-by-day.md

Day-by-day plan

Mon 17 Aug 2026 → Tue 17 Nov 2026 · 67 working days, Mon–Fri. Roderick + son unless noted. Generated by build/gen_day_plan.py from the built checklist — regenerate rather than hand-edit.

Phase 1 — Strip-out & demolition

DayDateWorkItems
1Mon 17 AugSite set-up: protect what stays, isolate services. Start lifting parquet — Open Plan.WHS-01
2Tue 18 AugContinue parquet lift — bedrooms and corridors.WHS-01
3Wed 19 AugFinish parquet lift. Strip out old ceiling lights, bedside lamps and window plugs.WHS-01, MBR-02, MBR-03, MBR-04
4Thu 20 AugDemolish dining feature wall + fireplace. Check for live services in the wall first.OPN-01
5Fri 21 AugClear demolition debris, first skip out. Walk the back balcony and write in its scope.WHS-02, BBL-01

Phase 2 — First fix — plumbing, electrical, ethernet, gas

DayDateWorkItems
6Mon 24 AugSet out and confirm the new consumer unit position at the entrance (OQ-03 must be answered).ENT-01
7Tue 25 AugInstall consumer unit, terminate mains. Nothing else energises until this is in.ENT-01
8Wed 26 AugChase entrance and corridors. Pull intercom cable.ENT-02, MCR-01, MCR-02
9Thu 27 AugOpen Plan kitchen zone — ceiling lights, hood, dedicated oven circuit.OPN-03, OPN-04, OPN-05
10Fri 28 AugOpen Plan dining/living — cabinet lights, rope light, switches, plugs, TV point.OPN-06 to OPN-10
11Mon 31 AugEthernet runs back to the entrance unit. Relocate double-pole switch.OPN-11, OPN-12, OFF-08
12Tue 01 SepKitchen sink onto main feed, RO connection left ready.OPN-13
13Wed 02 SepMain bedroom first fix — ceiling, rope, bedside, vanity, wardrobe LED.MBR-05 to MBR-09
14Thu 03 SepStudy/office first fix — ceiling, under-desk LED, plugs, switches, LED bar.OFF-02 to OFF-07
15Fri 04 SepCy's bedroom first fix.CYB-01 to CYB-05
16Mon 07 SepRafe's bedroom first fix.RAF-01 to RAF-05
17Tue 08 SepMain bathroom — gyser position, pipework and point. Ensuite switch route.BTH-01
18Wed 09 SepFront balcony — side lights, floor lights, switches, waterproof plugs, camera point.FBL-01 to FBL-08
19Thu 10 SepFront balcony gas line + pressure test and certificate.FBL-09
20Fri 11 SepCorridor emergency light short circuit. Test all existing feeds.MCR-03, MCR-04, WHS-03
21Mon 14 SepFirst-fix sign-off walk. Photograph every open chase before anything is closed up.all first fix

Phase 3 — Clear site — skip

DayDateWorkItems
22Tue 15 SepSecond skip — chasing debris out, sweep, floors prepped for tiling.WHS-02

Phase 4 — Structural gypsum (partitions)

DayDateWorkItems
23Wed 16 SepSet out and frame the study partition.OFF-01
24Thu 17 SepStudy partition — rockwool insulation and inner two layers of board.OFF-01
25Fri 18 SepStudy partition — outer board, tape and joint.OFF-01
26Mon 21 SepSmall corridor partition extended +10cm for the new cabinets.SCR-01
27Tue 22 SepGypsum behind sofa (250mm x 1m) including the rope-light housing.OPN-02
28Wed 23 SepMain bedroom — close off old ceiling light area.MBR-01

Phase 5 — Tiling

DayDateWorkItems
29Thu 24 SepScreed check and levelling — open plan.WHS-04
30Fri 25 SepScreed levelling — bedrooms and corridors.WHS-04
31Mon 28 SepTank ensuite floor and shower walls. Must be done before any ensuite tile.ENS-03
32Tue 29 SepSet out 120x120 in the open plan — agree the starting line and cut positions with Christine.WHS-05
33Wed 30 SepTile open plan — living/dining.WHS-05
34Thu 01 OctTile open plan — kitchen end.WHS-05
35Fri 02 OctTile entrance and main corridor.WHS-05
36Mon 05 OctTile main corridor and small corridor.WHS-05
37Tue 06 OctTile main bedroom.WHS-05
38Wed 07 OctTile study and Cy's bedroom.WHS-05
39Thu 08 OctTile Rafe's bedroom.WHS-05
40Fri 09 OctTile front balcony — weather permitting.WHS-05
41Mon 12 OctTile back balcony — weather permitting.WHS-05
42Tue 13 OctEnsuite 60x30 tiling — walls.ENS-04
43Wed 14 OctEnsuite tiling — floor, then grout.ENS-04
44Thu 15 OctGrout throughout, clean down, protect finished floors.WHS-05

Phase 6 — Gypsum amendments & making good

DayDateWorkItems
45Fri 16 OctPatching and making good at all tile/wall junctions.
46Mon 19 OctFinish patching, sand, prep every surface for paint.

Phase 7 — Painting

DayDateWorkItems
47Tue 20 OctPrime and seal all new gypsum.
48Wed 21 OctCeilings — bedrooms and study.
49Thu 22 OctCeilings — open plan and corridors. Ensuite ceiling.ENS-01
50Fri 23 OctWalls — first coat throughout.
51Mon 26 OctWalls — second coat and cutting in.

Phase 8 — Second fix electrical

DayDateWorkItems
52Tue 27 OctFit sockets and switches — open plan and entrance. Intercom unit on.ENT-02
53Wed 28 OctFit sockets and switches — corridors and bedrooms.
54Thu 29 OctHang light fittings — open plan and corridors.
55Fri 30 OctHang light fittings — bedrooms and study. Rope and LED strips.
56Mon 02 NovWardrobe LED sensors, under-furniture LED, LED bar at study door.MBR-09, CYB-03, RAF-03
57Tue 03 NovFull circuit test, RCD test, certification. Confirm the oven no longer trips.ENT-01, OPN-05

Phase 9 — Carpentry — ULF (+ Roderick cut-ins)

DayDateWorkItems
58Wed 04 NovULF starts wardrobes — main bedroom. Roderick on call for cut-ins.MBR-11
59Thu 05 NovULF wardrobes — Cy's and Rafe's bedrooms.
60Fri 06 NovRoderick cuts in gyser/pump switches and the ensuite switch.MBR-10, ENS-02

Phase 10 — Fittings, sanitary & outdoor

DayDateWorkItems
61Mon 09 NovShower glass and tray; ensuite finished.ENS-05
62Tue 10 NovBathroom sanitary — sink, cabinets, flush accessories, mirror.BTH-02 to BTH-06
63Wed 11 NovMirrors — corridor 3x1.5m, office 2x2m, main bedroom vanity.MCR-06, OFF-09, MBR-12
64Thu 12 NovOutdoor wall finish — PVC or tiles, whichever OQ-01 decided.FBL-10
65Fri 13 NovVent covers, PVC sheet on TV wall, remaining fittings.MCR-05, OPN-14

Phase 11 — Snagging & handover

DayDateWorkItems
66Mon 16 NovSnagging walk with Christine against this checklist, room by room.all
67Tue 17 NovFix snags, final clean, hand over certificates and warranties.all

05-photo-index.md

Photo index — 75 site photos, August 2026

Kurt photographed the flat before works. Originals were HEIC (135 MB) and live on his Mac at ~/Desktop/HomeWorks&ReDesign/Site Photos/_Unsorted/Zonqor Works/. This package carries 1100px JPEGs in images/site/. IMG_9897IMG_9900 are stills extracted from 4 videos (suffixed _frame).

Status: 21 placed into the document, 54 not. The unplaced ones are mostly close-ups of switch plates and sockets that look identical across rooms. Kurt was asked for the order he walked the flat, so they could be assigned by photo-number range — he has not answered. That is still the fastest route to placing them.

Descriptions below are from visual inspection. Room attributions in the last table are unknown, not guesses to be trusted.

Placed in the document (21)

PhotoRoomWhat it shows
IMG_9826FBLfront balcony — BBQ corner, existing feed & camera
IMG_9827FBLfront balcony looking out, adhesive stacked
IMG_9828FBLfront balcony — already tiled, membrane at edge
IMG_9895FBLsliding door to front balcony
IMG_9834OPNopen plan overview — parquet still down, Fanal 120 tiles delivered
IMG_9835OPNkitchen island, units wrapped
IMG_9839OPNdining feature wall + fireplace, wall opened below
IMG_9893OPNfireplace close-up — installed, wall broken beneath
IMG_9840OPNliving — existing socket run
IMG_9896OPNgypsum board delivered on site
IMG_9845ENTEXISTING CONSUMER UNIT + thermostat + entrance door — key for OQ-03
IMG_9849MCRmain corridor looking through
IMG_9850MCRmain corridor — existing cove lighting
IMG_9843MCRcorridor switch position
IMG_9872BTHmain bathroom — existing grey marble finish
IMG_9873BTHmain bathroom — WC & window
IMG_9875BTHmain bathroom — ceiling & downlights
IMG_9886ENSensuite — existing shower enclosure
IMG_9888ENSensuite — shower tray & glass (to be changed, ENS-05)
IMG_9889ENSensuite — WC & window
IMG_9890ENSensuite — overview

Identified but not placed (18)

Room is confident; they were left out to keep the document compact. Add if useful.

PhotoLikely roomWhat it shows
IMG_9829FBLbalcony exterior wall, sea view
IMG_9894FBLFanal 31.6x90 tile boxes stacked on balcony — source of OQ-12
IMG_9898fFBLvideo still: balcony with Isomat AK22 adhesive stacked
IMG_9832OPNfeature wall niche/alcove
IMG_9836OPNcorner, skirting, parquet
IMG_9837OPNwall switch plate
IMG_9838OPNceiling downlights + kitchen units
IMG_9841OPNbuilt-in bench with socket
IMG_9842OPNbench/shelf run
IMG_9892OPNrecessed hatch with AV/equipment inside
IMG_9897fOPNvideo still: kitchen island
IMG_9844MCRcorridor/entry area with hooks
IMG_9864MCRcorridor ceiling downlight
IMG_9885ENSensuite WC + flush plate
IMG_9887ENSensuite ceiling/shower from above
IMG_9891ENSdoorway, taped
IMG_9874BTHgrey marble bathroom, bedding stored in front

Unidentified (36) — need Kurt's walk order

Bedrooms (MBR / CYB / RAF / OFF) are visually near-identical in close-up: white walls, wood-look floor, wall-mounted AC. Known distinguishers: Cy's has a window, Rafe's has the back balcony door, Main Bedroom has the ensuite door, the Study partition does not exist yet.

PhotoWhat it shows
IMG_9830narrow space, two black sockets, tiled floor
IMG_9831corner, tiled floor, white walls
IMG_9833window/door reveal, tiled
IMG_9846bedroom — AC unit, wooden door, switch panel, kitchen visible left
IMG_9847room with ceiling downlights and hooks
IMG_9848partition wall with switch, bed behind
IMG_9851switch plate close-up
IMG_9852socket close-up with cable, skirting
IMG_9853switch plate close-up, taped edge
IMG_9854multi-gang switch, bath/tub visible left
IMG_9855room with window, sockets at skirting
IMG_9856ceiling with hanging cable
IMG_9857socket close-up, taped edge
IMG_9858room corner, double socket, cable
IMG_9859AC unit above window
IMG_9860sockets at skirting, ladder present
IMG_9861corner with socket + extension lead
IMG_9862switch plate, doorway beyond
IMG_9863switch plate close-up
IMG_9865switch plate close-up
IMG_9866floor/wall junction, taped
IMG_9867room corner, two sockets, wood-look floor
IMG_9868door frame taped, socket low
IMG_9869room corner, sockets
IMG_9870bedroom with window, socket
IMG_9871bedroom — AC above window
IMG_9876wall with sockets at skirting
IMG_9877blank wall, single socket
IMG_9878switch plate, opening beyond
IMG_9879ceiling with 3-lamp fitting + downlights, AC, window
IMG_9880bedroom — AC, window, socket
IMG_9881switch + double socket close-up
IMG_9882room corner, switch + socket
IMG_9883wall with switch above triple socket
IMG_9884wall with three sockets in a row
IMG_9899fvideo still: bedroom with window + AC
IMG_9900fvideo still: bedroom with window + AC

Notable individual photos

thermostat panel. Directly relevant to OQ-03 and to designing the new board.

beneath it. The source of OQ-11.

large-format stone. The evidence behind OQ-10 (tiling scope).

06-source-data.md

Source data — what exists and how far to trust it

Spreadsheets

data/works_Sheet1.csv — the origin of the whole checklist

From Zonqor 2025 - Works.xlsx (67 MB, still on Kurt's Mac). 126 rows × 20 columns: # / NEW-EXIST / ACTION / ROOM / LOCATION / DESCRIPTION / FOR / Gypsum-paint works / Furniture Setup / Electrical Points / Light Points / Switch Points / Water Points / Gas Points / IMAGE.

The point-count columns are the only quantitative electrical data that exists. They give counts, never positions. Reliability: good for what and how many, silent on where.

data/works_Sheet2.csv — Roderick's cost estimate

14 rows. Contains the 145 msq tiling figure Kurt chose over the 125 sqm on the designer's plan. Now in question — see OQ-10.

data/lights_plugs_to_buy.csv

Sheet to buy from lights _ plugs _ mirrors.xlsx. Shopping list of switches, plugs and lights per room. Source of the mirror sizes (corridor 3 m × 1.5 m, office 2 m × 2 m) and the wardrobe LED sensors.

data/lights_plugs_needed_for_Roderick_-ULF.csv

Sheet needed for Roderick |ULF. Room-by-room wiring notes. This is where several items came from that are nowhere else: the ethernet behind the sofa, the LED bar near the office door, the emergency-light short circuit in the back corridor, 'unit for salva vita', and the outdoor kitchen/gas/tiles notes. The sheet name is also how it was established that ULF is a person, not an abbreviation.

Drawings

data/pdf_text/ holds the extracted text, but the PDFs are drawings and the text is thin — the rendered images in images/measurements/ are what you actually need to read.

FileContainsUse for
tiles_sq_m.jpgWhole-flat floor plan, '125 sqm' markedThe base plan. Room shapes and adjacencies.
openplan_dimensions_1/2/3.jpgOpen-plan elevations, mmOpen-plan setting out
main_bedroom_dimensions_1/2.jpgMain bedroom, mmMain bedroom
kids_bedroom_dimensions_1/2.jpgKids' bedrooms, mmCy / Rafe
corridor_dimensions_1.jpgCorridor, mmMain corridor
Copy_of__furniture_+_gypsum_measurement_p1.jpgStudy partition build-up; 'wardrobe around ensuite door'OFF-01, MBR-11
furniture_layout_ELEVATIONS_p1.jpgThe designer's only elevation sheetPartial elevations
balcony_plan_p1.jpgBalcony layoutFBL / BBL
study_and_bedroom_revised_p1.jpg, kids_bedroom_only_p1.jpg, Copy_of_kids_bedroom_measurement_opt4_p1.jpgLayout optionsBedrooms

No dimensioned drawing exists for: Entrance, Small Corridor, Ensuite, Main Bathroom, either balcony, or the Study as a room (only its partition spec).

Verbatim spec worth keeping

Study partition, from the gypsum measurement PDF:

Sound proof gypsum partition to consist of: 1. Inside of study to have 2 layers of boards. 2. Middle part to have a profile of 75 mm filled with rockwool insulation. 3. Outer part of study to have a single board.

Derived data

data/rooms.json and data/items.csv — the current 81 items with IDs, trade, assignee, phase, dependency, tags and notes. Generated from build/rooms_data2.py; regenerate rather than hand-edit.

07-site-observations.md

Site observations — what the photos changed

Kurt photographed the flat in August 2026, before works. Five findings materially affect the plan. None are resolved.

1. The flat is in far better condition than 'refurbishment' implies

The ensuite (IMG_98859890) and main bathroom (IMG_98729875) are finished in large-format travertine and grey marble, with shower glass, tray and sanitary ware installed. The front balcony (IMG_9828) is already tiled. The parquet is still down in the living/dining, corridors and bedrooms.

This is a refresh, not a strip-out. It affects how the scope should be described to anyone new.

2. The tiling quantity is probably wrong → OQ-10

WHS-05 reads '120×120 full house, including balconies, ~145 sq m'. If the bathrooms and the front balcony are already tiled, the real area is the parquet footprint only — materially less. This moves both the price and the 16-day tiling phase, and it revives the unexplained gap between the 125 sqm marked on the designer's floor plan and the 145 sqm in Roderick's quote.

Settle this before agreeing any price. Computing areas off the base plan may answer it directly.

3. The fireplace looks new, but the wall is already open → OQ-11

IMG_9893 and IMG_9839 show a linear gas fireplace, apparently newly installed, set into the feature wall — with the wall beneath it already broken open. OPN-01 says 'Dining Feature Wall + Fireplace Removal'. Either the fireplace is being torn out, or it is being kept and the wall reworked around it. Demolition is day 4 (Thu 20 Aug), so this cannot wait.

It also raises a second question nobody has answered: has demolition already started?

4. An unexplained second tile format → OQ-12

IMG_9834 shows Fanal 120 boxes in the open plan. IMG_9894 shows Fanal 31.6 × 90 stacked on the balcony. The work list only knows about 120×120 and the ensuite 60×30. Something is on site that nobody has accounted for.

5. The existing consumer unit is documented

IMG_9845 shows the existing board, its position beside the entrance door, and an adjacent thermostat panel. This is the single most useful photo for OQ-03 and for designing the new board (ENT-01).

Materials already delivered

Fanal tiles in both formats, Isomat AK22 tile adhesive (IMG_9827, IMG_9898_frame), gypsum board (IMG_9896). Worth reconciling against a materials schedule, which does not yet exist.

Also seen, not yet explained

08-whats-missing.md

What still needs building

The brief for the Claude Code work. The checklist is done; these are not.

Status, 16 Aug 2026 — item A is done. A reconstructed base plan now exists: output/Zonqor Base Plan.pdf, geometry in build/plan_data.py, method and confidence grades in docs/09-base-plan.md. It also produced an answer to OQ-10: the 125 / 145 m² gap is the two balconies. The "measure these on site" list in section A below is still outstanding — the plan is a reconstruction, not a survey, and items B and C have not been started.

A. Architecture / floor plan

Goal. A dimensioned plan of the flat, room by room, good enough for the worker to set out from and for an electrical plan to be drawn on top of.

What you have to work with

SourceWhat it givesReliability
images/measurements/tiles_sq_m.jpgThe only whole-flat floor plan. Room shapes, relative positions, marked "125 sqm".Shape: good. Area figure: disputed (quote says 145). Not dimensioned room-by-room.
openplan_dimensions_1/2/3.jpgOpen-plan elevations with mm dimensionsGood, but elevations not plan
main_bedroom_dimensions_1/2.jpgMain bedroom, mmGood
kids_bedroom_dimensions_1/2.jpgKids' bedrooms, mmGood
corridor_dimensions_1.jpgCorridor, mmGood
Copy_of__furniture_+_gypsum_measurement_p1.jpgStudy partition build-up spec; wardrobe-around-ensuite-door noteGood
furniture_layout_ELEVATIONS_p1.jpgThe designer's only elevation sheetPartial coverage
images/site/ (75 photos)Actual room shapes, window/door positions, existing finishesVisual only, no scale

Rooms with no dimensional drawing at all: Entrance, Small Corridor, Ensuite, Main Bathroom, both balconies, Study/Office (only the partition spec, not the room).

Hard constraint. There is no survey. A plan built from this is a reconstruction, and its accuracy varies by room. Label it as such, and mark which dimensions are read from a drawing versus inferred from the plan image. The honest deliverable is a plan with a confidence key, plus a short "measure these on site" list for the gaps.

Suggested first step. Read tiles_sq_m.jpg at full resolution, trace the room outlines and adjacencies, then cross-check each room against its dimension drawing where one exists. That gives a scaled base plan and an explicit list of what could not be verified.


B. Electrical plan

Goal. What the designer never produced. Specifically:

  1. Consumer unit / distribution board schedule — circuits, ratings, RCD/RCBO arrangement.

ENT-01 is a full replacement, so this is a new board designed from scratch. IMG_9845 shows the existing unit and its position — start there.

  1. Circuit schedule — which points sit on which circuit. The oven (OPN-05) trips the current

RCD and is a strong candidate for its own dedicated circuit; that is currently an open item, not a decision.

  1. Point layout per room — plotted on the base plan from A.
  2. Cable routes and chases — first fix is 16 days and is the phase everything else waits on.
  3. Ethernet — several points (OPN-11, OFF-08, CYB-05, RAF-05) all run back to the

central unit at the entrance. That hub needs designing: patch location, cable count, containment.

  1. Special items — sensor-activated wardrobe LEDs (MBR-09, CYB-03, RAF-03), rope/cove

lighting in several rooms, LED bar at the study door, the emergency-light short circuit in the corridor (MCR-04), outdoor waterproof points and the gas line on the front balcony.

What you have. The point counts per room, from data/works_Sheet1.csv (columns: Electrical Points, Light Points, Switch Points, Water Points, Gas Points) and the room-by-room wiring notes in data/lights_plugs_needed_for_Roderick_-ULF.csv. data/rooms.json carries the same information per item, with tags like "Electrical · 4 light points".

Hard constraint. No positions exist for any point. The counts say how many, never where. Any placement is a proposal for Roderick to confirm on site. The existing checklist deliberately labels its point summary "indicative — not a wiring diagram; confirm on site". Keep that discipline: a confidently-wrong electrical drawing on a live site is a safety problem, not a cosmetic one. Malta follows BS 7671-style practice; if you state anything normative, say what you are assuming.


C. Elevations

Per room: switch and socket heights, wardrobe internals, the feature-wall build-up, the vanity (the mirror is specified at 75 cm floor-to-vanity-surface), the corridor mirror (3 m × 1.5 m) and office mirror (2 m × 2 m). The designer's single elevation sheet is images/measurements/furniture_layout_ELEVATIONS_p1.jpg.


D. Smaller gaps

could be assigned by number range — unanswered. See 05-photo-index.md.

subsequent dates shift.

Two tile formats are already on site (Fanal 120 and Fanal 31.6×90) and one of them is unexplained.


Sequence worth following

  1. Base plan from tiles_sq_m.jpg + the dimension drawings → flag what needs measuring
  2. Confirm the tiling scope question (OQ-10) against that plan — it may answer itself once areas are computed
  3. Electrical point layout on the base plan → circuit grouping → board schedule
  4. Elevations for the rooms with carpentry
  5. Materials schedule

Each of these can be delivered in the same print-first A4 style as the checklist, and should carry the same honesty markers about what is measured versus assumed.

09-base-plan.md

Base plan — how it was built, and how far to trust it

Deliverable: output/Zonqor Base Plan.pdf (3 pages, A4, print-first). Geometry: build/plan_data.py. Generator: build/build_plan.py.

This closes item A of 08-whats-missing.md. It does not close the need for a survey.


What it is

A dimensioned floor plan of the whole flat — 12 rooms plus two balconies — with a floor area per room, a confidence grade per room, and a list of what has to be measured on site.

It is a reconstruction traced from a designer's drawing, not a measured survey. Nobody has put a tape on this flat. The drawing it comes from carries its own disclaimer: "All dimensions to be verified on site prior to fabrication and installation."


How the scale was established

This is the part that makes the plan usable rather than decorative, so it is worth recording.

images/measurements/kids_bedroom_only_p1.jpg carries a printed metric scale bar. Detecting its tick marks gives:

images/measurements/study_and_bedroom_revised_p1.jpg is the same drawing plotted larger. Its envelope measures 1.3137× the first sheet, giving 78.68 px/m. That sheet is A4 at 200 dpi, where 78.68 px/m works out to 9.99 mm per metre on paper — exactly 1:100.

Two independent routes to the same scale, agreeing to 0.08%. The second sheet is the base, because it is higher resolution and its callout photos sit clear of the drawing.

How the geometry was extracted

The architectural shell was separated from the furniture by colour: walls are drawn in black (near-neutral, dark), furniture and annotation in blue, red, teal and orange. Masking to max(RGB) < 190 and saturation < 45 leaves the shell alone. Wall lines were then found as projection-profile peaks per band, and the envelope traced as the outermost dark pixel per row. At 200 dpi and 1:100, one pixel is ~13 mm, so single readings are good to roughly ±0.03 m — which is far better than the underlying drawing deserves. The stated ±3% comes from doubting the source drawing, not from the measurement of it.


The flat

A single long bay: 7.21 m wide × 26.00 m deep externally, plus the front balcony beyond the south wall. External walls are 0.22 m thick, consistently on all four sides.

The west side steps in twice. Those two indentations are light wells, and they are the strongest single check on the whole reconstruction: the four rooms that take windows off them — main bathroom, study, ensuite, entrance — are exactly the four rooms the site photos show windows in, and nothing else on that side has one.

A second check: CLAUDE.md §4 records the room order Kurt gave as a physical walk through the flat. Reading the plan from the front door northwards reproduces that order. The reconstruction was not fitted to it — they agree independently.

Only one height is known anywhere in the flat: 2.61 m floor to ceiling in the dining area (830 + 1050 + 730 mm off the feature-wall elevation, openplan_dimensions_1.jpg).


The finding that matters: OQ-10

The 125 m² / 145 m² discrepancy is the two balconies. The two figures were never in conflict — they measure different things.

Internal rooms, measured off the plan133.2
less main bathroom (7.3) and ensuite (3.2)122.7
designer's figure marked on tiles_sq_m.jpg125
back balcony (5.8) + front balcony (~15.8)21.6
Roderick's quote145

122.7 against the designer's 125 is a 1.8% agreement — inside the tolerance of this reconstruction. And 125 + 21.6 = 146.6, against Roderick's 145.

So: 125 m² is the internal floor excluding the wet rooms. 145 m² is everything, balconies included. Both are defensible descriptions of the flat.

That reframes the commercial question. It is not "which number is right" — it is "how much of it is actually being tiled?" The photographs say the ensuite, the main bathroom and the front balcony are already finished. If none of those are re-tiled, the new 120×120 area is nearer 123 m² than 145 — a difference of about 22 m² to settle before a price is agreed.

⚠ This is arithmetic off a reconstructed plan. It does not establish what Roderick intended to include, and it is not a substitute for asking him. It tells you what to ask and roughly how much money is in the answer.


Confidence, per room

Graded on the plan and in the schedule:

dining/kitchen end of the open plan.

open plan.

The three weakest points, stated plainly

  1. The small corridor (SCR). Geometry puts it between the kids' rooms and the bathroom.

The checklist's room order puts it between the main corridor and the main bedroom. These disagree, and SCR-01 ("extend by 10 cm for the new cabinets") is not specific enough to settle it. Confirm what SCR-01 actually refers to before Phase 4.

  1. Which kids' bedroom is which. CYB is "with window", RAF is "with back balcony door".

Both north rooms front the back balcony. The plan cannot tell them apart, so the labels on it are positional (north-west / north-east) and the mapping to Cy and Rafe is unresolved.

  1. The back balcony. A full-width strip only 0.85 m deep. It is read as a balcony because

tiles_sq_m.jpg excludes it from the shaded area, but it could be a storage recess. BBL already had "scope not established" against it; that is still true.


What must be measured on site

The plan's own last page lists these. In priority order:

  1. One diagonal per room — nothing here has been checked for square.
  2. Which kids' room is Cy's and which is Rafe's.
  3. Where the small corridor actually is.
  4. The ensuite — internal size, and whether it is being re-tiled at all (drives OQ-05 and OQ-10).
  5. Floor-to-ceiling height in every room — only the dining area is known.
  6. Door and window positions and widths — not traced, and needed before any electrical layout.
  7. The front balcony's curved parapet — chord and depth. A curve cannot be scaled reliably.

Rebuilding it

cd build
python3 build_plan.py     # writes ../output/base_plan.html
python3 pdfplan.py        # renders ../output/Zonqor Base Plan.pdf
pdftoppm -png -r 110 "../output/Zonqor Base Plan.pdf" /tmp/p    # then look at the pages

plan_data.py is the single source of truth for the geometry — room rectangles, the envelope polygon, wall thickness, confidence grades and the measure-on-site list all live there. The generator derives the drawn scale from the viewBox so the "1:130" printed on the sheet cannot drift away from what is actually plotted.

Next: the electrical plan (item B of 08-whats-missing.md) is drawn on top of this base. Do not commit point positions until at least items 1, 5 and 6 above have been measured.

10-project-index.md

index.html — the local project browser

Open index.html in the project root. Double-click it. That is the whole instruction.

It works with no internet, no server, no build step and no installed dependencies. Everything it needs is already on disk beside it.


What it gives you

One page, 35 panes, sidebar navigation with a filter box:

SectionWhat is in it
StartOverview — key dates, counts, measured areas, all 12 open questions, and the three things to settle first
DeliverablesSite Checklist, Base Plan, Photo Sort Sheet — every page shown as an image, plus a link to the real PDF
DocumentsCLAUDE.md, README.md and all 10 docs, rendered from markdown
Dataitems.csv, rooms.json, both works_ sheets, both lights_plugs_ sheets, and the extracted PDF text
Images15 measurement drawings, 19 designer renders, 75 site photos — captioned, click to enlarge
Build scriptsAll the Python, so you can read what generated what

The filter box searches nav items and photo captions, so typing consumer in the site-photos pane narrows to IMG_9845.


Rebuilding it

cd build
python3 build_index.py        # writes ../index.html

Run it after changing any doc, any data file, or after rebuilding either PDF. It takes a second or two. It regenerates PDF page previews only when the PDF is newer than them.


Three constraints that matter if you move things

  1. index.html must stay in the project root. Images and PDF previews are referenced by

relative path (images/site/…, output/preview/…). Move the file and every image breaks. It is a browser for this folder, not a portable document.

  1. Text is embedded; images are not. Docs, CSVs, JSON and scripts are pre-rendered into the

HTML by Python. Images and PDF pages are linked. This is deliberate:

page would open and silently show nothing. Pre-rendering in Python avoids that entirely.

  1. PDFs are shown as page images, not in an <iframe>. An iframe depends on the browser

having a working PDF viewer under file://. That is unreliable — it renders nothing at all in headless Chromium and is inconsistent in Safari. The page images always display. Print from the real PDF, not from the previews — the previews are 96 dpi JPEGs, fine for reading on screen, not for Christine's paper copy.


How the markdown gets rendered

build_index.py carries a ~60-line markdown renderer (headings, tables, fenced code, blockquotes, lists, rules, bold/italic/code/strike/links). That is everything these docs actually use. There is no JavaScript markdown library and no CDN, which is what makes the page work offline. If you add markdown syntax the docs do not currently use, check it renders — the renderer is deliberately minimal, not general.

Photo captions in the galleries are parsed out of docs/05-photo-index.md at build time, so improving that file improves the gallery for free.


It is also hosted

https://zonqor-refurb-c7cc97.pages.dev — Cloudflare Pages, account info@cksloop.com, project zonqor-refurb-c7cc97, first deployed 17 Aug 2026.

Cloudflare Pages serves a directory, which is why the site works unmodified: the relative paths to images/ and output/ resolve exactly as they do locally, at full resolution, with the real PDFs downloadable. Nothing had to be downscaled or embedded.

To redeploy after any change:

./build/deploy.sh          # rebuilds index.html, stages, uploads

The script deploys a staged copy, not the project folder. build/, data/, docs/ and _archive/ (the 21 MB handoff zip) are deliberately not uploaded — the docs and data are already embedded inside index.html, and the rest is nobody else's business.

⚠ The hosted copy is public

A *.pages.dev URL is readable by anyone who has it. There is no password on it. The page carries photos of the inside of the flat, the contractor and carpenter names, and the pricing structure. The project name is deliberately unguessable (-c7cc97) so it will not be found by accident, but that is obscurity, not security.

If it needs to be genuinely private, put Cloudflare Access in front of it — Cloudflare dashboard → Zero Trust → Access → Applications → add zonqor-refurb-c7cc97.pages.dev, and allow only your own and Christine's email. That gives a real login. Until then, treat the URL itself as the secret.

Two Cloudflare behaviours worth knowing: it redirects /index.html to /, and it strips .html from paths (/output/Zonqor Site Checklist.html/output/Zonqor Site Checklist). Both are 308 redirects that browsers follow, so every link on the page still works.


Regenerating everything from scratch

If you ever need to rebuild the whole project offline, in order:

cd build
python3 build_v3.py        # checklist HTML   (stdlib only)
python3 verify_refs.py     # MUST pass        (needs beautifulsoup4)
python3 gen_day_plan.py    # rewrites docs/04-day-by-day.md
python3 pdfcheck.py        # checklist PDF    (needs playwright + local chromium)
cp ../output/renovation_checklist.html "../output/Zonqor Site Checklist.html"

python3 build_plan.py      # base plan HTML   (stdlib only)
python3 pdfplan.py         # base plan PDF

python3 build_index.py     # index.html       (needs poppler's pdftoppm for page previews)

Everything above runs offline. Python 3.14 is at /opt/homebrew/bin/python3; beautifulsoup4, playwright and its Chromium (~/Library/Caches/ms-playwright/chromium-1208) and poppler (/opt/homebrew/bin/pdftoppm) are all installed locally. Nothing fetches anything.

The only pieces that are not installed are pandas, pdfplumber and pdf2image — and none of them are needed for any of the above. They were one-off tools used to extract data/ from Kurt's original spreadsheets, which is already done.

items.csv

data/items.csv

idroom_coderoomtradeassignedphasetitledepends_ontagsnotes
WHS-01WHSWhole HouseGypsum / StructuralRoderick1Strip out old parquet flooring throughoutstartGypsum; Remove existing; NEWLift and remove the existing parquet across the flat. First job on site — everything else waits on the floor being clear.
WHS-02WHSWhole HouseGypsum / StructuralRoderick3Skip hire and waste removalWHS-01 — parquet strippedGypsum; NEWAt least two skips: one after strip-out/demolition, one after first fix and chasing. Confirm who books and pays for the skip.
WHS-03WHSWhole HouseElectricalRoderick2Fix electricity problems and pointsENT-01 — new consumer unitElectricalDiagnose and repair existing faults; check/replace faulty sockets and switches; add points as needed throughout the flat.
WHS-04WHSWhole HouseTilingRoderick4Check and level floor screed after parquet removalWHS-01 — parquet strippedTiling; NEWOnce the parquet is up, check the screed for level and damage. Patch/level before tiling — 120x120 tiles are unforgiving on an uneven floor.
WHS-05WHSWhole HouseTilingRoderick4Lay 120x120 tiles — full house, including balconies (~145 sq m)WHS-04 — floor levelled; first-fix completeTiling120x120 throughout the flat, ~145 sq m per Roderick's quote. ! Site photos show the ensuite, main bathroom and front balcony are ALREADY TILED — re-measure the actual parquet area before this price is agreed (OQ-10).
FBL-01FBLFront BalconyElectricalRoderick22 side lights — BBQ sidestartElectrical; 2 light points
FBL-02FBLFront BalconyElectricalRoderick21 electrical/USB point — BBQ side (for camera)startElectrical; 1 point
FBL-03FBLFront BalconyElectricalRoderick2Central floor lights (under railing) — mini lights or LED stripstartElectrical; 1 light point
FBL-04FBLFront BalconyElectricalRoderick2Switch for balcony side lightsstartElectrical; 1 switchAt the current kitchen/balcony switch position.
FBL-05FBLFront BalconyElectricalRoderick2Switch for balcony floor lightsstartElectrical; 1 switchAt the current kitchen/balcony switch position.
FBL-06FBLFront BalconyElectricalRoderick2Rope lighting — front (outdoor)OQ-02 — outdoor kitchen specElectrical
FBL-07FBLFront BalconyElectricalRoderick22 outdoor wall lightsOQ-02 — outdoor kitchen specElectrical; 2 points
FBL-08FBLFront BalconyElectricalRoderick22 waterproof plugs + LED kit for outdoor kitchen unitsOQ-02 — outdoor kitchen specElectrical
FBL-09FBLFront BalconyPlumbing / GasRoderick2Run gas line for outdoor kitchenOQ-02 — outdoor kitchen specGas/PlumbingGas run must be pressure-tested and certified. Confirm bottle vs. mains supply and where the bottle housing sits.
FBL-10FBLFront BalconyFittings / CarpentryULF9Outdoor wall finish — PVC sheet OR tilesOQ-01 — PVC vs. tiles; elec & gas rough-in (this room)Fittings/Tiling; ! To confirmOne or the other, not both — still to be decided. See OQ-01.
OPN-01OPNOpen Plan — Kitchen -> Dining -> LivingGypsum / StructuralRoderick1Dining Feature Wall + Fireplace RemovalstartDining; Gypsum; Remove existingRemove the existing gypsum feature wall/partition and the fireplace to open the living/dining space. Confirm the final opening size before starting. Check for services in the wall before breaking.
OPN-02OPNOpen Plan — Kitchen -> Dining -> LivingGypsum / StructuralRoderick5Add gypsum behind sofaOPN-01 — feature wall removedLiving; Gypsum250mm wide x 1m high.
OPN-03OPNOpen Plan — Kitchen -> Dining -> LivingElectricalRoderick23 ceiling lightsgypsum (this room)Kitchen; Electrical; 3 light pointsReuses an existing electrical point.
OPN-04OPNOpen Plan — Kitchen -> Dining -> LivingElectricalRoderick2Hood electrical pointgypsum (this room)Kitchen; Electrical; 1 pointReuses an existing electrical point.
OPN-05OPNOpen Plan — Kitchen -> Dining -> LivingElectricalRoderick2Fix oven electrical point — currently trips the salvavita (RCD)ENT-01 — new consumer unitKitchen; Electrical; 2 points; Known issueOven trips the salvavita when switched on. Check wiring, the connection point, and whether the oven needs its own dedicated circuit off the new unit.
OPN-06OPNOpen Plan — Kitchen -> Dining -> LivingElectricalRoderick2Light points for dining cabinetsgypsum (this room)Dining; Electrical; 1 light point
OPN-07OPNOpen Plan — Kitchen -> Dining -> LivingElectricalRoderick2Rope lighting within the new gypsum wall (behind sofa)OPN-02 — gypsum behind sofaLiving; Electrical; 1 light point
OPN-08OPNOpen Plan — Kitchen -> Dining -> LivingElectricalRoderick2Switch for sofa rope lightinggypsum (this room)Living; Electrical; 1 switch
OPN-09OPNOpen Plan — Kitchen -> Dining -> LivingElectricalRoderick22 plugs near sofagypsum (this room)Living; Electrical; 2 points
OPN-10OPNOpen Plan — Kitchen -> Dining -> LivingElectricalRoderick2TV unit electrical pointOQ-07 — TV unit electrical specLiving; Electrical; ! To confirm
OPN-11OPNOpen Plan — Kitchen -> Dining -> LivingElectricalRoderick2Ethernet point behind sofagypsum (this room)Living; ElectricalRun back to the central unit at the entrance — pull with the first-fix cabling, not after tiling.
OPN-12OPNOpen Plan — Kitchen -> Dining -> LivingElectricalRoderick2Relocate double-pole switch when open-plan wall is removedOPN-01 — feature wall removedDining; Electrical
OPN-13OPNOpen Plan — Kitchen -> Dining -> LivingPlumbing / GasRoderick2Re-plumb kitchen sink onto main water feed (remove tank feed)startKitchen; Plumbing; Prep for ROSwitch the sink supply from the tank line to the main feed, and leave it ready for a future reverse-osmosis filter.
OPN-14OPNOpen Plan — Kitchen -> Dining -> LivingFittings / CarpentryULF9Install PVC sheet on wall with TVOQ-06 — PVC sheet spec; tiling completeLiving; Fittings; ! To confirm
ENT-01ENTEntranceElectricalRoderick2Install new consumer unit / RCD (salvavita) panelOQ-03 — panel location and capacityElectrical; Major itemFull refresh of the electrical panel. Should also resolve the kitchen oven tripping the salvavita. Confirm final position and capacity (OQ-03) before ordering. Needs certification on completion.
ENT-02ENTEntranceElectricalRoderick2Intercom — install / replace unit and wiringENT-01 — new consumer unitElectrical; NEWConfirm whether the existing intercom is reused or replaced, and whether it is audio-only or video — this changes the cable pulled at first fix.
MCR-01MCRMain CorridorElectricalRoderick2Update rope lighting to match new dining rope lightstartElectrical; 1 light point
MCR-02MCRMain CorridorElectricalRoderick2New plug near feature wallstartElectrical
MCR-03MCRMain CorridorElectricalRoderick2Test existing wire feedstartElectrical
MCR-04MCRMain CorridorElectricalRoderick2Fix emergency light short circuitstartElectrical; Known issueTrace and repair the short before the new unit is energised.
MCR-05MCRMain CorridorFittings / CarpentryULF9Install vent coverselec & plumbing rough-in (this room)Fittings
MCR-06MCRMain CorridorFittings / CarpentryULF9Install corridor mirror — 3m x 1.5melec & plumbing rough-in (this room)FittingsLarge and heavy — confirm fixings and who lifts it. Fit after painting.
SCR-01SCRSmall CorridorGypsum / StructuralRoderick5Extend gypsum partition +10cm to fit new cabinetsstartGypsumExisting cabinet LED lighting point to be reused/kept.
MBR-01MBRMain BedroomGypsum / StructuralRoderick5Gypsum — close off old ceiling light area to create new layoutstartGypsum
MBR-02MBRMain BedroomElectricalRoderick1Remove 6 existing ceiling lightsstartElectrical; Remove existing
MBR-03MBRMain BedroomElectricalRoderick1Remove 2 bedside hanging lampsstartElectrical; Remove existing
MBR-04MBRMain BedroomElectricalRoderick1Remove bedside window plugsstartElectrical; Remove existing
MBR-05MBRMain BedroomElectricalRoderick24 new ceiling lights above bedgypsum (this room)Electrical; 4 light points
MBR-06MBRMain BedroomElectricalRoderick2Rope lighting above bedgypsum (this room)Electrical; 1 light point
MBR-07MBRMain BedroomElectricalRoderick22 new bedside hanging lamps + 4 switchesgypsum (this room)Electrical; 2 lights, 4 switchesSwitches positioned near bedside.
MBR-08MBRMain BedroomElectricalRoderick22 ceiling points near vanity/windowgypsum (this room)Electrical; 2 light points
MBR-09MBRMain BedroomElectricalRoderick2Wire for wardrobe LED lighting (sensor-activated)gypsum (this room)Electrical; 1 light + sensorSensor-operated, not a manual switch.
MBR-10MBRMain BedroomElectricalRoderick8Cut into wardrobe for gyser/pump switchesMBR-11 — wardrobe built (ULF)ElectricalGyser and pump switches built into the wardrobe carcass.
MBR-11MBRMain BedroomFittings / CarpentryULF8Build wardrobe around ensuite door for maximum storagetiling and painting completeCarpentry
MBR-12MBRMain BedroomFittings / CarpentryULF9Install vanity mirrorvanity unit installed (by others)Fittings; ! To confirm75cm from floor to vanity surface — fit after the vanity unit is in.
OFF-01OFFStudy / OfficeGypsum / StructuralRoderick5Build new gypsum partition wallstartGypsum2 layers of board on the inside, 75mm rockwool insulation in the middle, 1 single board on the outer side.
OFF-02OFFStudy / OfficeElectricalRoderick21 ceiling light pointgypsum (this room)Electrical; 1 light point
OFF-03OFFStudy / OfficeElectricalRoderick2Wire for LED lighting under deskgypsum (this room)Electrical; 1 light point
OFF-04OFFStudy / OfficeElectricalRoderick2PlugsOQ-04 — plug quantityElectrical; ! To confirm
OFF-05OFFStudy / OfficeElectricalRoderick2Switch for ceiling lightgypsum (this room)Electrical; 1 switch
OFF-06OFFStudy / OfficeElectricalRoderick2Switch for under-desk lightgypsum (this room)Electrical; 1 switch
OFF-07OFFStudy / OfficeElectricalRoderick2Wire LED bar near doorgypsum (this room)Electrical; 1 light point
OFF-08OFFStudy / OfficeElectricalRoderick2Internet/ethernet point near deskgypsum (this room)ElectricalRun back to the central unit at the entrance during first fix.
OFF-09OFFStudy / OfficeFittings / CarpentryULF9Install office mirror — 2m x 2melec & plumbing rough-in (this room)Fittings
ENS-01ENSEnsuiteGypsum / StructuralRoderick6Paint ceilingtiling completePainting
ENS-02ENSEnsuiteElectricalRoderick8Add ensuite light switchMBR-10 — wardrobe cut-outElectrical; 1 switchPositioned via the main bedroom wardrobe cut-out.
ENS-03ENSEnsuiteTilingRoderick4Tank/waterproof floor and shower walls before tilingfirst fix completeTiling; NEWWet-area waterproofing was not in the original list but must happen before the ensuite is tiled — confirm it is in Roderick's price.
ENS-04ENSEnsuiteTilingRoderick4Lay ensuite tiles — 60x30 (dimensions to confirm)OQ-05 & OQ-10 — confirm scope; ENS-03 waterproofingTiling; ! To confirm! Site photos show the ensuite is already tiled in large-format travertine. Confirm whether it is being re-tiled at all, or only the shower area (OQ-10).
ENS-05ENSEnsuiteFittings / CarpentryULF9Change shower glass + traytiling completeFittings
BTH-01BTHMain BathroomPlumbing / GasRoderick2Install water heater (gyser) + electrical pointstartPlumbing; 1 pointConfirm gyser size/position before first fix — it sets the pipe and cable runs.
BTH-02BTHMain BathroomFittings / CarpentryULF9Replace sink accessories with newelec & plumbing rough-in (this room)Fittings
BTH-03BTHMain BathroomFittings / CarpentryULF9Replace flushing accessories with newelec & plumbing rough-in (this room)Fittings
BTH-04BTHMain BathroomFittings / CarpentryULF9Attach mirrorelec & plumbing rough-in (this room)Fittings
BTH-05BTHMain BathroomFittings / CarpentryULF9Attach sinkelec & plumbing rough-in (this room)Fittings
BTH-06BTHMain BathroomFittings / CarpentryULF9Attach cabinetselec & plumbing rough-in (this room)Fittings
CYB-01CYBCy's Bedroom (with window)ElectricalRoderick2Plugs under desk with USBstartElectrical; 1 point
CYB-02CYBCy's Bedroom (with window)ElectricalRoderick22 bedside plugsstartElectrical; 2 points
CYB-03CYBCy's Bedroom (with window)ElectricalRoderick2Wire for wardrobe LED lighting (sensor-activated)startElectrical; 1 light + sensor
CYB-04CYBCy's Bedroom (with window)ElectricalRoderick2Wire feed for LED under wall furniturestartElectrical
CYB-05CYBCy's Bedroom (with window)ElectricalRoderick2Internet point near deskstartElectrical
RAF-01RAFRafe's Bedroom (with back balcony door)ElectricalRoderick2Plugs under desk with USBstartElectrical; 1 point
RAF-02RAFRafe's Bedroom (with back balcony door)ElectricalRoderick22 bedside plugsstartElectrical; 2 points
RAF-03RAFRafe's Bedroom (with back balcony door)ElectricalRoderick2Wire for wardrobe LED lighting (sensor-activated)startElectrical; 1 light + sensor
RAF-04RAFRafe's Bedroom (with back balcony door)ElectricalRoderick2Wire feed for LED under wall furniturestartElectrical
RAF-05RAFRafe's Bedroom (with back balcony door)ElectricalRoderick2Internet point near deskstartElectrical
BBL-01BBLBack BalconyGypsum / StructuralRoderick1Back balcony — confirm scope on sitestart! Scope to confirm; NEWCovered by the whole-house 120x120 tiling (WHS-05). Check for: outdoor lighting, a weatherproof socket, drainage falls, railing condition and door threshold. Add items here as found.

lights_plugs_needed_for_Roderick_-ULF.csv

data/lights_plugs_needed_for_Roderick_-ULF.csv

rafewall white
furniture teal/dark blue
wire feed for light wardrobe with sensor
wire feed for led under wall furtniture
feed for plug on furniture near bed
internet near desk
Cyrilwall teal
wire feed for plug on furniture near bed
wire feed for light wardrobe with sensor
wire feed for led under wall furtniture
internet near desk
Back & main corridornew plug near feature wall
to test wire feed
vent covers
Yvette led furniture
chrek emergency light short curcuit with back corridor
main bedroomYvette- led around bed
cut in wardrobe for switches (gyser/pump)
switch for ensuite?
wire feed for light wardrobe with sensor
wire feed for led around bed
officeinternet
wire feed for led bar near door
open planethernet for wall behind sofa
double pole switch to be moved with wall
unit for salva vita
outdoorrope lighhtning outdoor for front
to pass gas
tiles for wall
outdoor kitchen

lights_plugs_to_buy.csv

data/lights_plugs_to_buy.csv

2 gang3 gang5gang2x plug4x plugplug furniturewter proof plugsLED +kit (case/switch)light sensors- wardrobeCeiling lightslamps
Rafe13411
Cy121411
back/main corridor312
main bedroom1322182 hanging lamps
Office1312
open plan12113hood & dining
outdoor212 wall llights
4731221025313
mirrors
corridor3m*1.5m
office2m*2m
main bedroom vanityto check if ulf and after furniture (75cm from floor to vanity surfice)
pvc sheetoutdoor
wall with tv
table 2.5m
chairs * 8

rooms.json

data/rooms.json · 7 top-level entries

{
 "project": "Zonqor Apartment refurbishment",
 "generated": "2026-08-16",
 "start_date": "2026-08-13",
 "work_week": "Mon-Fri",
 "crew": "Roderick + son (2), ULF for carpentry",
 "rooms": [
  {
   "code": "WHS",
   "name": "Whole House",
   "banner": "Cross-room work that is not tied to a single room. Everything here is priced once, for the whole flat.",
   "items": [
    {
     "id": "WHS-01",
     "title": "Strip out old parquet flooring throughout",
     "trade": "Gypsum / Structural",
     "trade_key": "gypsum",
     "assigned": "Roderick",
     "phase": 1,
     "depends_on": "start",
     "notes": "Lift and remove the existing parquet across the flat. First job on site \u2014 everything else waits on the floor being clear.",
     "tags": [
      "Gypsum",
      "Remove existing",
      "NEW"
     ]
    },
    {
     "id": "WHS-02",
     "title": "Skip hire and waste removal",
     "trade": "Gypsum / Structural",
     "trade_key": "gypsum",
     "assigned": "Roderick",
     "phase": 3,
     "depends_on": "WHS-01 \u2014 parquet stripped",
     "notes": "At least two skips: one after strip-out/demolition, one after first fix and chasing. Confirm who books and pays for the skip.",
     "tags": [
      "Gypsum",
      "NEW"
     ]
    },
    {
     "id": "WHS-03",
     "title": "Fix electricity problems and points",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "ENT-01 \u2014 new consumer unit",
     "notes": "Diagnose and repair existing faults; check/replace faulty sockets and switches; add points as needed throughout the flat.",
     "tags": [
      "Electrical"
     ]
    },
    {
     "id": "WHS-04",
     "title": "Check and level floor screed after parquet removal",
     "trade": "Tiling",
     "trade_key": "tile",
     "assigned": "Roderick",
     "phase": 4,
     "depends_on": "WHS-01 \u2014 parquet stripped",
     "notes": "Once the parquet is up, check the screed for level and damage. Patch/level before tiling \u2014 120x120 tiles are unforgiving on an uneven floor.",
     "tags": [
      "Tiling",
      "NEW"
     ]
    },
    {
     "id": "WHS-05",
     "title": "Lay 120x120 tiles \u2014 full house, including balconies (~145 sq m)",
     "trade": "Tiling",
     "trade_key": "tile",
     "assigned": "Roderick",
     "phase": 4,
     "depends_on": "WHS-04 \u2014 floor levelled; first-fix complete",
     "notes": "120x120 throughout the flat, ~145 sq m per Roderick's quote. ! Site photos show the ensuite, main bathroom and front balcony are ALREADY TILED \u2014 re-measure the actual parquet area before this price is agreed (OQ-10).",
     "tags": [
      "Tiling"
     ]
    }
   ],
   "pictures": [
    {
     "file": "tiles_sq_m.jpg",
     "caption": "Floor plan \u2014 tiling area reference (125 sqm marked; 145 sqm quoted)"
    }
   ]
  },
  {
   "code": "FBL",
   "name": "Front Balcony",
   "banner": "! Outdoor kitchen design/equipment still to be specified (OQ-02) before ordering or starting those items.",
   "items": [
    {
     "id": "FBL-01",
     "title": "2 side lights \u2014 BBQ side",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical",
      "2 light points"
     ]
    },
    {
     "id": "FBL-02",
     "title": "1 electrical/USB point \u2014 BBQ side (for camera)",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical",
      "1 point"
     ]
    },
    {
     "id": "FBL-03",
     "title": "Central floor lights (under railing) \u2014 mini lights or LED strip",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical",
      "1 light point"
     ]
    },
    {
     "id": "FBL-04",
     "title": "Switch for balcony side lights",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": "At the current kitchen/balcony switch position.",
     "tags": [
      "Electrical",
      "1 switch"
     ]
    },
    {
     "id": "FBL-05",
     "title": "Switch for balcony floor lights",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": "At the current kitchen/balcony switch position.",
     "tags": [
      "Electrical",
      "1 switch"
     ]
    },
    {
     "id": "FBL-06",
     "title": "Rope lighting \u2014 front (outdoor)",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "OQ-02 \u2014 outdoor kitchen spec",
     "notes": null,
     "tags": [
      "Electrical"
     ]
    },
    {
     "id": "FBL-07",
     "title": "2 outdoor wall lights",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "OQ-02 \u2014 outdoor kitchen spec",
     "notes": null,
     "tags": [
      "Electrical",
      "2 points"
     ]
    },
    {
     "id": "FBL-08",
     "title": "2 waterproof plugs + LED kit for outdoor kitchen units",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "OQ-02 \u2014 outdoor kitchen spec",
     "notes": null,
     "tags": [
      "Electrical"
     ]
    },
    {
     "id": "FBL-09",
     "title": "Run gas line for outdoor kitchen",
     "trade": "Plumbing / Gas",
     "trade_key": "plumb",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "OQ-02 \u2014 outdoor kitchen spec",
     "notes": "Gas run must be pressure-tested and certified. Confirm bottle vs. mains supply and where the bottle housing sits.",
     "tags": [
      "Gas/Plumbing"
     ]
    },
    {
     "id": "FBL-10",
     "title": "Outdoor wall finish \u2014 PVC sheet OR tiles",
     "trade": "Fittings / Carpentry",
     "trade_key": "fit",
     "assigned": "ULF",
     "phase": 9,
     "depends_on": "OQ-01 \u2014 PVC vs. tiles; elec & gas rough-in (this room)",
     "notes": "One or the other, not both \u2014 still to be decided. See OQ-01.",
     "tags": [
      "Fittings/Tiling",
      "! To confirm"
     ]
    }
   ],
   "pictures": [
    {
     "file": "balcony_1.jpg",
     "caption": "Front balcony / outdoor kitchen \u2014 render 1"
    },
    {
     "file": "balcony_2.jpg",
     "caption": "Front balcony / outdoor kitchen \u2014 render 2"
    },
    {
     "file": "site_9826.jpg",
     "caption": "SITE: front balcony \u2014 BBQ corner, existing feed & camera"
    },
    {
     "file": "site_9827.jpg",
     "caption": "SITE: front balcony looking out \u2014 adhesive stacked"
    },
    {
     "file": "site_9828.jpg",
     "caption": "SITE: front balcony \u2014 already tiled, membrane at edge"
    },
    {
     "file": "site_9895.jpg",
     "caption": "SITE: sliding door to front balcony"
    }
   ]
  },
  {
   "code": "OPN",
   "name": "Open Plan \u2014 Kitchen -> Dining -> Living",
   "banner": "One continuous space. Zone tags below show which end of the room each item sits in.",
   "items": [
    {
     "id": "OPN-01",
     "title": "Dining Feature Wall + Fireplace Removal",
     "trade": "Gypsum / Structural",
     "trade_key": "gypsum",
     "assigned": "Roderick",
     "phase": 1,
     "depends_on": "start",
     "notes": "Remove the existing gypsum feature wall/partition and the fireplace to open the living/dining space. Confirm the final opening size before starting. Check for services in the wall before breaking.",
     "tags": [
      "Dining",
      "Gypsum",
      "Remove existing"
     ]
    },
    {
     "id": "OPN-02",
     "title": "Add gypsum behind sofa",
     "trade": "Gypsum / Structural",
     "trade_key": "gypsum",
     "assigned": "Roderick",
     "phase": 5,
     "depends_on": "OPN-01 \u2014 feature wall removed",
     "notes": "250mm wide x 1m high.",
     "tags": [
      "Living",
      "Gypsum"
     ]
    },
    {
     "id": "OPN-03",
     "title": "3 ceiling lights",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "gypsum (this room)",
     "notes": "Reuses an existing electrical point.",
     "tags": [
      "Kitchen",
      "Electrical",
      "3 light points"
     ]
    },
    {
     "id": "OPN-04",
     "title": "Hood electrical point",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "gypsum (this room)",
     "notes": "Reuses an existing electrical point.",
     "tags": [
      "Kitchen",
      "Electrical",
      "1 point"
     ]
    },
    {
     "id": "OPN-05",
     "title": "Fix oven electrical point \u2014 currently trips the salvavita (RCD)",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "ENT-01 \u2014 new consumer unit",
     "notes": "Oven trips the salvavita when switched on. Check wiring, the connection point, and whether the oven needs its own dedicated circuit off the new unit.",
     "tags": [
      "Kitchen",
      "Electrical",
      "2 points",
      "Known issue"
     ]
    },
    {
     "id": "OPN-06",
     "title": "Light points for dining cabinets",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "gypsum (this room)",
     "notes": null,
     "tags": [
      "Dining",
      "Electrical",
      "1 light point"
     ]
    },
    {
     "id": "OPN-07",
     "title": "Rope lighting within the new gypsum wall (behind sofa)",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "OPN-02 \u2014 gypsum behind sofa",
     "notes": null,
     "tags": [
      "Living",
      "Electrical",
      "1 light point"
     ]
    },
    {
     "id": "OPN-08",
     "title": "Switch for sofa rope lighting",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "gypsum (this room)",
     "notes": null,
     "tags": [
      "Living",
      "Electrical",
      "1 switch"
     ]
    },
    {
     "id": "OPN-09",
     "title": "2 plugs near sofa",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "gypsum (this room)",
     "notes": null,
     "tags": [
      "Living",
      "Electrical",
      "2 points"
     ]
    },
    {
     "id": "OPN-10",
     "title": "TV unit electrical point",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "OQ-07 \u2014 TV unit electrical spec",
     "notes": null,
     "tags": [
      "Living",
      "Electrical",
      "! To confirm"
     ]
    },
    {
     "id": "OPN-11",
     "title": "Ethernet point behind sofa",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "gypsum (this room)",
     "notes": "Run back to the central unit at the entrance \u2014 pull with the first-fix cabling, not after tiling.",
     "tags": [
      "Living",
      "Electrical"
     ]
    },
    {
     "id": "OPN-12",
     "title": "Relocate double-pole switch when open-plan wall is removed",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "OPN-01 \u2014 feature wall removed",
     "notes": null,
     "tags": [
      "Dining",
      "Electrical"
     ]
    },
    {
     "id": "OPN-13",
     "title": "Re-plumb kitchen sink onto main water feed (remove tank feed)",
     "trade": "Plumbing / Gas",
     "trade_key": "plumb",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": "Switch the sink supply from the tank line to the main feed, and leave it ready for a future reverse-osmosis filter.",
     "tags": [
      "Kitchen",
      "Plumbing",
      "Prep for RO"
     ]
    },
    {
     "id": "OPN-14",
     "title": "Install PVC sheet on wall with TV",
     "trade": "Fittings / Carpentry",
     "trade_key": "fit",
     "assigned": "ULF",
     "phase": 9,
     "depends_on": "OQ-06 \u2014 PVC sheet spec; tiling complete",
     "notes": null,
     "tags": [
      "Living",
      "Fittings",
      "! To confirm"
     ]
    }
   ],
   "pictures": [
    {
     "file": "openplan_dimensions_1.jpg",
     "caption": "Open plan \u2014 measurement drawing 1"
    },
    {
     "file": "openplan_dimensions_2.jpg",
     "caption": "Open plan \u2014 measurement drawing 2"
    },
    {
     "file": "openplan_dimensions_3.jpg",
     "caption": "Open plan \u2014 measurement drawing 3"
    },
    {
     "file": "1__dining__feature_wall_revised.jpg",
     "caption": "Dining \u2014 feature wall render"
    },
    {
     "file": "7__dining_+_kitchen_revised.jpg",
     "caption": "Kitchen / dining \u2014 render"
    },
    {
     "file": "15_dining_+_tv_unit_revised.jpg",
     "caption": "Dining \u2014 TV unit render"
    },
    {
     "file": "2__sofa_revised.jpg",
     "caption": "Living \u2014 sofa wall render"
    },
    {
     "file": "site_9834.jpg",
     "caption": "SITE: open plan overview \u2014 parquet still down, 120 tiles delivered"
    },
    {
     "file": "site_9835.jpg",
     "caption": "SITE: kitchen island, units wrapped"
    },
    {
     "file": "site_9839.jpg",
     "caption": "SITE: dining feature wall + fireplace, wall opened below"
    },
    {
     "file": "site_9893.jpg",
     "caption": "SITE: fireplace close-up \u2014 installed, wall broken beneath"
    },
    {
     "file": "site_9840.jpg",
     "caption": "SITE: living \u2014 existing socket run"
    },
    {
     "file": "site_9896.jpg",
     "caption": "SITE: gypsum board delivered on site"
    }
   ]
  },
  {
   "code": "ENT",
   "name": "Entrance",
   "banner": "Home of the new electrical central unit \u2014 the first-fix hub for the whole flat. Everything electrical downstream waits on ENT-01.",
   "items": [
    {
     "id": "ENT-01",
     "title": "Install new consumer unit / RCD (salvavita) panel",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "OQ-03 \u2014 panel location and capacity",
     "notes": "Full refresh of the electrical panel. Should also resolve the kitchen oven tripping the salvavita. Confirm final position and capacity (OQ-03) before ordering. Needs certification on completion.",
     "tags": [
      "Electrical",
      "Major item"
     ]
    },
    {
     "id": "ENT-02",
     "title": "Intercom \u2014 install / replace unit and wiring",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "ENT-01 \u2014 new consumer unit",
     "notes": "Confirm whether the existing intercom is reused or replaced, and whether it is audio-only or video \u2014 this changes the cable pulled at first fix.",
     "tags": [
      "Electrical",
      "NEW"
     ]
    }
   ],
   "pictures": [
    {
     "file": "site_9845.jpg",
     "caption": "SITE: existing consumer unit, thermostat & entrance door"
    }
   ]
  },
  {
   "code": "MCR",
   "name": "Main Corridor",
   "banner": null,
   "items": [
    {
     "id": "MCR-01",
     "title": "Update rope lighting to match new dining rope light",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical",
      "1 light point"
     ]
    },
    {
     "id": "MCR-02",
     "title": "New plug near feature wall",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical"
     ]
    },
    {
     "id": "MCR-03",
     "title": "Test existing wire feed",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical"
     ]
    },
    {
     "id": "MCR-04",
     "title": "Fix emergency light short circuit",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": "Trace and repair the short before the new unit is energised.",
     "tags": [
      "Electrical",
      "Known issue"
     ]
    },
    {
     "id": "MCR-05",
     "title": "Install vent covers",
     "trade": "Fittings / Carpentry",
     "trade_key": "fit",
     "assigned": "ULF",
     "phase": 9,
     "depends_on": "elec & plumbing rough-in (this room)",
     "notes": null,
     "tags": [
      "Fittings"
     ]
    },
    {
     "id": "MCR-06",
     "title": "Install corridor mirror \u2014 3m x 1.5m",
     "trade": "Fittings / Carpentry",
     "trade_key": "fit",
     "assigned": "ULF",
     "phase": 9,
     "depends_on": "elec & plumbing rough-in (this room)",
     "notes": "Large and heavy \u2014 confirm fixings and who lifts it. Fit after painting.",
     "tags": [
      "Fittings"
     ]
    }
   ],
   "pictures": [
    {
     "file": "10_corridor_revised.jpg",
     "caption": "Main corridor \u2014 render"
    },
    {
     "file": "corridor_dimensions_1.jpg",
     "caption": "Corridor \u2014 measurement drawing"
    },
    {
     "file": "site_9849.jpg",
     "caption": "SITE: main corridor looking through"
    },
    {
     "file": "site_9850.jpg",
     "caption": "SITE: main corridor \u2014 existing cove lighting"
    },
    {
     "file": "site_9843.jpg",
     "caption": "SITE: corridor switch position"
    }
   ]
  },
  {
   "code": "SCR",
   "name": "Small Corridor",
   "banner": null,
   "items": [
    {
     "id": "SCR-01",
     "title": "Extend gypsum partition +10cm to fit new cabinets",
     "trade": "Gypsum / Structural",
     "trade_key": "gypsum",
     "assigned": "Roderick",
     "phase": 5,
     "depends_on": "start",
     "notes": "Existing cabinet LED lighting point to be reused/kept.",
     "tags": [
      "Gypsum"
     ]
    }
   ],
   "pictures": [
    {
     "file": "12_corridor_small_revised.jpg",
     "caption": "Small corridor \u2014 render"
    }
   ]
  },
  {
   "code": "MBR",
   "name": "Main Bedroom",
   "banner": null,
   "items": [
    {
     "id": "MBR-01",
     "title": "Gypsum \u2014 close off old ceiling light area to create new layout",
     "trade": "Gypsum / Structural",
     "trade_key": "gypsum",
     "assigned": "Roderick",
     "phase": 5,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Gypsum"
     ]
    },
    {
     "id": "MBR-02",
     "title": "Remove 6 existing ceiling lights",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 1,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical",
      "Remove existing"
     ]
    },
    {
     "id": "MBR-03",
     "title": "Remove 2 bedside hanging lamps",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 1,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical",
      "Remove existing"
     ]
    },
    {
     "id": "MBR-04",
     "title": "Remove bedside window plugs",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 1,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical",
      "Remove existing"
     ]
    },
    {
     "id": "MBR-05",
     "title": "4 new ceiling lights above bed",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "gypsum (this room)",
     "notes": null,
     "tags": [
      "Electrical",
      "4 light points"
     ]
    },
    {
     "id": "MBR-06",
     "title": "Rope lighting above bed",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "gypsum (this room)",
     "notes": null,
     "tags": [
      "Electrical",
      "1 light point"
     ]
    },
    {
     "id": "MBR-07",
     "title": "2 new bedside hanging lamps + 4 switches",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "gypsum (this room)",
     "notes": "Switches positioned near bedside.",
     "tags": [
      "Electrical",
      "2 lights, 4 switches"
     ]
    },
    {
     "id": "MBR-08",
     "title": "2 ceiling points near vanity/window",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "gypsum (this room)",
     "notes": null,
     "tags": [
      "Electrical",
      "2 light points"
     ]
    },
    {
     "id": "MBR-09",
     "title": "Wire for wardrobe LED lighting (sensor-activated)",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "gypsum (this room)",
     "notes": "Sensor-operated, not a manual switch.",
     "tags": [
      "Electrical",
      "1 light + sensor"
     ]
    },
    {
     "id": "MBR-10",
     "title": "Cut into wardrobe for gyser/pump switches",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 8,
     "depends_on": "MBR-11 \u2014 wardrobe built (ULF)",
     "notes": "Gyser and pump switches built into the wardrobe carcass.",
     "tags": [
      "Electrical"
     ]
    },
    {
     "id": "MBR-11",
     "title": "Build wardrobe around ensuite door for maximum storage",
     "trade": "Fittings / Carpentry",
     "trade_key": "fit",
     "assigned": "ULF",
     "phase": 8,
     "depends_on": "tiling and painting complete",
     "notes": null,
     "tags": [
      "Carpentry"
     ]
    },
    {
     "id": "MBR-12",
     "title": "Install vanity mirror",
     "trade": "Fittings / Carpentry",
     "trade_key": "fit",
     "assigned": "ULF",
     "phase": 9,
     "depends_on": "vanity unit installed (by others)",
     "notes": "75cm from floor to vanity surface \u2014 fit after the vanity unit is in.",
     "tags": [
      "Fittings",
      "! To confirm"
     ]
    }
   ],
   "pictures": [
    {
     "file": "main_bedroom_1.jpg",
     "caption": "Main bedroom \u2014 render (overview)"
    },
    {
     "file": "main_bedroom_6-bed.jpg",
     "caption": "Main bedroom \u2014 bed wall render"
    },
    {
     "file": "main_bedroom_4-_wardrobe.jpg",
     "caption": "Main bedroom \u2014 wardrobe render"
    },
    {
     "file": "main_bedroom_5-_vanity.jpg",
     "caption": "Main bedroom \u2014 vanity render"
    },
    {
     "file": "main_bedroom_dimensions_1.jpg",
     "caption": "Main bedroom \u2014 measurement drawing 1"
    },
    {
     "file": "main_bedroom_dimensions_2.jpg",
     "caption": "Main bedroom \u2014 measurement drawing 2"
    }
   ]
  },
  {
   "code": "OFF",
   "name": "Study / Office",
   "banner": "Sits off the Main Bedroom. ! The new partition is a full wall, not an amendment \u2014 build it BEFORE tiling, not after (see the sequencing note in the plan).",
   "items": [
    {
     "id": "OFF-01",
     "title": "Build new gypsum partition wall",
     "trade": "Gypsum / Structural",
     "trade_key": "gypsum",
     "assigned": "Roderick",
     "phase": 5,
     "depends_on": "start",
     "notes": "2 layers of board on the inside, 75mm rockwool insulation in the middle, 1 single board on the outer side.",
     "tags": [
      "Gypsum"
     ]
    },
    {
     "id": "OFF-02",
     "title": "1 ceiling light point",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "gypsum (this room)",
     "notes": null,
     "tags": [
      "Electrical",
      "1 light point"
     ]
    },
    {
     "id": "OFF-03",
     "title": "Wire for LED lighting under desk",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "gypsum (this room)",
     "notes": null,
     "tags": [
      "Electrical",
      "1 light point"
     ]
    },
    {
     "id": "OFF-04",
     "title": "Plugs",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "OQ-04 \u2014 plug quantity",
     "notes": null,
     "tags": [
      "Electrical",
      "! To confirm"
     ]
    },
    {
     "id": "OFF-05",
     "title": "Switch for ceiling light",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "gypsum (this room)",
     "notes": null,
     "tags": [
      "Electrical",
      "1 switch"
     ]
    },
    {
     "id": "OFF-06",
     "title": "Switch for under-desk light",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "gypsum (this room)",
     "notes": null,
     "tags": [
      "Electrical",
      "1 switch"
     ]
    },
    {
     "id": "OFF-07",
     "title": "Wire LED bar near door",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "gypsum (this room)",
     "notes": null,
     "tags": [
      "Electrical",
      "1 light point"
     ]
    },
    {
     "id": "OFF-08",
     "title": "Internet/ethernet point near desk",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "gypsum (this room)",
     "notes": "Run back to the central unit at the entrance during first fix.",
     "tags": [
      "Electrical"
     ]
    },
    {
     "id": "OFF-09",
     "title": "Install office mirror \u2014 2m x 2m",
     "trade": "Fittings / Carpentry",
     "trade_key": "fit",
     "assigned": "ULF",
     "phase": 9,
     "depends_on": "elec & plumbing rough-in (this room)",
     "notes": null,
     "tags": [
      "Fittings"
     ]
    }
   ],
   "pictures": [
    {
     "file": "study_1.jpg",
     "caption": "Study / office \u2014 render 1"
    },
    {
     "file": "study_2.jpg",
     "caption": "Study / office \u2014 render 2"
    },
    {
     "file": "study_3.jpg",
     "caption": "Study / office \u2014 render 3"
    }
   ]
  },
  {
   "code": "ENS",
   "name": "Ensuite",
   "banner": null,
   "items": [
    {
     "id": "ENS-01",
     "title": "Paint ceiling",
     "trade": "Gypsum / Structural",
     "trade_key": "gypsum",
     "assigned": "Roderick",
     "phase": 6,
     "depends_on": "tiling complete",
     "notes": null,
     "tags": [
      "Painting"
     ]
    },
    {
     "id": "ENS-02",
     "title": "Add ensuite light switch",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 8,
     "depends_on": "MBR-10 \u2014 wardrobe cut-out",
     "notes": "Positioned via the main bedroom wardrobe cut-out.",
     "tags": [
      "Electrical",
      "1 switch"
     ]
    },
    {
     "id": "ENS-03",
     "title": "Tank/waterproof floor and shower walls before tiling",
     "trade": "Tiling",
     "trade_key": "tile",
     "assigned": "Roderick",
     "phase": 4,
     "depends_on": "first fix complete",
     "notes": "Wet-area waterproofing was not in the original list but must happen before the ensuite is tiled \u2014 confirm it is in Roderick's price.",
     "tags": [
      "Tiling",
      "NEW"
     ]
    },
    {
     "id": "ENS-04",
     "title": "Lay ensuite tiles \u2014 60x30 (dimensions to confirm)",
     "trade": "Tiling",
     "trade_key": "tile",
     "assigned": "Roderick",
     "phase": 4,
     "depends_on": "OQ-05 & OQ-10 \u2014 confirm scope; ENS-03 waterproofing",
     "notes": "! Site photos show the ensuite is already tiled in large-format travertine. Confirm whether it is being re-tiled at all, or only the shower area (OQ-10).",
     "tags": [
      "Tiling",
      "! To confirm"
     ]
    },
    {
     "id": "ENS-05",
     "title": "Change shower glass + tray",
     "trade": "Fittings / Carpentry",
     "trade_key": "fit",
     "assigned": "ULF",
     "phase": 9,
     "depends_on": "tiling complete",
     "notes": null,
     "tags": [
      "Fittings"
     ]
    }
   ],
   "pictures": [
    {
     "file": "site_9886.jpg",
     "caption": "SITE: ensuite \u2014 existing shower enclosure"
    },
    {
     "file": "site_9888.jpg",
     "caption": "SITE: ensuite \u2014 shower tray & glass to be changed"
    },
    {
     "file": "site_9889.jpg",
     "caption": "SITE: ensuite \u2014 WC & window"
    },
    {
     "file": "site_9890.jpg",
     "caption": "SITE: ensuite \u2014 overview"
    }
   ]
  },
  {
   "code": "BTH",
   "name": "Main Bathroom",
   "banner": null,
   "items": [
    {
     "id": "BTH-01",
     "title": "Install water heater (gyser) + electrical point",
     "trade": "Plumbing / Gas",
     "trade_key": "plumb",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": "Confirm gyser size/position before first fix \u2014 it sets the pipe and cable runs.",
     "tags": [
      "Plumbing",
      "1 point"
     ]
    },
    {
     "id": "BTH-02",
     "title": "Replace sink accessories with new",
     "trade": "Fittings / Carpentry",
     "trade_key": "fit",
     "assigned": "ULF",
     "phase": 9,
     "depends_on": "elec & plumbing rough-in (this room)",
     "notes": null,
     "tags": [
      "Fittings"
     ]
    },
    {
     "id": "BTH-03",
     "title": "Replace flushing accessories with new",
     "trade": "Fittings / Carpentry",
     "trade_key": "fit",
     "assigned": "ULF",
     "phase": 9,
     "depends_on": "elec & plumbing rough-in (this room)",
     "notes": null,
     "tags": [
      "Fittings"
     ]
    },
    {
     "id": "BTH-04",
     "title": "Attach mirror",
     "trade": "Fittings / Carpentry",
     "trade_key": "fit",
     "assigned": "ULF",
     "phase": 9,
     "depends_on": "elec & plumbing rough-in (this room)",
     "notes": null,
     "tags": [
      "Fittings"
     ]
    },
    {
     "id": "BTH-05",
     "title": "Attach sink",
     "trade": "Fittings / Carpentry",
     "trade_key": "fit",
     "assigned": "ULF",
     "phase": 9,
     "depends_on": "elec & plumbing rough-in (this room)",
     "notes": null,
     "tags": [
      "Fittings"
     ]
    },
    {
     "id": "BTH-06",
     "title": "Attach cabinets",
     "trade": "Fittings / Carpentry",
     "trade_key": "fit",
     "assigned": "ULF",
     "phase": 9,
     "depends_on": "elec & plumbing rough-in (this room)",
     "notes": null,
     "tags": [
      "Fittings"
     ]
    }
   ],
   "pictures": [
    {
     "file": "site_9872.jpg",
     "caption": "SITE: main bathroom \u2014 existing finish"
    },
    {
     "file": "site_9873.jpg",
     "caption": "SITE: main bathroom \u2014 WC & window"
    },
    {
     "file": "site_9875.jpg",
     "caption": "SITE: main bathroom \u2014 ceiling & downlights"
    }
   ]
  },
  {
   "code": "CYB",
   "name": "Cy's Bedroom (with window)",
   "banner": null,
   "items": [
    {
     "id": "CYB-01",
     "title": "Plugs under desk with USB",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical",
      "1 point"
     ]
    },
    {
     "id": "CYB-02",
     "title": "2 bedside plugs",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical",
      "2 points"
     ]
    },
    {
     "id": "CYB-03",
     "title": "Wire for wardrobe LED lighting (sensor-activated)",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical",
      "1 light + sensor"
     ]
    },
    {
     "id": "CYB-04",
     "title": "Wire feed for LED under wall furniture",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical"
     ]
    },
    {
     "id": "CYB-05",
     "title": "Internet point near desk",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical"
     ]
    }
   ],
   "pictures": [
    {
     "file": "Cy_kids_bedroom_7.jpg",
     "caption": "Cy's bedroom \u2014 render 1"
    },
    {
     "file": "Cy_kids_bedroom_10.jpg",
     "caption": "Cy's bedroom \u2014 render 2"
    },
    {
     "file": "kids_bedroom_dimensions_2.jpg",
     "caption": "Kids bedroom \u2014 measurement drawing"
    }
   ]
  },
  {
   "code": "RAF",
   "name": "Rafe's Bedroom (with back balcony door)",
   "banner": null,
   "items": [
    {
     "id": "RAF-01",
     "title": "Plugs under desk with USB",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical",
      "1 point"
     ]
    },
    {
     "id": "RAF-02",
     "title": "2 bedside plugs",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical",
      "2 points"
     ]
    },
    {
     "id": "RAF-03",
     "title": "Wire for wardrobe LED lighting (sensor-activated)",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical",
      "1 light + sensor"
     ]
    },
    {
     "id": "RAF-04",
     "title": "Wire feed for LED under wall furniture",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical"
     ]
    },
    {
     "id": "RAF-05",
     "title": "Internet point near desk",
     "trade": "Electrical",
     "trade_key": "elec",
     "assigned": "Roderick",
     "phase": 2,
     "depends_on": "start",
     "notes": null,
     "tags": [
      "Electrical"
     ]
    }
   ],
   "pictures": [
    {
     "file": "Rafe_kids_bedroom_1.jpg",
     "caption": "Rafe's bedroom \u2014 render 1"
    },
    {
     "file": "Rafe_kids_bedroom_5.jpg",
     "caption": "Rafe's bedroom \u2014 render 2"
    },
    {
     "file": "kids_bedroom_dimensions_1.jpg",
     "caption": "Kids bedroom \u2014 measurement drawing"
    }
   ]
  },
  {
   "code": "BBL",
   "name": "Back Balcony",
   "banner": "Off Rafe's bedroom. No items listed yet beyond the whole-house tiling \u2014 walk it with Roderick on day one and write in anything found.",
   "items": [
    {
     "id": "BBL-01",
     "title": "Back balcony \u2014 confirm scope on site",
     "trade": "Gypsum / Structural",
     "trade_key": "gypsum",
     "assigned": "Roderick",
     "phase": 1,
     "depends_on": "start",
     "notes": "Covered by the whole-house 120x120 tiling (WHS-05). Check for: outdoor lighting, a weatherproof socket, drainage falls, railing condition and door threshold. Add items here as found.",
     "tags": [
      "! Scope to confirm",
      "NEW"
     ]
    }
   ],
   "pictures": []
  }
 ],
 "phases": {
  "1": {
   "name": "Strip-out & demolition",
   "description": "Parquet up, feature wall and fireplace down, old fittings removed."
  },
  "2": {
   "name": "First fix \u2014 plumbing, electrical, ethernet",
   "description": "Consumer unit, chasing, all cable and pipe runs, gas line. Nothing gets closed up until this is signed off."
  },
  "3": {
   "name": "Clear site \u2014 skip",
   "description": "Rubble and chasing debris out before tiling starts."
  },
  "4": {
   "name": "Tiling",
   "description": "Screed levelled, wet areas tanked, 120x120 laid through the flat and balconies, ensuite in 60x30."
  },
  "5": {
   "name": "Gypsum \u2014 partition & amendments",
   "description": "Study partition, main bedroom ceiling closure, sofa wall, small corridor extension."
  },
  "6": {
   "name": "Painting",
   "description": "Ceilings and walls, including the ensuite ceiling."
  },
  "7": {
   "name": "Second fix electrical",
   "description": "Fit and terminate every point, hang light fittings, LED strips and sensors, then test and certify."
  },
  "8": {
   "name": "Carpentry \u2014 ULF",
   "description": "Wardrobes, cabinets, TV unit. Roderick returns to cut in the gyser/pump and ensuite switches once the wardrobe is up."
  },
  "9": {
   "name": "Fittings, sanitary & outdoor",
   "description": "Mirrors, shower glass and tray, sanitary ware, vent covers, PVC/tile outdoor wall finish."
  },
  "10": {
   "name": "Snagging & handover",
   "description": "Walk the flat against this checklist, list defects, fix, final clean, hand over certificates."
  }
 }
}

works_Sheet1.csv

data/works_Sheet1.csv

NEED NEW241444441800
# NEW / EXISTACTIONROOMLOCATIONDESCRIPTIONFORGypsum/paint worksFurniture SetupElectrical PointsLight PointsSwitch PointsWater PointsGas PointsIMAGE
ExistKeepBalconyBBQ Side2 WaterProof Electrical PointsGeneral Useç
ExistKeepBalconyBBQ SideWater TapGeneral Use
ExistKeepBalconyCeilingMain Ceiling LightTop Lights
ExistKeepBalconyGas Points2 Gas PointsFire Place / Cooker
1NewToDoBalconyBBQ Side2 Lights PointsSide Lights2
2NewToDoBalconyBBQ Side1 Electrical / USB PointCamera1
3NewToDoBalconyCentral Floor (Under Railing)Central Floor (Under Railing) - Mini Lights OR LEDFloor Lights1
4NewToDoBalconyKitchen - Current Balcany SwitchSwitch for Side Lights1
5NewToDoBalconyKitchen - Current Balcany SwitchSwitch for Floor Lights1
ExistRemoveMain BedroomCeiling 6 ceiling lights6
ExistRemoveMain BedroomHanging Lamps2 bed side hanging lamps2
ExistRemoveMain BedroomBed Side Windowbed side plugs1
ExistKeepMain BedroomBed Side Wallbed side plugs
ExistKeepMain BedroomAC1 Ac point _ double pole switch
ExistKeepMain BedroomTv UnitDouble Point
ExistKeepMain BedroomVanity UnitDouble Point
ExistKeepMain BedroomBehind DoorDouble Point
NewToDoMain BedroomCeiling4 ceiling lights above bed4
NewToDoMain BedroomBedRope lightning above bed1
NewToDoMain BedroomSide bed hanging lamps2 bedside hanging lamps 2
NewToDoMain BedroomSide bed SwitchesSwitch for above- near bedside4
NewToDoMain BedroomVanity / Window ceiling2 ceiling points2
NewToDoMain BedroomWardrobeLED Light Point1
NewToDoMain BedroomGypsumClose of light ceiling to create new6
NewTo doStudyGypsuminside with 2 layers boards- Inside with 75mm rockwool insulation- outerpart 1 single board2
NewTo doStudyCeiling1 light point1
NewTo doStudylight under desk1 light point1
NewTo doStudyplugs??
NewTo doStudySwitchFor ceiling1
NewTo doStudySwitchunder desk1
ExistKeepRafe Bedroomcurrent switch (room ceiling+balcony)
ExistKeepRafe BedroomAC1 Ac point _ double pole switch
ExistKeepRafe BedroomPlugs behind wardrobe
ExistKeepRafe BedroomPlugs near ac /desk
NewTo doRafe BedroomPlugs under desk with USB1
NewTo doRafe Bedroomplug bedside2
ExistKeepCy Bedroomcurrent switch (room ceiling)
ExistKeepCy BedroomAC1 Ac point _ double pole switch
ExistKeepCy BedroomPlugs near wardrobe + other walls
ExistKeepCy BedroomPlugs near ac /desk
NewTo doCy BedroomPlugs under desk with USB1
NewTo doCy Bedroomplug bedside2
ExistKeepDiningwall rope lightningto fix1
ExistKeepDiningceiling lights
ExistKeepDiningplug near balconychristmas tree
NewTo doDininglight points for cabinets1
NewTo doDiningremoval of gypsum feature wall1
ExistKeepSofaceiling lights
NewTo doSofagypsumadd gyspum behind sofa 250mm wide- 1m high1
NewTo doSofarope lightningwithin the gypsum wall1
NewTo doSofaswitchto switch rope lightning1
NewTo doSofaplug2
Sofatv unit?
NewTo doKitchenCeiling lightsexisting electrial point3
NewTo doKitchenHoodexisting electrial point1
NewTo doKitchenPlumbingMain water with kitchen
ExistTo doKitchenOven pointTo fix oven plug electrical point2
ExistKeepLong CorridorCeiling Lights
ExistKeepLong Corridorwall feature lights
ExistKeepLong CorridorRope lightningto change to match new dining rope1
ExistKeepSmall CorridorCeiling Lights
ExistKeepSmall CorridorElectrical point Cabinet lightnings LED
NewTo doSmall CorridorGypsumadd extra 10 cm with current partion to add cabinets1
ExistKeepEnsuiteCeiling lights
NewTo doEnsuiteChange shower glass + tray1
NewTo doEnsuiteCeilingneed painting1
ExistTo doMain Bathroomaccssories-sinkreplace with new1
ExistTo doMain Bathroomaccssories-flushingreplace with new1
NewTo doMain BathroomMirrorto attach1
NewTo doMain Bathroomsinkto attach1
NewTo doMain Bathroomcabinetsto attach1
NewTo doMain BathroomGyser11
12722229
roderick28@hotmail.co.uk
Fittings Bathrooms
Madum

works_Sheet2.csv

data/works_Sheet2.csv

RoderickEuros Per sqm145msq
tiles120x20304500
skirting4.5675
paint 5.5
Floor balcony40400
Bedroom office1100
behind sofa350
Point electric351855
Back balcony tiles (same as front)
change water from sink
gyser
bathroom cabinets
bathroom ensuite to change
small corridor partition

pdf_text/ — extracted drawing text

Copy_of__furniture_+_gypsum_measurement.txt

SOURCE: Furniture measurements/Copy of _furniture + gypsum measurement.pdf
PAGES: 1

--- page 1 ---
0052
low cabinets high cabinets for storage
wardrobe around ensuite door
for maximum storage
Entryway unit
1775
250
054
800 500 500
rorrim Sound proof gypsum partition to consist of'
1. Inside of study to have 2 layers of boards
2. Middle part to have a profile of 75 mm
filled with rockwool insulation
3. Outer part of study to have a single board
1952
0011
600
ksed
dna
deb
evoba
stenibac
0012
sevlehs desk
side
storage
and
shelving
above 1400
0002
350 300 005 350
0
0001
0002
side storage
cabinets above bed
tv
unit
shelving
enclosed
storage
1563,97
0002
drawers large mirror with dressing table
058
700
054 1050 500
5982 0981
500
0321 dap gniklaw
600
610
700 900
tnempiuqe
myg
1500
standing desk 006
311 113
1600
0972
600

Copy_of_kids_bedroom_measurement_opt4.txt

SOURCE: Furniture measurements/Copy of kids bedroom measurement_opt4.pdf
PAGES: 1

--- page 1 ---
low
cabinets
high
cabinets
for
storage
350
2591
1100
600
cabinets above bed and desk
2100
shelves
desk
evoba gnivlehs dna egarots edis
1400
2000
350
300
500
500 350
1000
2000
cabinets
above
bed
side
storage
O
p
t
i
o
n
4

balcony_plan.txt

SOURCE: Furniture measurements/balcony plan.pdf
PAGES: 1

--- page 1 ---
egarots deso
gas
cylinders
under
bbq
800
600
1500
900

furniture_layout_ELEVATIONS.txt

SOURCE: Furniture measurements/Elevations/furniture layout ELEVATIONS.pdf
PAGES: 1

--- page 1 ---
0052
low cabinets high cabinets for storage
wardrobe around ensuite door
for maximum storage
Entryway unit
1775
250
054
800 500 500
rorrim Sound proof gypsum partition to consist of'
1. Inside of study to have 2 layers of boards
2. Middle part to have a profile of 75 mm
filled with rockwool insulation
3. Outer part of study to have a single board
1952
0011
600
ksed dna
deb
evoba
stenibac
0002
sevlehs desk side
storage
and shelving
above 1500
0002
350 200 005 350 0001
0002
side storage
cabinets above bed
0002
tv
unit
Open
shelving
drawers large mirror with dressing table
058
700
054 1150 500
5982
500
0321 dap gniklaw
610 600
600 800
tnempiuqe
myg
1500
standing desk 006
311 113
1600
0972
600
052
0002
1050
500
500
675 1500
1400
1400
350
1600
1200
gas cylinders under bbq
008
1500
600
005
005
009
036
005
005
950
052052052
008
004
008
0531
008
004
008
Open
shelving
380
0984

kids_bedroom_only.txt

SOURCE: Furniture measurements/kids bedroom only.pdf
PAGES: 1

--- page 1 ---
0462
0052
Entryway unit
1775
004
1400
1400
0002
elevated furniture
drawers
desk sevlehs
1400
0002 ladder ksed
reddal
drawers
flehs koob 250 600
005
0012
1900
0091
039 006 300
600
0052
006
500
0041
1200
059
low cabinets high cabinets for storage 053
0011
bunk beds with desk underneath
1600
Option 1
1 0 1 2 3 4 5 6 7 8 9 10 m
0 10 20 30 ft
scale 1:100
ksed
0002
870
600
5532
drawers lwairtghe dmreirsrsoirng table
shelving ctaalblinets
001 058 1570
700
054 1050 500
5982 0981
500
700
850
wardrobe around ensuite door
for maximum storage
Gypsum wall

study_and_bedroom_revised.txt

SOURCE: Furniture measurements/study and bedroom revised.pdf
PAGES: 1

--- page 1 ---
0052
low cabinets high cabinets for storage
wardrobe around ensuite door
for maximum storage
Entryway unit
1775
250
054
800 500 500
rorrim Sound proof gypsum partition to consist of'
1. Inside of study to have 2 layers of boards
2. Middle part to have a profile of 75 mm
filled with rockwool insulation
3. Outer part of study to have a single board
1952
0011
600
ksed
dna
deb
evoba
stenibac
0012
sevlehs desk
side
storage
and
shelving
above 1400
0002
350 300 005 350
0
0001
0002
side storage
cabinets above bed
tv
unit
shelving
enclosed
storage
1563,97
0002
drawers large mirror with dressing table
058
700
054 1050 500
5982 0981
500
0321 dap gniklaw
600
610
700 900
tnempiuqe
myg
1500
standing desk 006
311 113
1600
0972
600

Measurement drawings

15 images. Click any image to enlarge. These are the dimensional source of truth. Two of them carry the scale bar the base plan was calibrated on.

Copy_of__furniture_+_gypsum_measurement_p1.jpg
Copy_of__furniture_+_gypsum_measurement_p1
Copy_of_kids_bedroom_measurement_opt4_p1.jpg
Copy_of_kids_bedroom_measurement_opt4_p1
balcony_plan_p1.jpg
balcony_plan_p1
corridor_dimensions_1.jpg
corridor_dimensions_1
furniture_layout_ELEVATIONS_p1.jpg
furniture_layout_ELEVATIONS_p1
kids_bedroom_dimensions_1.jpg
kids_bedroom_dimensions_1
kids_bedroom_dimensions_2.jpg
kids_bedroom_dimensions_2
kids_bedroom_only_p1.jpg
kids_bedroom_only_p1
main_bedroom_dimensions_1.jpg
main_bedroom_dimensions_1
main_bedroom_dimensions_2.jpg
main_bedroom_dimensions_2
openplan_dimensions_1.jpg
openplan_dimensions_1
openplan_dimensions_2.jpg
openplan_dimensions_2
openplan_dimensions_3.jpg
openplan_dimensions_3
study_and_bedroom_revised_p1.jpg
study_and_bedroom_revised_p1
tiles_sq_m.jpg
tiles_sq_m

Designer renders

19 images. Click any image to enlarge. Visual intent only — no dimensions.

10_corridor_revised.jpg
10_corridor_revised
12_corridor_small_revised.jpg
12_corridor_small_revised
15_dining_+_tv_unit_revised.jpg
15_dining_+_tv_unit_revised
1__dining__feature_wall_revised.jpg
1__dining__feature_wall_revised
2__sofa_revised.jpg
2__sofa_revised
7__dining_+_kitchen_revised.jpg
7__dining_+_kitchen_revised
Cy_kids_bedroom_10.jpg
Cy_kids_bedroom_10
Cy_kids_bedroom_7.jpg
Cy_kids_bedroom_7
Rafe_kids_bedroom_1.jpg
Rafe_kids_bedroom_1
Rafe_kids_bedroom_5.jpg
Rafe_kids_bedroom_5
balcony_1.jpg
balcony_1
balcony_2.jpg
balcony_2
main_bedroom_1.jpg
main_bedroom_1
main_bedroom_4-_wardrobe.jpg
main_bedroom_4-_wardrobe
main_bedroom_5-_vanity.jpg
main_bedroom_5-_vanity
main_bedroom_6-bed.jpg
main_bedroom_6-bed
study_1.jpg
study_1
study_2.jpg
study_2
study_3.jpg
study_3

Site photos (August 2026)

75 images. Click any image to enlarge. 21 are placed in the checklist, 54 are not. Captions come from docs/05-photo-index.md.

IMG_9826.jpg
IMG_9826 FBL
front balcony — BBQ corner, existing feed & camera
IMG_9827.jpg
IMG_9827 FBL
front balcony looking out, adhesive stacked
IMG_9828.jpg
IMG_9828 FBL
front balcony — already tiled, membrane at edge
IMG_9829.jpg
IMG_9829 FBL
balcony exterior wall, sea view
IMG_9830.jpg
IMG_9830
IMG_9831.jpg
IMG_9831
IMG_9832.jpg
IMG_9832 OPN
feature wall niche/alcove
IMG_9833.jpg
IMG_9833
IMG_9834.jpg
IMG_9834 OPN
open plan overview — parquet still down, Fanal 120 tiles delivered
IMG_9835.jpg
IMG_9835 OPN
kitchen island, units wrapped
IMG_9836.jpg
IMG_9836 OPN
corner, skirting, parquet
IMG_9837.jpg
IMG_9837 OPN
wall switch plate
IMG_9838.jpg
IMG_9838 OPN
ceiling downlights + kitchen units
IMG_9839.jpg
IMG_9839 OPN
dining feature wall + fireplace, wall opened below
IMG_9840.jpg
IMG_9840 OPN
living — existing socket run
IMG_9841.jpg
IMG_9841 OPN
built-in bench with socket
IMG_9842.jpg
IMG_9842 OPN
bench/shelf run
IMG_9843.jpg
IMG_9843 MCR
corridor switch position
IMG_9844.jpg
IMG_9844 MCR
corridor/entry area with hooks
IMG_9845.jpg
IMG_9845 ENT
EXISTING CONSUMER UNIT + thermostat + entrance door — key for OQ-03
IMG_9846.jpg
IMG_9846
IMG_9847.jpg
IMG_9847
IMG_9848.jpg
IMG_9848
IMG_9849.jpg
IMG_9849 MCR
main corridor looking through
IMG_9850.jpg
IMG_9850 MCR
main corridor — existing cove lighting
IMG_9851.jpg
IMG_9851
IMG_9852.jpg
IMG_9852
IMG_9853.jpg
IMG_9853
IMG_9854.jpg
IMG_9854
IMG_9855.jpg
IMG_9855
IMG_9856.jpg
IMG_9856
IMG_9857.jpg
IMG_9857
IMG_9858.jpg
IMG_9858
IMG_9859.jpg
IMG_9859
IMG_9860.jpg
IMG_9860
IMG_9861.jpg
IMG_9861
IMG_9862.jpg
IMG_9862
IMG_9863.jpg
IMG_9863
IMG_9864.jpg
IMG_9864 MCR
corridor ceiling downlight
IMG_9865.jpg
IMG_9865
IMG_9866.jpg
IMG_9866
IMG_9867.jpg
IMG_9867
IMG_9868.jpg
IMG_9868
IMG_9869.jpg
IMG_9869
IMG_9870.jpg
IMG_9870
IMG_9871.jpg
IMG_9871
IMG_9872.jpg
IMG_9872 BTH
main bathroom — existing grey marble finish
IMG_9873.jpg
IMG_9873 BTH
main bathroom — WC & window
IMG_9874.jpg
IMG_9874 BTH
grey marble bathroom, bedding stored in front
IMG_9875.jpg
IMG_9875 BTH
main bathroom — ceiling & downlights
IMG_9876.jpg
IMG_9876
IMG_9877.jpg
IMG_9877
IMG_9878.jpg
IMG_9878
IMG_9879.jpg
IMG_9879
IMG_9880.jpg
IMG_9880
IMG_9881.jpg
IMG_9881
IMG_9882.jpg
IMG_9882
IMG_9883.jpg
IMG_9883
IMG_9884.jpg
IMG_9884
IMG_9885.jpg
IMG_9885 ENS
ensuite WC + flush plate
IMG_9886.jpg
IMG_9886 ENS
ensuite — existing shower enclosure
IMG_9887.jpg
IMG_9887 ENS
ensuite ceiling/shower from above
IMG_9888.jpg
IMG_9888 ENS
ensuite — shower tray & glass (to be changed, ENS-05)
IMG_9889.jpg
IMG_9889 ENS
ensuite — WC & window
IMG_9890.jpg
IMG_9890 ENS
ensuite — overview
IMG_9891.jpg
IMG_9891 ENS
doorway, taped
IMG_9892.jpg
IMG_9892 OPN
recessed hatch with AV/equipment inside
IMG_9893.jpg
IMG_9893 OPN
fireplace close-up — installed, wall broken beneath
IMG_9894.jpg
IMG_9894 FBL
Fanal 31.6x90 tile boxes stacked on balcony — source of OQ-12
IMG_9895.jpg
IMG_9895 FBL
sliding door to front balcony
IMG_9896.jpg
IMG_9896 OPN
gypsum board delivered on site
IMG_9897_frame.jpg
IMG_9897_frame
IMG_9898_frame.jpg
IMG_9898_frame
IMG_9899_frame.jpg
IMG_9899_frame
IMG_9900_frame.jpg
IMG_9900_frame

build_index.py

build/build_index.py

"""Build index.html — a local browser for everything in this project.

    python3 build_index.py      # writes ../index.html, then open it in a browser

Design notes:
  * Text files (docs, CSV, JSON, scripts) are EMBEDDED, pre-rendered here in Python. That keeps
    the page working offline with no JS libraries and no fetch() — file:// blocks fetch, so an
    index that loaded its own docs at runtime would silently show nothing.
  * Images and PDFs are REFERENCED by relative path. img/iframe src is not blocked under file://,
    and embedding 109 images as base64 would make a ~30 MB page.
  * Therefore index.html must stay at the project root, beside images/ and output/.
"""
import os, re, json, html, csv, io, datetime

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.normpath(os.path.join(HERE, ".."))
rel  = lambda *p: os.path.join(ROOT, *p)
read = lambda p: open(p, encoding="utf-8", errors="replace").read()
E    = html.escape

# --------------------------------------------------------------------- markdown -> html
def md(src):
    """Small line-based markdown renderer. Covers what the docs actually use:
    headings, tables, fenced code, blockquotes, lists, hr, bold/italic/code/strike, links."""
    out, lines, i = [], src.split("\n"), 0
    list_stack = []

    def inline(t):
        t = E(t)
        t = re.sub(r"`([^`]+)`", r"<code>\1</code>", t)
        t = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", t)
        t = re.sub(r"~~([^~]+)~~", r"<del>\1</del>", t)
        t = re.sub(r"(?<![*\w])\*([^*\n]+)\*(?!\*)", r"<em>\1</em>", t)
        t = re.sub(r"\[\[([^\]]+)\]\]", r"<em>\1</em>", t)
        t = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'<a href="\2">\1</a>', t)
        return t

    def close_lists():
        while list_stack:
            out.append(f"</{list_stack.pop()}>")

    while i < len(lines):
        ln = lines[i]
        if ln.startswith("```"):
            close_lists(); i += 1; buf = []
            while i < len(lines) and not lines[i].startswith("```"):
                buf.append(lines[i]); i += 1
            out.append("<pre class='code'>" + E("\n".join(buf)) + "</pre>"); i += 1; continue
        if re.match(r"^\s*\|", ln) and i+1 < len(lines) and re.match(r"^\s*\|[\s:|-]+\|?\s*$", lines[i+1]):
            close_lists()
            cells = lambda r: [c.strip() for c in r.strip().strip("|").split("|")]
            head = cells(ln); i += 2; body = []
            while i < len(lines) and re.match(r"^\s*\|", lines[i]):
                body.append(cells(lines[i])); i += 1
            t = ["<table><thead><tr>" + "".join(f"<th>{inline(c)}</th>" for c in head) + "</tr></thead><tbody>"]
            for r in body:
                t.append("<tr>" + "".join(f"<td>{inline(c)}</td>" for c in r) + "</tr>")
            out.append("".join(t) + "</tbody></table>"); continue
        if re.match(r"^\s*(---|\*\*\*)\s*$", ln):
            close_lists(); out.append("<hr>"); i += 1; continue
        m = re.match(r"^(#{1,6})\s+(.*)$", ln)
        if m:
            close_lists(); lvl = len(m.group(1))
            out.append(f"<h{lvl}>{inline(m.group(2))}</h{lvl}>"); i += 1; continue
        if ln.startswith(">"):
            close_lists(); buf = []
            while i < len(lines) and lines[i].startswith(">"):
                buf.append(lines[i].lstrip("> ").rstrip()); i += 1
            out.append("<blockquote>" + inline(" ".join(buf)) + "</blockquote>"); continue
        m = re.match(r"^(\s*)([-*+]|\d+\.)\s+(.*)$", ln)
        if m:
            tag = "ol" if m.group(2)[0].isdigit() else "ul"
            if not list_stack or list_stack[-1] != tag:
                close_lists(); out.append(f"<{tag}>"); list_stack.append(tag)
            out.append(f"<li>{inline(m.group(3))}</li>"); i += 1; continue
        if not ln.strip():
            close_lists(); i += 1; continue
        close_lists(); buf = []
        while i < len(lines) and lines[i].strip() and not re.match(r"^(#|>|\s*\||\s*[-*+]\s|\s*\d+\.\s|```)", lines[i]):
            buf.append(lines[i]); i += 1
        out.append("<p>" + inline(" ".join(buf)) + "</p>")
    close_lists()
    return "\n".join(out)

def csv_table(path, limit=400):
    rows = list(csv.reader(io.StringIO(read(path))))
    if not rows: return "<p class='muted'>empty</p>"
    head, body = rows[0], rows[1:limit]
    t = ["<div class='scroll'><table class='data'><thead><tr>"
         + "".join(f"<th>{E(c)}</th>" for c in head) + "</tr></thead><tbody>"]
    for r in body:
        t.append("<tr>" + "".join(f"<td>{E(c)}</td>" for c in r) + "</tr>")
    t.append("</tbody></table></div>")
    if len(rows)-1 > limit:
        t.append(f"<p class='muted'>showing first {limit} of {len(rows)-1} rows</p>")
    return "".join(t)

# --------------------------------------------------------------------- photo captions
def photo_captions():
    """Pull the per-photo descriptions out of docs/05-photo-index.md so the gallery is annotated."""
    caps = {}
    for ln in read(rel("docs", "05-photo-index.md")).split("\n"):
        m = re.match(r"^\|\s*`?(IMG_\d+[a-z_]*)`?\s*\|\s*\*?\*?([A-Z]{3})?\*?\*?\s*\|\s*(.*?)\s*\|", ln)
        if m:
            caps[m.group(1)] = (m.group(2) or "", m.group(3))
    return caps
CAPS = photo_captions()

def gallery(folder, note=""):
    files = sorted(f for f in os.listdir(rel(*folder)) if f.lower().endswith((".jpg", ".jpeg", ".png")))
    cards = []
    for f in files:
        stem = os.path.splitext(f)[0]
        room, desc = CAPS.get(stem, ("", ""))
        src = "/".join(folder) + "/" + f
        badge = f"<span class='pill'>{E(room)}</span>" if room else ""
        cards.append(
            f"<figure class='card' data-search='{E((f + ' ' + room + ' ' + desc).lower())}'>"
            f"<img loading='lazy' src='{E(src)}' alt='{E(f)}' onclick=\"zoom('{E(src)}','{E(f)}')\">"
            f"<figcaption><b>{E(stem)}</b> {badge}<br><span class='muted'>{E(desc) or '&mdash;'}</span>"
            f"</figcaption></figure>")
    head = f"<p class='muted'>{len(files)} images. Click any image to enlarge.{' ' + note if note else ''}</p>"
    return head + "<div class='grid'>" + "".join(cards) + "</div>"

# --------------------------------------------------------------------- panes
PANES, NAV = {}, []

def pane(section, key, label, body, sub=""):
    PANES[key] = f"<h1 class='pane-h'>{E(label)}</h1>" + (f"<p class='pane-s'>{sub}</p>" if sub else "") + body
    NAV.append((section, key, label))

# --- overview
def overview():
    import plan_data
    rooms = plan_data.ROOMS
    a = lambda r: (r[4]-r[2]) * (r[5]-r[3])
    internal = sum(a(r) for r in rooms if r[0] != "BBL")
    wet = sum(a(r) for r in rooms if r[0] in ("BTH", "ENS"))
    oq = re.findall(r"^## (OQ-\d+) — (.*?)$", read(rel("docs", "02-open-questions.md")), re.M)
    oq_rows = "".join(f"<tr><td class='mono'>{E(c)}</td><td>{md(t).replace('<p>','').replace('</p>','')}</td></tr>"
                      for c, t in oq)
    return f"""
<div class='cards'>
  <div class='stat'><b>Mon 17 Aug 2026</b><span>start on site (delayed from 13 Aug)</span></div>
  <div class='stat'><b>Tue 17 Nov 2026</b><span>finish · 67 working days</span></div>
  <div class='stat'><b>81</b><span>work items across 13 areas</span></div>
  <div class='stat'><b>12</b><span>open questions, none answered</span></div>
  <div class='stat'><b>{internal:.0f} m²</b><span>internal floor, measured</span></div>
  <div class='stat'><b>{internal-wet:.0f} m²</b><span>less the wet rooms &mdash; cf. 125 m²</span></div>
</div>
<h2>What this is</h2>
<p>Kurt's refurbishment of the Zonqor apartment. <b>Roderick</b> (main contractor, with his son)
does gypsum, electrical, plumbing and tiling; <b>ULF</b> does carpentry; <b>Christine</b> walks the
site each evening, signs items off and agrees prices on paper. Print is the primary medium —
everything here is built to be printed.</p>
<h2>The two deliverables</h2>
<ul>
<li><b>Site Checklist</b> — 23 A4 pages: work plan, day-by-day, open questions, 13 room sections
with per-item price and sign-off fields, price summary, appendix of 49 photos.</li>
<li><b>Base Plan</b> — reconstructed floor plan, 12 rooms with areas and confidence grades, plus
the measure-on-site list. <b>A reconstruction, not a survey.</b></li>
</ul>
<h2>Open questions — all still unanswered</h2>
<table>{oq_rows}</table>
<h2>The three things to settle first</h2>
<ol>
<li><b>OQ-10 tiling scope</b> — before any price is agreed. The base plan shows the 125/145 m² gap
is the two balconies; what is still open is how much is actually being tiled (~22 m² of argument).</li>
<li><b>OQ-11 fireplace</b> — remove or keep? Needed by plan day 4, <b>Thu 20 Aug</b>.</li>
<li><b>OQ-03 consumer unit</b> and <b>OQ-08 intercom</b> — both change first-fix cable, week one.</li>
</ol>
"""

import sys
sys.path.insert(0, HERE)
pane("Start", "overview", "Zonqor Refurbishment — project index", overview(),
     "Everything in this folder, in one place. Built "
     + datetime.date.today().strftime("%d %B %Y"))

# --- deliverables
# PDFs are shown as pre-rendered page images, NOT in an <iframe>. An iframe depends on the
# browser having a PDF viewer and on file:// letting it load — headless Chromium shows nothing,
# and Safari is unreliable. Page images always render. The real PDF is one click away.
PREVIEW = rel("output", "preview")
os.makedirs(PREVIEW, exist_ok=True)

def pdf_pages(pdf_path, stem, dpi=96):
    src = rel(*pdf_path.split("/"))
    have = sorted(f for f in os.listdir(PREVIEW) if f.startswith(stem + "-") and f.endswith(".jpg"))
    if not have or os.path.getmtime(src) > min(os.path.getmtime(os.path.join(PREVIEW, f)) for f in have):
        for f in have:
            os.remove(os.path.join(PREVIEW, f))
        os.system(f'pdftoppm -jpeg -r {dpi} "{src}" "{os.path.join(PREVIEW, stem)}"')
        have = sorted(f for f in os.listdir(PREVIEW) if f.startswith(stem + "-") and f.endswith(".jpg"))
    return have

for key, label, path, stem, note in [
    ("d-check", "Site Checklist (PDF)", "output/Zonqor Site Checklist.pdf", "checklist",
     "The live document — this is what Christine prints and works from."),
    ("d-plan", "Base Plan (PDF)", "output/Zonqor Base Plan.pdf", "baseplan",
     "Reconstructed floor plan, room schedule, and the measure-on-site list."),
    ("d-photo", "Photo Sort Sheet (PDF)", "output/Photo Sort Sheet.pdf", "photosort",
     "Contact sheet used to try to place the 54 unassigned site photos."),
]:
    pages = pdf_pages(path, stem)
    imgs = "".join(
        f"<figure class='page'><img loading='lazy' src='output/preview/{E(f)}' "
        f"onclick=\"zoom('output/preview/{E(f)}','{E(label)} — page {n}')\">"
        f"<figcaption class='muted'>page {n} of {len(pages)}</figcaption></figure>"
        for n, f in enumerate(pages, 1))
    pane("Deliverables", key, label,
         f"<p class='muted'>{len(pages)} pages. {note} &middot; "
         f"<a href='{path}' target='_blank'><b>open the real PDF</b></a> "
         f"(print from that, not from these previews). Click a page to enlarge.</p>"
         f"<div class='pages'>{imgs}</div>")

pane("Deliverables", "d-html", "Site Checklist (HTML)",
     "<p class='muted'>Same document, with live-totalling price boxes. "
     "<a href='output/Zonqor Site Checklist.html' target='_blank'>Open full screen</a> to use it.</p>"
     "<iframe class='pdf' src='output/Zonqor Site Checklist.html'></iframe>")

# --- docs
DOCS = [("CLAUDE.md", "CLAUDE.md — master context"), ("README.md", "README.md")]
DOCS += [(f"docs/{f}", f) for f in sorted(os.listdir(rel("docs"))) if f.endswith(".md")]
for p, label in DOCS:
    pane("Documents", "doc-" + re.sub(r"\W+", "-", p), label, md(read(rel(p))))

# --- data
for f in sorted(os.listdir(rel("data"))):
    fp = rel("data", f)
    if f.endswith(".csv"):
        pane("Data", "data-" + re.sub(r"\W+", "-", f), f,
             f"<p class='muted'>data/{E(f)}</p>" + csv_table(fp))
    elif f.endswith(".json"):
        obj = json.loads(read(fp))
        pane("Data", "data-" + re.sub(r"\W+", "-", f), f,
             f"<p class='muted'>data/{E(f)} &middot; {len(obj) if isinstance(obj, list) else len(obj.keys())} "
             f"top-level entries</p><pre class='code'>{E(json.dumps(obj, indent=1)[:200000])}</pre>")
txt = sorted(os.listdir(rel("data", "pdf_text")))
pane("Data", "data-pdftext", "pdf_text/ — extracted drawing text",
     "".join(f"<h3>{E(f)}</h3><pre class='code'>{E(read(rel('data','pdf_text',f)))}</pre>" for f in txt))

# --- images
pane("Images", "img-meas", "Measurement drawings",
     gallery(("images", "measurements"),
             "These are the dimensional source of truth. Two of them carry the scale bar the "
             "base plan was calibrated on."))
pane("Images", "img-design", "Designer renders",
     gallery(("images", "design"), "Visual intent only — no dimensions."))
pane("Images", "img-site", "Site photos (August 2026)",
     gallery(("images", "site"),
             "21 are placed in the checklist, 54 are not. Captions come from docs/05-photo-index.md."))

# --- build
for f in sorted(os.listdir(rel("build"))):
    if f.endswith(".py"):
        pane("Build scripts", "b-" + re.sub(r"\W+", "-", f), f,
             f"<p class='muted'>build/{E(f)}</p><pre class='code'>{E(read(rel('build', f)))}</pre>")

# --------------------------------------------------------------------- assemble
sections, order = {}, []
for sec, key, label in NAV:
    sections.setdefault(sec, []).append((key, label))
    if sec not in order: order.append(sec)

nav_html = "".join(
    f"<div class='nav-sec'>{E(sec)}</div>" +
    "".join(f"<a class='nav-i' data-k='{k}' onclick=\"show('{k}')\">{E(l)}</a>" for k, l in sections[sec])
    for sec in order)
panes_html = "".join(f"<section id='p-{k}' class='pane'>{v}</section>" for k, v in PANES.items())

HTML = f"""<!doctype html><html lang="en"><head><meta charset="utf-8">
<title>Zonqor Refurbishment — project index</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
:root {{ --bg:#fff; --fg:#111827; --mut:#6b7280; --line:#e5e7eb; --accent:#b45309;
        --side:#f9fafb; --code:#f3f4f6; --sel:#fff7ed; }}
@media (prefers-color-scheme: dark) {{
  :root {{ --bg:#0f1115; --fg:#e5e7eb; --mut:#9ca3af; --line:#272b33; --accent:#f59e0b;
          --side:#151821; --code:#1a1e27; --sel:#2a1f0d; }}
}}
* {{ box-sizing:border-box; }}
body {{ margin:0; background:var(--bg); color:var(--fg); font:15px/1.6 -apple-system,
        "Segoe UI", Helvetica, Arial, sans-serif; }}
#side {{ position:fixed; inset:0 auto 0 0; width:270px; background:var(--side);
         border-right:1px solid var(--line); overflow-y:auto; padding:14px 0 40px; }}
#side h2 {{ font-size:14px; margin:0; padding:0 16px 10px; letter-spacing:.4px; }}
#side h2 span {{ color:var(--mut); font-weight:400; display:block; font-size:11px; }}
#q {{ width:calc(100% - 32px); margin:0 16px 12px; padding:7px 9px; font-size:13px;
      border:1px solid var(--line); border-radius:6px; background:var(--bg); color:var(--fg); }}
.nav-sec {{ font-size:10.5px; text-transform:uppercase; letter-spacing:.8px; color:var(--mut);
            padding:14px 16px 5px; }}
.nav-i {{ display:block; padding:5px 16px; font-size:13px; cursor:pointer; color:var(--fg);
          text-decoration:none; border-left:3px solid transparent; }}
.nav-i:hover {{ background:var(--code); }}
.nav-i.on {{ background:var(--sel); border-left-color:var(--accent); font-weight:600; }}
#main {{ margin-left:270px; padding:26px 34px 90px; max-width:1100px; }}
.pane {{ display:none; }} .pane.on {{ display:block; }}
.pane-h {{ font-size:23px; margin:0 0 4px; }}
.pane-s {{ color:var(--mut); margin:0 0 18px; }}
h1,h2,h3,h4 {{ line-height:1.25; }}
h2 {{ font-size:17px; margin:26px 0 8px; border-bottom:1px solid var(--line); padding-bottom:5px; }}
h3 {{ font-size:14.5px; margin:18px 0 6px; }}
p, li {{ font-size:14px; }}
a {{ color:var(--accent); }}
table {{ border-collapse:collapse; width:100%; margin:10px 0 16px; font-size:13px; }}
th {{ text-align:left; border-bottom:2px solid var(--line); padding:6px 8px; font-size:11.5px;
      text-transform:uppercase; letter-spacing:.4px; color:var(--mut); }}
td {{ border-bottom:1px solid var(--line); padding:6px 8px; vertical-align:top; }}
.scroll {{ overflow-x:auto; border:1px solid var(--line); border-radius:7px; margin:10px 0 16px; }}
.scroll table {{ margin:0; }}
table.data {{ font-size:11.5px; width:max-content; min-width:100%; }}
table.data td {{ white-space:nowrap; max-width:340px; overflow:hidden; text-overflow:ellipsis; }}
table.data th {{ position:sticky; top:0; background:var(--side); }}
code {{ background:var(--code); padding:1px 5px; border-radius:4px; font-size:12.5px; }}
pre.code {{ background:var(--code); padding:12px 14px; border-radius:7px; overflow-x:auto;
            font-size:12px; line-height:1.45; }}
blockquote {{ border-left:3px solid var(--accent); background:var(--sel); margin:12px 0;
              padding:9px 14px; border-radius:0 5px 5px 0; }}
blockquote p {{ margin:0; }}
hr {{ border:0; border-top:1px solid var(--line); margin:22px 0; }}
.muted {{ color:var(--mut); font-size:13px; }}
.mono {{ font-family:ui-monospace,Menlo,monospace; font-weight:600; white-space:nowrap; }}
iframe.pdf {{ width:100%; height:78vh; border:1px solid var(--line); border-radius:7px;
              background:#fff; }}
.cards {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(155px,1fr)); gap:10px;
          margin:6px 0 20px; }}
.stat {{ border:1px solid var(--line); border-radius:8px; padding:11px 13px; background:var(--side); }}
.stat b {{ display:block; font-size:19px; }}
.stat span {{ color:var(--mut); font-size:11.5px; }}
.grid {{ display:grid; grid-template-columns:repeat(auto-fill,minmax(185px,1fr)); gap:13px; }}
.card {{ margin:0; border:1px solid var(--line); border-radius:8px; overflow:hidden;
         background:var(--side); }}
.card img {{ width:100%; height:135px; object-fit:cover; display:block; cursor:zoom-in;
             background:var(--code); }}
.card figcaption {{ padding:7px 9px; font-size:11.5px; line-height:1.4; }}
.pill {{ background:var(--accent); color:#fff; border-radius:4px; padding:0 5px; font-size:10px;
         font-weight:700; }}
.pages {{ display:grid; grid-template-columns:repeat(auto-fill,minmax(290px,1fr)); gap:16px; }}
.page {{ margin:0; }}
.page img {{ width:100%; border:1px solid var(--line); border-radius:5px; background:#fff;
             cursor:zoom-in; display:block; }}
.page figcaption {{ padding-top:5px; font-size:11.5px; }}
#lb {{ position:fixed; inset:0; background:rgba(0,0,0,.92); display:none; z-index:99;
       align-items:center; justify-content:center; flex-direction:column; cursor:zoom-out; }}
#lb.on {{ display:flex; }}
#lb img {{ max-width:94vw; max-height:88vh; object-fit:contain; }}
#lb div {{ color:#eee; font-size:13px; padding:10px; }}
@media (max-width:820px) {{ #side {{ position:static; width:auto; height:auto; }} #main {{ margin:0; padding:18px; }} }}
</style></head><body>

<nav id="side">
  <h2>Zonqor Refurbishment<span>project index &middot; {datetime.date.today().strftime('%d %b %Y')}</span></h2>
  <input id="q" placeholder="Filter files…" oninput="filt(this.value)">
  {nav_html}
</nav>
<main id="main">{panes_html}</main>
<div id="lb" onclick="this.classList.remove('on')"><img id="lbi"><div id="lbc"></div></div>

<script>
function show(k) {{
  document.querySelectorAll('.pane').forEach(p => p.classList.remove('on'));
  document.querySelectorAll('.nav-i').forEach(a => a.classList.remove('on'));
  const p = document.getElementById('p-' + k); if (p) p.classList.add('on');
  const a = document.querySelector('.nav-i[data-k="' + k + '"]'); if (a) a.classList.add('on');
  document.getElementById('main').scrollTo(0, 0); window.scrollTo(0, 0);
  history.replaceState(null, '', '#' + k);
}}
function zoom(src, cap) {{
  document.getElementById('lbi').src = src;
  document.getElementById('lbc').textContent = cap;
  document.getElementById('lb').classList.add('on');
}}
function filt(v) {{
  v = v.trim().toLowerCase();
  document.querySelectorAll('.nav-i').forEach(a => {{
    a.style.display = (!v || a.textContent.toLowerCase().includes(v)) ? '' : 'none';
  }});
  document.querySelectorAll('.nav-sec').forEach(s => {{
    let n = s.nextElementSibling, any = false;
    while (n && n.classList.contains('nav-i')) {{ if (n.style.display !== 'none') any = true; n = n.nextElementSibling; }}
    s.style.display = any ? '' : 'none';
  }});
  document.querySelectorAll('.card').forEach(c => {{
    c.style.display = (!v || (c.dataset.search || '').includes(v)) ? '' : 'none';
  }});
}}
document.addEventListener('keydown', e => {{ if (e.key === 'Escape') document.getElementById('lb').classList.remove('on'); }});
// Without this, changing the hash (back/forward, a pasted deep link) leaves the pane behind.
window.addEventListener('hashchange', () => show(location.hash.slice(1) || 'overview'));
show(location.hash ? location.hash.slice(1) : 'overview');
</script></body></html>"""

out = rel("index.html")
open(out, "w", encoding="utf-8").write(HTML)
print(f"wrote {out}  |  {len(PANES)} panes, {len(HTML)/1024:.0f} KB")

build_plan.py

build/build_plan.py

"""Build the reconstructed base plan — print-first A4, same house style as the checklist.

    python3 build_plan.py     # writes ../output/base_plan.html
    python3 pdfplan.py        # renders ../output/Zonqor Base Plan.pdf

Geometry comes from plan_data.py and nowhere else. Print is the primary medium.
"""
import os, sys, datetime
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from plan_data import (ROOMS, ENVELOPE, WALL_T, CEILING, BALCONY_ARC,
                       ALREADY_FINISHED, TO_MEASURE, SCALE_NOTE)

SCALE = 130                     # 1:130 — the whole 28.7 m plus margins fits one A4 page
MM = 1000.0 / SCALE             # mm on paper per metre
# viewBox in metres: x, y, width, height. The <svg> box below is derived from this so the
# rendered scale is EXACTLY 1:SCALE — if the two aspect ratios disagree the browser
# letterboxes and the printed scale silently stops matching the label on the sheet.
VB = (-1.6, -1.4, 12.6, 31.4)
CONF = {"A": ("#dbeafe", "read consistently off both sheets"),
        "B": ("#fef3c7", "single reading — check on site"),
        "C": ("#fee2e2", "inferred — must be confirmed")}

area = lambda r: (r[4]-r[2]) * (r[5]-r[3])
BAL_S = 15.8                                            # front balcony, traced arc
internal = sum(area(r) for r in ROOMS if r[0] != "BBL")
wet      = sum(area(r) for r in ROOMS if r[0] in ("BTH", "ENS"))
bal_n    = sum(area(r) for r in ROOMS if r[0] == "BBL")

# ----------------------------------------------------------------------- the drawing
def svg():
    p = []
    env = " ".join(f"{x},{y}" for x, y in ENVELOPE)
    p.append(f'<polygon points="{env}" fill="#fff" stroke="#111" stroke-width="0.16"/>')

    a = BALCONY_ARC
    p.append(f'<path d="M 7.21,26.00 L {a["start"][0]},{a["start"][1]} '
             f'A 5.4,5.4 0 0 1 {a["end"][0]},{a["end"][1]} '
             f'L {a["west_edge"][0]},{a["west_edge"][1]} L 0,26.00 Z" '
             f'fill="#fee2e2" stroke="#111" stroke-width="0.10"/>')
    p.append('<text x="2.6" y="26.95" class="rc" text-anchor="middle">FBL</text>')
    p.append('<text x="2.6" y="27.45" class="ra" text-anchor="middle">~15.8 m² · curved parapet</text>')

    for code, name, x0, y0, x1, y1, cf, _ in ROOMS:
        w, h = x1-x0, y1-y0
        p.append(f'<rect x="{x0}" y="{y0}" width="{w}" height="{h}" fill="{CONF[cf][0]}" '
                 f'stroke="#111" stroke-width="0.07"/>')
        cx, cy = x0+w/2, y0+h/2
        a_m = area((code, name, x0, y0, x1, y1))
        if w < 1.6:                                      # too narrow for horizontal text — rotate it
            p.append(f'<text x="{cx}" y="{cy}" class="rc" text-anchor="middle" '
                     f'transform="rotate(-90 {cx} {cy})">{code} · {w:.2f}×{h:.2f} · {a_m:.1f} m²</text>')
        elif h < 1.15:                                   # thin strip — label outside with a leader
            p.append(f'<text x="{x1+0.35}" y="{cy+0.12}" class="rc">{code} · {a_m:.1f} m²</text>')
            p.append(f'<line x1="{x1}" y1="{cy}" x2="{x1+0.3}" y2="{cy}" '
                     f'stroke="#111" stroke-width="0.03"/>')
        else:
            n_lines = 3 if h >= 2.0 else 2               # only fit the area line if the room is deep
            p.append(f'<text x="{cx}" y="{cy-(0.25 if n_lines==3 else 0.1)}" class="rc" '
                     f'text-anchor="middle">{code}</text>')
            p.append(f'<text x="{cx}" y="{cy+(0.28 if n_lines==3 else 0.42)}" class="ra" '
                     f'text-anchor="middle">{w:.2f} × {h:.2f}</text>')
            if n_lines == 3:
                p.append(f'<text x="{cx}" y="{cy+0.78}" class="ra" text-anchor="middle">'
                         f'{a_m:.1f} m²</text>')

    # the two light wells on the west side
    for lx, ly, lw, lh in [(0.0, 4.60, 2.19, 4.00), (0.0, 13.50, 2.43, 6.09)]:
        p.append(f'<rect x="{lx-1.5}" y="{ly}" width="{lw+1.5}" height="{lh}" fill="#f3f4f6" '
                 f'stroke="#9ca3af" stroke-width="0.05" stroke-dasharray="0.25,0.18"/>')
        p.append(f'<text x="{lx-1.35}" y="{ly+lh/2}" class="ra">light well</text>')

    # overall dimensions
    p.append('<line x1="0" y1="-0.75" x2="7.21" y2="-0.75" stroke="#b45309" stroke-width="0.05"/>')
    p.append('<text x="3.6" y="-0.95" class="dim" text-anchor="middle">7.21 m overall</text>')
    p.append('<line x1="8.0" y1="0" x2="8.0" y2="26.00" stroke="#b45309" stroke-width="0.05"/>')
    p.append('<text x="8.35" y="13.0" class="dim" transform="rotate(90 8.35 13.0)" '
             'text-anchor="middle">26.00 m overall (excl. front balcony)</text>')

    # north arrow + scale bar
    p.append('<path d="M 9.3,3.0 L 9.6,4.1 L 9.3,3.85 L 9.0,4.1 Z" fill="#111"/>')
    p.append('<text x="9.3" y="2.7" class="ra" text-anchor="middle">N</text>')
    for i in range(5):
        p.append(f'<rect x="{8.9+i*0.4}" y="6.0" width="0.4" height="0.22" '
                 f'fill="{"#111" if i%2==0 else "#fff"}" stroke="#111" stroke-width="0.02"/>')
    p.append('<text x="8.9" y="6.75" class="ra">0</text>')
    p.append('<text x="10.9" y="6.75" class="ra" text-anchor="end">2 m</text>')
    return "\n".join(p)

rows = "\n".join(
    f'<tr><td class="c">{c}</td><td>{n}</td><td class="n">{x1-x0:.2f}</td>'
    f'<td class="n">{y1-y0:.2f}</td><td class="n"><b>{area((c,n,x0,y0,x1,y1)):.1f}</b></td>'
    f'<td class="cf cf{cf}">{cf}</td><td class="nt">{note}</td></tr>'
    for c, n, x0, y0, x1, y1, cf, note in ROOMS)

measure = "\n".join(f'<tr><td class="c">{w}</td><td><b>{what}</b></td><td class="nt">{why}</td></tr>'
                    for w, what, why in TO_MEASURE)

HTML = f"""<meta charset="utf-8"><title>Zonqor Base Plan</title>
<style>
@page {{ size: A4; margin: 11mm 10mm 12mm 10mm; }}
* {{ box-sizing: border-box; }}
body {{ font: 9pt/1.35 "Helvetica Neue", Arial, sans-serif; color: #111; margin: 0; }}
h1 {{ font-size: 15pt; margin: 0 0 1mm; }}
h2 {{ font-size: 11pt; margin: 6mm 0 2mm; border-bottom: 1.2pt solid #111; padding-bottom: 1mm; }}
.sub {{ color: #555; margin-bottom: 3mm; }}
.warn {{ border: 1pt solid #b45309; background: #fffbeb; padding: 2.5mm 3mm; margin: 3mm 0;
         font-size: 8.5pt; border-radius: 1mm; }}
.warn b {{ color: #b45309; letter-spacing: .3pt; }}
.wrap {{ display: flex; gap: 6mm; align-items: flex-start; }}
svg {{ flex: none; }}
.side {{ font-size: 8pt; }}
.key div {{ margin-bottom: 1.5mm; }}
.sw {{ display: inline-block; width: 4mm; height: 3mm; border: .5pt solid #111;
       margin-right: 1.5mm; vertical-align: -.3mm; }}
table {{ width: 100%; border-collapse: collapse; font-size: 7.6pt; }}
th {{ text-align: left; border-bottom: 1pt solid #111; padding: 1.2mm 1mm; font-size: 7pt;
      letter-spacing: .3pt; text-transform: uppercase; }}
td {{ border-bottom: .4pt solid #ddd; padding: 1.2mm 1mm; vertical-align: top; }}
td.c {{ font-family: monospace; font-weight: bold; white-space: nowrap; }}
td.n {{ text-align: right; font-variant-numeric: tabular-nums; }}
td.nt {{ color: #555; font-size: 7.2pt; }}
td.cf {{ text-align: center; font-weight: bold; }}
.cfA {{ background: #dbeafe; }} .cfB {{ background: #fef3c7; }} .cfC {{ background: #fee2e2; }}
.rc {{ font: bold 0.30px sans-serif; }}
.ra {{ font: 0.24px sans-serif; fill: #444; }}
.dim {{ font: 0.28px sans-serif; fill: #b45309; }}
.newpage {{ page-break-before: always; }}
.tot td {{ border-top: 1pt solid #111; font-weight: bold; background: #f9fafb; }}
</style>

<h1>Zonqor Apartment — Reconstructed Base Plan</h1>
<div class="sub">Traced from the designer's 1:100 layout · scale {SCALE and f"1:{SCALE}"} on this sheet ·
drawn {datetime.date.today().strftime('%d %B %Y')}</div>

<div class="wrap">
<svg width="{VB[2]*MM:.2f}mm" height="{VB[3]*MM:.2f}mm" viewBox="{VB[0]} {VB[1]} {VB[2]} {VB[3]}">
{svg()}
</svg>
<div class="side">
  <div class="warn" style="margin-top:0"><b>⚠ A RECONSTRUCTION, NOT A SURVEY.</b>
  Nobody has measured the flat. Every dimension here was scaled off the designer's furniture-layout
  drawing, which itself carries the note <i>"All dimensions to be verified on site prior to
  fabrication and installation."</i> Overall accuracy is about ±3% ({SCALE_NOTE}). Rooms shaded
  amber or red are worse than that. <b>Do not order materials or cut anything to these figures.</b>
  Use the plan to set out and to argue about scope; measure before you buy.</div>
  <h2 style="margin-top:4mm">How to read it</h2>
  <div class="key">
    <div><span class="sw" style="background:#dbeafe"></span><b>A</b> — {CONF['A'][1]}</div>
    <div><span class="sw" style="background:#fef3c7"></span><b>B</b> — {CONF['B'][1]}</div>
    <div><span class="sw" style="background:#fee2e2"></span><b>C</b> — {CONF['C'][1]}</div>
  </div>
  <p>North is up. The flat is a single long bay, <b>7.21 m wide and 26.00 m deep</b>, with the front
  balcony beyond the south wall. External walls measure <b>{WALL_T:.2f} m</b> throughout.</p>
  <p>The west side steps in twice. Those two <b>light wells</b> are what the bathroom, study, ensuite
  and entrance take their windows from — which is the strongest single check on this reconstruction,
  because the site photos show windows in exactly those four rooms and nowhere else on that side.</p>
  <p>Only one height is known: <b>{CEILING:.2f} m floor to ceiling</b> in the dining area
  (830 + 1050 + 730 mm off the feature-wall elevation). Every other room is unmeasured.</p>
  <h2>Where the scale came from</h2>
  <p>The sheet <i>kids_bedroom_only</i> carries a printed scale bar: 10 m spans 599 px, with 1 m
  ticks at exactly 60.0 px. The sheet <i>study_and_bedroom_revised</i> is the same drawing plotted
  1.3137× larger, which works out to exactly 1:100 on A4. Two independent routes to the same scale,
  agreeing to <b>0.08%</b>.</p>
</div>
</div>

<div class="newpage"></div>
<h2>Room schedule</h2>
<table>
<thead><tr><th>Code</th><th>Room</th><th style="text-align:right">Width m</th>
<th style="text-align:right">Depth m</th><th style="text-align:right">Floor m²</th>
<th style="text-align:center">Conf</th><th>Notes and what is uncertain</th></tr></thead>
<tbody>
{rows}
<tr class="tot"><td></td><td>Rooms measured above (excl. front balcony)</td><td></td><td></td>
<td class="n">{sum(area(r) for r in ROOMS):.1f}</td><td></td><td></td></tr>
</tbody></table>

<h2>What this settles about the tiling quantity (OQ-10)</h2>
<div class="warn" style="border-color:#166534; background:#f0fdf4">
<b style="color:#166534">THE 125 vs 145 m² GAP IS THE TWO BALCONIES.</b><br>
Measured off the plan, the internal rooms come to <b>{internal:.1f} m²</b>. Take out the main
bathroom and ensuite — which the site photos show are already finished — and you are left with
<b>{internal-wet:.1f} m²</b>. The designer's plan is marked <b>125 m²</b>. Those agree to 1.8%,
inside the tolerance of this reconstruction.<br><br>
Add the two balconies — back {bal_n:.1f} m² plus front ~{BAL_S:.1f} m² = <b>{bal_n+BAL_S:.1f} m²</b> —
and you land almost exactly on the <b>145 m²</b> in Roderick's quote. The gap between the two figures
is <b>{145-125} m²</b>, and the balconies are <b>{bal_n+BAL_S:.1f} m²</b>.<br><br>
<b>So the two numbers are not in conflict — they are measuring different things.</b> 125 m² is the
internal floor excluding the wet rooms; 145 m² is everything including balconies. The commercial
question is therefore not "which number is right" but <b>"how much of it is actually being
tiled?"</b> — and the photographs say the ensuite, the main bathroom and the front balcony are
already tiled. On that reading the new 120×120 area is nearer <b>{internal-wet:.0f} m²</b> than 145.
That is a difference of roughly <b>{145-(internal-wet):.0f} m²</b> to settle before any price is agreed.
</div>
<p style="font-size:8pt;color:#555">This is an arithmetic argument off a reconstructed plan, not a
measurement of the flat, and it does not tell you whether Roderick intended to re-tile the wet rooms.
It tells you what to ask him, and roughly how much money is in the answer.</p>

<h2>Measure these on site</h2>
<table>
<thead><tr><th>Where</th><th>What</th><th>Why it matters</th></tr></thead>
<tbody>
{measure}
</tbody></table>
<p style="font-size:8pt;color:#555;margin-top:4mm">Next: the electrical plan is drawn <i>on top of</i>
this base plan, so the items above should be checked before any point layout is committed. See
<code>docs/08-whats-missing.md</code> section B.</p>
"""

out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "output", "base_plan.html")
open(out, "w").write(HTML)
print(f"wrote {os.path.normpath(out)}  |  {len(ROOMS)} rooms, "
      f"internal {internal:.1f} m², internal less wet rooms {internal-wet:.1f} m²")

build_v3.py

build/build_v3.py

import base64, os, re, sys, datetime
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from rooms_data2 import ROOMS, ROOM_IMAGES, PHASES

IMG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "img_print")
THUMB_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "img_thumb")

def b64(d, f):
    with open(os.path.join(d, f), "rb") as fh:
        return base64.b64encode(fh.read()).decode("ascii")

GROUP_ORDER = ["gypsum", "elec", "plumb", "tile", "fit"]
GROUP_LABEL = {"gypsum": "Gypsum / Structural", "elec": "Electrical",
               "plumb": "Plumbing / Gas", "tile": "Tiling", "fit": "Fittings / Carpentry"}
ASSIGNED = {"gypsum": "Roderick", "elec": "Roderick", "plumb": "Roderick",
            "tile": "Roderick", "fit": "ULF"}

# ----------------------------------------------------------- appendix numbering
img_id, img_order = {}, []
for name, code, _b, _i in ROOMS:
    for fname, cap in ROOM_IMAGES.get(code, []):
        if fname in img_id:
            next(e for e in img_order if e[1] == fname)[3].append(code)
            continue
        img_id[fname] = "A%02d" % (len(img_order) + 1)
        img_order.append((img_id[fname], fname, cap, [code]))

placeholder = {}
for name, code, _b, _i in ROOMS:
    if not ROOM_IMAGES.get(code):
        placeholder[code] = ("A%02d" % (len(img_order) + len(placeholder) + 1), name)

def ref_ids(code):
    if code in placeholder:
        return [placeholder[code][0]]
    out = []
    for fname, _c in ROOM_IMAGES.get(code, []):
        if img_id[fname] not in out:
            out.append(img_id[fname])
    return out

def compress(ids):
    if not ids: return ""
    nums = sorted(int(i[1:]) for i in ids)
    runs, s, p = [], nums[0], nums[0]
    for n in nums[1:]:
        if n == p + 1: p = n; continue
        runs.append((s, p)); s = p = n
    runs.append((s, p))
    return ", ".join("A%02d" % a if a == b else "A%02d&ndash;A%02d" % (a, b) for a, b in runs)

# ------------------------------------------------------------------ item render
def default_dep(g, s1, s2, s3):
    if g == "gypsum": return "start"
    if g in ("elec", "plumb"): return "gypsum (this room)" if s1 else "start"
    if g == "tile":
        return "elec &amp; plumbing rough-in (this room)" if s2 else ("gypsum (this room)" if s1 else "start")
    if g == "fit":
        if s3: return "tiling (this room)"
        if s2: return "elec &amp; plumbing rough-in (this room)"
        if s1: return "gypsum (this room)"
    return "start"

def render_item(it, dep, iid, code, ph):
    a = ASSIGNED[it["group"]]
    acls = "a-rod" if a == "Roderick" else "a-ulf"
    note = f'<span class="i-note">{it["notes"]}</span>' if it["notes"] else ""
    pht = f'<span class="phase-chip">Ph {ph}</span>' if ph else ""
    return f'''    <div class="wi" data-room="{code}">
      <div class="wi-h"><span class="wi-id">{iid}</span><span class="wi-t">{it["title"]}</span>{pht}<span class="wi-a {acls}">{a}</span></div>
      <div class="wi-m">{it["tags"]}<span class="dep">after: {dep}</span>{note}</div>
      <div class="wi-f"><span class="fl">Status</span><label class="cb"><input type="checkbox" class="st-done" onchange="onDone(this)"><span>Done</span></label><label class="cb"><input type="checkbox"><span>Part</span></label><label class="cb"><input type="checkbox"><span>Issue</span></label><span class="fl fl2">Date</span><span class="wl"></span><span class="fl fl-p">Price &euro;</span><input type="text" inputmode="decimal" class="price" data-room="{code}" oninput="recalc()"><span class="fl fl2">Init</span><span class="wl wl-s"></span></div>
      <div class="wi-c"><span class="fl">Comments</span><span class="nl"></span></div>
    </div>'''

def render_thumbs(code):
    imgs = ROOM_IMAGES.get(code, [])
    if not imgs:
        aid, nm = placeholder[code]
        return (f'    <div class="rthumbs"><div class="thumb-none">'
                f'<b>{aid}</b> &mdash; no designer picture for {nm}. Take an on-site photo; slot reserved in the appendix.'
                f'</div></div>')
    seen, cells = set(), []
    for f, c in imgs:
        if f in seen: continue
        seen.add(f)
        cells.append(f'''      <figure class="tn" onclick="lb(this)"><img src="data:image/jpeg;base64,{b64(THUMB_DIR, f)}" alt="{c}" data-cap="{img_id[f]} — {c}"><figcaption>{img_id[f]}</figcaption></figure>''')
    return '    <div class="rthumbs">\n' + "\n".join(cells) + '\n    </div>'

def render_room(name, code, banner, items):
    groups = {}
    for it in items:
        groups.setdefault(it["group"], []).append(it)
    s1 = bool(groups.get("gypsum")); s2 = bool(groups.get("elec")) or bool(groups.get("plumb")); s3 = bool(groups.get("tile"))
    ids = ref_ids(code)
    refs = (f'<span class="rref rref-no">{ids[0]} &mdash; photo needed</span>' if code in placeholder
            else f'<span class="rref">Pictures {compress(ids)}</span>')
    parts = [render_thumbs(code)]
    if banner:
        parts.append(f'    <div class="banner">{banner}</div>')
    n = 1
    for g in GROUP_ORDER:
        if g not in groups: continue
        parts.append(f'    <div class="tgrp tg-{g}">{GROUP_LABEL[g]}</div>')
        for it in groups[g]:
            parts.append(render_item(it, it["dep_override"] or default_dep(g, s1, s2, s3),
                                     f"{code}-{n:02d}", code, it.get("phase")))
            n += 1
    parts.append(f'    <div class="rtot"><span class="fl">{name} &mdash; total agreed</span>'
                 f'<span class="rtv">&euro; <span class="rt" id="rt-{code}"></span></span></div>')
    return f'''  <section class="room">
    <div class="rhead"><h2>{name}</h2><span class="rcode">{code}</span>{refs}<span class="rcount">{len(items)} items</span></div>
{chr(10).join(parts)}
  </section>'''

rooms_html = "\n".join(render_room(n, c, b, i) for n, c, b, i in ROOMS)

summary_rows = "\n".join(
    f'<tr><td class="s-code">{c}</td><td>{n}</td><td class="s-n">{len(i)}</td>'
    f'<td class="s-val">&euro; <span class="rt" id="st-{c}"></span></td><td class="s-sign"></td></tr>'
    for n, c, b, i in ROOMS)

# ------------------------------------------------------------------- appendix
ap = "\n".join(
    f'''      <figure class="ap" onclick="lb(this)"><img src="data:image/jpeg;base64,{b64(IMG_DIR, f)}" alt="{c}" data-cap="{aid} — {c}">
        <figcaption><b>{aid}</b> {c} <span class="ap-r">[{", ".join(sorted(set(cs)))}]</span></figcaption></figure>'''
    for aid, f, c, cs in img_order)
ph_html = "\n".join(
    f'''      <figure class="ap"><div class="photo-slot">Attach on-site photo<br>{nm}</div>
        <figcaption><b>{aid}</b> {nm} &mdash; photo needed <span class="ap-r">[{cd}]</span></figcaption></figure>'''
    for cd, (aid, nm) in placeholder.items())

# ------------------------------------------------------------------ day plan
START = datetime.date(2026, 8, 17)   # Monday 17 August 2026 — delayed from Thu 13 Aug
# Never hardcode the start date in the HTML below; derive it from START so the two cannot drift.
DAYS = [
 (1,"Site set-up: protect what stays, isolate services. Start lifting parquet — Open Plan.","WHS-01"),
 (1,"Continue parquet lift — bedrooms and corridors.","WHS-01"),
 (1,"Finish parquet lift. Strip out old ceiling lights, bedside lamps and window plugs.","WHS-01, MBR-02, MBR-03, MBR-04"),
 (1,"Demolish dining feature wall + fireplace. <b>Check for live services in the wall first.</b>","OPN-01"),
 (1,"Clear demolition debris, first skip out. Walk the back balcony and write in its scope.","WHS-02, BBL-01"),
 (2,"Set out and confirm the new consumer unit position at the entrance (OQ-03 must be answered).","ENT-01"),
 (2,"Install consumer unit, terminate mains. <b>Nothing else energises until this is in.</b>","ENT-01"),
 (2,"Chase entrance and corridors. Pull intercom cable.","ENT-02, MCR-01, MCR-02"),
 (2,"Open Plan kitchen zone — ceiling lights, hood, dedicated oven circuit.","OPN-03, OPN-04, OPN-05"),
 (2,"Open Plan dining/living — cabinet lights, rope light, switches, plugs, TV point.","OPN-06 to OPN-10"),
 (2,"Ethernet runs back to the entrance unit. Relocate double-pole switch.","OPN-11, OPN-12, OFF-08"),
 (2,"Kitchen sink onto main feed, RO connection left ready.","OPN-13"),
 (2,"Main bedroom first fix — ceiling, rope, bedside, vanity, wardrobe LED.","MBR-05 to MBR-09"),
 (2,"Study/office first fix — ceiling, under-desk LED, plugs, switches, LED bar.","OFF-02 to OFF-07"),
 (2,"Cy's bedroom first fix.","CYB-01 to CYB-05"),
 (2,"Rafe's bedroom first fix.","RAF-01 to RAF-05"),
 (2,"Main bathroom — gyser position, pipework and point. Ensuite switch route.","BTH-01"),
 (2,"Front balcony — side lights, floor lights, switches, waterproof plugs, camera point.","FBL-01 to FBL-08"),
 (2,"Front balcony gas line + pressure test and certificate.","FBL-09"),
 (2,"Corridor emergency light short circuit. Test all existing feeds.","MCR-03, MCR-04, WHS-03"),
 (2,"<b>First-fix sign-off walk.</b> Photograph every open chase before anything is closed up.","all first fix"),
 (3,"Second skip — chasing debris out, sweep, floors prepped for tiling.","WHS-02"),
 (4,"Set out and frame the study partition.","OFF-01"),
 (4,"Study partition — rockwool insulation and inner two layers of board.","OFF-01"),
 (4,"Study partition — outer board, tape and joint.","OFF-01"),
 (4,"Small corridor partition extended +10cm for the new cabinets.","SCR-01"),
 (4,"Gypsum behind sofa (250mm x 1m) including the rope-light housing.","OPN-02"),
 (4,"Main bedroom — close off old ceiling light area.","MBR-01"),
 (5,"Screed check and levelling — open plan.","WHS-04"),
 (5,"Screed levelling — bedrooms and corridors.","WHS-04"),
 (5,"Tank ensuite floor and shower walls. <b>Must be done before any ensuite tile.</b>","ENS-03"),
 (5,"Set out 120x120 in the open plan — agree the starting line and cut positions with Christine.","WHS-05"),
 (5,"Tile open plan — living/dining.","WHS-05"),
 (5,"Tile open plan — kitchen end.","WHS-05"),
 (5,"Tile entrance and main corridor.","WHS-05"),
 (5,"Tile main corridor and small corridor.","WHS-05"),
 (5,"Tile main bedroom.","WHS-05"),
 (5,"Tile study and Cy's bedroom.","WHS-05"),
 (5,"Tile Rafe's bedroom.","WHS-05"),
 (5,"Tile front balcony — weather permitting.","WHS-05"),
 (5,"Tile back balcony — weather permitting.","WHS-05"),
 (5,"Ensuite 60x30 tiling — walls.","ENS-04"),
 (5,"Ensuite tiling — floor, then grout.","ENS-04"),
 (5,"Grout throughout, clean down, protect finished floors.","WHS-05"),
 (6,"Patching and making good at all tile/wall junctions.","—"),
 (6,"Finish patching, sand, prep every surface for paint.","—"),
 (7,"Prime and seal all new gypsum.","—"),
 (7,"Ceilings — bedrooms and study.","—"),
 (7,"Ceilings — open plan and corridors. Ensuite ceiling.","ENS-01"),
 (7,"Walls — first coat throughout.","—"),
 (7,"Walls — second coat and cutting in.","—"),
 (8,"Fit sockets and switches — open plan and entrance. Intercom unit on.","ENT-02"),
 (8,"Fit sockets and switches — corridors and bedrooms.","—"),
 (8,"Hang light fittings — open plan and corridors.","—"),
 (8,"Hang light fittings — bedrooms and study. Rope and LED strips.","—"),
 (8,"Wardrobe LED sensors, under-furniture LED, LED bar at study door.","MBR-09, CYB-03, RAF-03"),
 (8,"<b>Full circuit test, RCD test, certification.</b> Confirm the oven no longer trips.","ENT-01, OPN-05"),
 (9,"ULF starts wardrobes — main bedroom. Roderick on call for cut-ins.","MBR-11"),
 (9,"ULF wardrobes — Cy's and Rafe's bedrooms.","—"),
 (9,"Roderick cuts in gyser/pump switches and the ensuite switch.","MBR-10, ENS-02"),
 (10,"Shower glass and tray; ensuite finished.","ENS-05"),
 (10,"Bathroom sanitary — sink, cabinets, flush accessories, mirror.","BTH-02 to BTH-06"),
 (10,"Mirrors — corridor 3x1.5m, office 2x2m, main bedroom vanity.","MCR-06, OFF-09, MBR-12"),
 (10,"Outdoor wall finish — PVC or tiles, whichever OQ-01 decided.","FBL-10"),
 (10,"Vent covers, PVC sheet on TV wall, remaining fittings.","MCR-05, OPN-14"),
 (11,"<b>Snagging walk with Christine</b> against this checklist, room by room.","all"),
 (11,"Fix snags, final clean, hand over certificates and warranties.","all"),
]

def workdays(start, n):
    out, d = [], start
    while len(out) < n:
        if d.weekday() < 5:
            out.append(d)
        d += datetime.timedelta(days=1)
    return out

dates = workdays(START, len(DAYS))
PH_NAMES = {
 1:"Strip-out &amp; demolition", 2:"First fix — plumbing, electrical, ethernet, gas",
 3:"Clear site — skip", 4:"Structural gypsum (partitions)", 5:"Tiling",
 6:"Gypsum amendments &amp; making good", 7:"Painting", 8:"Second fix electrical",
 9:"Carpentry — ULF (+ Roderick cut-ins)", 10:"Fittings, sanitary &amp; outdoor",
 11:"Snagging &amp; handover"}
PH_CLS = {1:"p1",2:"p2",3:"p3",4:"p4",5:"p5",6:"p6",7:"p7",8:"p8",9:"p9",10:"p10",11:"p11"}

day_rows, last_ph = [], None
for i, ((ph, task, items), d) in enumerate(zip(DAYS, dates), 1):
    if ph != last_ph:
        day_rows.append(f'<tr class="ph-row {PH_CLS[ph]}"><td colspan="5">Phase {ph} &mdash; {PH_NAMES[ph]}</td></tr>')
        last_ph = ph
    wk = "" if i == 1 and d.weekday() != 0 else ""
    day_rows.append(
        f'<tr><td class="d-n">{i}</td><td class="d-d">{d.strftime("%a %d %b")}</td>'
        f'<td class="d-p">{ph}</td><td>{task}</td><td class="d-i">{items}</td></tr>')
day_table = "\n".join(day_rows)
END = dates[-1]

ph_counts = {}
for ph, _t, _i in DAYS:
    ph_counts[ph] = ph_counts.get(ph, 0) + 1
phase_rows = "\n".join(
    f'<tr><td class="d-p">{p}</td><td>{PH_NAMES[p]}</td><td class="s-n">{ph_counts[p]} d</td>'
    f'<td class="p-who">{"ULF + Roderick" if p == 9 else "Roderick + son"}</td></tr>'
    for p in sorted(ph_counts))

OQ = [("OQ-01","Outdoor wall finish &mdash; PVC sheet or tiles?","Front Balcony","Before day 63"),
      ("OQ-02","Outdoor kitchen &mdash; confirm design / equipment spec","Front Balcony","Before day 18"),
      ("OQ-03","New consumer unit &mdash; confirm location and capacity","Entrance","<b>Before day 6</b>"),
      ("OQ-04","Study / Office plugs &mdash; confirm quantity","Study / Office","Before day 14"),
      ("OQ-05","Ensuite tiles &mdash; confirm 60x30 dimensions","Ensuite","Before day 42"),
      ("OQ-06","Living / Dining TV wall &mdash; confirm PVC sheet spec","Open Plan","Before day 64"),
      ("OQ-07","Sofa-area TV unit &mdash; confirm electrical point spec","Open Plan","Before day 10"),
      ("OQ-08","Intercom &mdash; reuse or replace? Audio or video?","Entrance","<b>Before day 8</b>"),
      ("OQ-09","Who books and pays for the skips?","Whole House","Before day 5"),
      ("OQ-10","Tiling scope &mdash; site photos show the ensuite, main bathroom and front balcony are <b>already tiled</b>. Does the ~145 sq m still stand, and what exactly is in it?","Whole House","<b>Before pricing</b>"),
      ("OQ-11","Fireplace &mdash; the site photo shows it installed and the wall already opened beneath it. Is it being removed, or kept with the wall reworked around it?","Open Plan","<b>Before day 4</b>"),
      ("OQ-12","Two tile formats are on site: Fanal 120 and Fanal 31.6x90. What is the 31.6x90 for &mdash; it is not in the work list.","Whole House","Before day 29")]
oq_rows = "\n".join(
    f'''    <div class="oq"><span class="wi-id">{i}</span><span class="wi-t">{t}</span>'''
    f'''<span class="tag room">{r}</span><span class="tag status-tbc">{w}</span>'''
    f'''<span class="oq-dec"><span class="fl">Decision &amp; date</span><span class="nl"></span></span></div>'''
    for i, t, r, w in OQ)

log_rows = "".join('<tr><td></td><td></td><td></td><td></td><td></td><td></td></tr>' for _ in range(16))
TOTAL = sum(len(i) for _, _, _, i in ROOMS)

html = f'''<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zonqor Refurbishment — Site Checklist, Price Sheet &amp; Work Plan</title>
<style>
  @page{{size:A4 portrait;margin:10mm 9mm 11mm;}}
  :root{{--ink:#111827;--sub:#5b6472;--line:#d8d4cd;--hair:#9aa0a8;--accent:#8a4b09;--bg:#f6f5f2;}}
  *{{box-sizing:border-box;}} html{{-webkit-print-color-adjust:exact;print-color-adjust:exact;}}
  body{{margin:0;background:var(--bg);color:var(--ink);font-size:12.5px;line-height:1.35;
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;padding:26px 14px 60px;}}
  .sheet{{max-width:860px;margin:0 auto;background:#fff;border:1px solid var(--line);border-radius:6px;
    box-shadow:0 1px 3px rgba(0,0,0,.07);padding:24px 26px 30px;}}
  .bar{{max-width:860px;margin:0 auto 12px;display:flex;gap:10px;justify-content:space-between;align-items:center;flex-wrap:wrap;}}
  .bar-l{{display:flex;gap:13px;align-items:center;flex-wrap:wrap;font-size:12px;color:var(--sub);}}
  .btn{{border:1px solid var(--ink);background:var(--ink);color:#fff;padding:7px 14px;border-radius:6px;font-size:12.5px;font-weight:600;cursor:pointer;}}
  .btn-2{{background:#fff;color:var(--ink);}} .btn:hover{{opacity:.86;}}
  .tgl{{display:inline-flex;align-items:center;gap:5px;cursor:pointer;}}
  .runtot{{font-weight:700;}} .runtot span{{color:var(--accent);}}

  h1{{font-size:21px;margin:0 0 3px;letter-spacing:-.015em;}}
  .cover{{border-bottom:2px solid var(--ink);padding-bottom:11px;margin-bottom:13px;}}
  .cover .sub{{font-size:12.5px;color:var(--sub);}}
  .cover .rev{{margin-top:6px;font-size:11.5px;color:var(--sub);}} .cover .rev b{{color:var(--ink);}}
  .grid2{{display:grid;grid-template-columns:1fr 1fr;gap:12px;}}
  .box{{border:1px solid var(--line);border-radius:5px;padding:9px 11px;background:#fcfbf9;}}
  .box h3{{margin:0 0 5px;font-size:10px;text-transform:uppercase;letter-spacing:.07em;color:var(--accent);}}
  .box p{{margin:0 0 5px;font-size:11.5px;color:var(--sub);line-height:1.45;}} .box p:last-child{{margin:0;}}
  .box ol,.box ul{{margin:0;padding-left:15px;font-size:11.5px;color:var(--sub);line-height:1.5;}}
  .who{{display:flex;gap:7px;font-size:11.5px;margin-bottom:3px;}} .who b{{min-width:64px;}}
  .signrow{{display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;margin-top:12px;}}
  .sig{{font-size:11px;color:var(--sub);}} .sig .sl{{border-bottom:1px solid var(--hair);height:22px;margin-bottom:3px;}}
  .sec{{font-size:14.5px;margin:0 0 2px;}} .secsub{{font-size:11.5px;color:var(--sub);margin-bottom:9px;}}
  .warn{{border:1px solid #e0b48a;background:#fdf3e8;border-radius:5px;padding:9px 11px;margin-bottom:9px;}}
  .warn h3{{margin:0 0 4px;font-size:10px;text-transform:uppercase;letter-spacing:.07em;color:#9a4a06;}}
  .warn p{{margin:0 0 5px;font-size:11.5px;color:#6b4520;line-height:1.45;}} .warn p:last-child{{margin:0;}}

  .room{{margin-top:13px;}}
  .rhead{{display:flex;align-items:baseline;gap:7px;border-bottom:1.4px solid var(--ink);padding-bottom:3px;margin-bottom:5px;flex-wrap:wrap;}}
  .rhead h2{{font-size:12.5px;margin:0;text-transform:uppercase;letter-spacing:.05em;color:var(--accent);}}
  .rcode{{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;font-weight:700;background:#eceae6;color:#3f4652;padding:1px 5px;border-radius:3px;}}
  .rref{{font-size:10px;color:var(--sub);font-style:italic;}} .rref-no{{color:#a1620c;}}
  .rcount{{margin-left:auto;font-size:10.5px;color:var(--sub);}}
  .rthumbs{{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px;}}
  .tn{{margin:0;width:74px;cursor:pointer;}}
  .tn img{{width:74px;height:52px;object-fit:cover;border:1px solid var(--line);border-radius:3px;display:block;}}
  .tn figcaption{{font-size:8.5px;font-weight:700;color:var(--sub);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;text-align:center;margin-top:1px;}}
  .thumb-none{{font-size:10.5px;color:#a1620c;background:#fdf6e7;border:1px dashed #e0b48a;border-radius:4px;padding:5px 8px;width:100%;}}
  .banner{{font-size:11px;color:#7c4a03;background:#fdf6e7;border:1px solid #e8d9b4;border-radius:4px;padding:5px 8px;margin-bottom:5px;}}
  .tgrp{{font-size:9.5px;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:var(--sub);margin:8px 0 4px;border-bottom:1px solid var(--line);padding-bottom:2px;}}
  .tg-gypsum{{color:#6b21a8;}} .tg-elec{{color:#8a5a06;}} .tg-plumb{{color:#1d4ed8;}} .tg-tile{{color:#9f1239;}} .tg-fit{{color:#0369a1;}}

  .wi{{border:1px solid var(--line);border-radius:4px;padding:5px 8px 6px;margin-bottom:4px;}}
  .wi-h{{display:flex;gap:7px;align-items:baseline;}}
  .wi-id{{flex:0 0 auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:9.5px;font-weight:700;color:#3f4652;background:#eceae6;border-radius:3px;padding:1px 5px;}}
  .wi-t{{flex:1 1 auto;font-size:12.5px;font-weight:700;line-height:1.25;}}
  .phase-chip{{flex:0 0 auto;font-size:9px;font-weight:700;background:#eef2f7;color:#3d4c63;border:.5px solid #d2dae5;border-radius:3px;padding:1px 5px;white-space:nowrap;}}
  .wi-a{{flex:0 0 auto;font-size:9.5px;font-weight:700;padding:1px 7px;border-radius:999px;white-space:nowrap;}}
  .a-rod{{background:#dff3e2;color:#15603a;border:.5px solid #b6e0c1;}} .a-ulf{{background:#fdeada;color:#8a3f0d;border:.5px solid #f2d3ae;}}
  .wi-m{{display:flex;gap:5px;flex-wrap:wrap;align-items:baseline;margin-top:2px;}}
  .tag{{font-size:9.5px;padding:.5px 6px;border-radius:999px;background:#f4efe6;color:var(--accent);font-weight:600;white-space:nowrap;border:.5px solid #e5dcc9;}}
  .tag.trade-elec{{background:#fdf6d8;color:#7a4f05;border-color:#eddfa8;}}
  .tag.trade-plumb{{background:#e3ecfd;color:#1d4ed8;border-color:#c3d4f7;}}
  .tag.trade-gypsum{{background:#f4e9fd;color:#6b21a8;border-color:#e0cef7;}}
  .tag.trade-tile{{background:#fde5e9;color:#9f1239;border-color:#f6ccd4;}}
  .tag.trade-fit{{background:#e2f1fb;color:#0369a1;border-color:#c4e0f2;}}
  .tag.status-remove{{background:#fde4e4;color:#991b1b;border-color:#f5c7c7;}}
  .tag.status-known{{background:#fdeaea;color:#b91c1c;border-color:#f3c2c2;}}
  .tag.status-tbc{{background:#eef1f4;color:#44506a;border-color:#dbe1e8;}}
  .tag.status-new{{background:#e6f6ec;color:#15603a;border-color:#bfe3cb;}}
  .dep{{font-size:10px;color:#7b6f5f;font-style:italic;}}
  .i-note{{font-size:10.5px;color:var(--sub);flex:1 1 100%;line-height:1.3;}}
  .wi-f{{display:flex;align-items:center;gap:5px;flex-wrap:wrap;margin-top:5px;padding-top:4px;border-top:1px dashed var(--line);}}
  .fl{{font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;color:var(--sub);}}
  .fl2{{margin-left:9px;}} .fl-p{{margin-left:9px;color:var(--accent);}}
  .cb{{display:inline-flex;align-items:center;gap:3px;font-size:10.5px;cursor:pointer;}}
  .cb input{{width:12px;height:12px;margin:0;accent-color:var(--accent);cursor:pointer;}}
  .wl{{display:inline-block;width:60px;border-bottom:1px solid var(--hair);height:13px;}} .wl-s{{width:34px;}}
  .price{{width:70px;border:none;border-bottom:1px solid var(--hair);background:transparent;font:inherit;font-size:12px;font-weight:700;text-align:right;padding:0 2px 1px;}}
  .price:focus{{outline:none;border-bottom-color:var(--accent);background:#fdf8ef;}}
  .wi-c{{display:flex;align-items:baseline;gap:6px;margin-top:4px;}}
  .nl{{flex:1 1 auto;border-bottom:1px solid var(--line);height:13px;}}
  .wi.is-done{{background:#f6fbf6;border-color:#bfe0c6;}} .wi.is-done .wi-t{{color:#3f6b4a;}}
  .rtot{{display:flex;justify-content:flex-end;gap:8px;align-items:baseline;margin-top:5px;padding-top:4px;border-top:1.4px solid var(--ink);}}
  .rtv{{font-size:12.5px;font-weight:800;}}
  .rt{{display:inline-block;min-width:62px;border-bottom:1px solid var(--hair);text-align:right;padding:0 3px;}}
  .oq{{display:flex;gap:7px;align-items:baseline;flex-wrap:wrap;border:1px solid #e8d9b4;background:#fefaf1;border-radius:4px;padding:6px 9px;margin-bottom:4px;}}
  .oq .wi-t{{flex:0 1 auto;}}
  .oq-dec{{flex:1 1 100%;display:flex;align-items:baseline;gap:6px;margin-top:2px;}}

  .apgrid{{display:grid;grid-template-columns:repeat(3,1fr);gap:11px;}}
  .ap{{margin:0;cursor:pointer;}}
  .ap img{{width:100%;height:118px;object-fit:cover;border:1px solid var(--line);border-radius:4px;display:block;}}
  .ap figcaption{{font-size:9.5px;color:var(--sub);line-height:1.3;margin-top:3px;}}
  .ap figcaption b{{color:var(--ink);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;}}
  .ap-r{{color:#a1620c;}}
  .photo-slot{{border:1px dashed var(--hair);border-radius:4px;height:118px;display:flex;align-items:center;justify-content:center;font-size:9.5px;color:var(--hair);text-transform:uppercase;letter-spacing:.05em;text-align:center;padding:6px;}}

  table.sum,table.log,table.plan,table.day{{width:100%;border-collapse:collapse;font-size:11.5px;}}
  table th{{text-align:left;font-size:9px;text-transform:uppercase;letter-spacing:.06em;color:var(--sub);border-bottom:1.3px solid var(--ink);padding:4px 5px;font-weight:800;}}
  table td{{border-bottom:1px solid var(--line);padding:5px;vertical-align:top;}}
  table.log td,table.log th{{border-right:1px solid var(--line);}}
  table.log td:last-child,table.log th:last-child{{border-right:none;}} table.log td{{height:26px;}}
  .s-code{{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-weight:700;font-size:10px;width:42px;}}
  .s-n{{width:46px;color:var(--sub);}} .s-val{{width:112px;text-align:right;font-weight:700;white-space:nowrap;}}
  .s-sign{{width:78px;}}
  table.sum tfoot td{{border-top:1.8px solid var(--ink);border-bottom:none;padding-top:7px;font-weight:800;font-size:12.5px;}}
  .lg-d{{width:58px;}} .lg-w{{width:68px;}} .lg-i{{width:104px;}} .lg-a{{width:66px;}} .lg-s{{width:48px;}}
  .d-n{{width:30px;font-weight:700;color:var(--sub);font-size:10.5px;}}
  .d-d{{width:74px;font-weight:700;white-space:nowrap;font-size:11px;}}
  .d-p{{width:26px;color:var(--sub);font-size:10.5px;}}
  .d-i{{width:132px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:9.5px;color:var(--sub);}}
  .p-who{{width:110px;color:var(--sub);}}
  tr.ph-row td{{background:#eef2f7;font-weight:800;font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:#2f3b4d;padding:4px 5px;border-bottom:1px solid var(--line);}}

  .lb{{display:none;position:fixed;inset:0;background:rgba(0,0,0,.88);z-index:99;align-items:center;justify-content:center;flex-direction:column;padding:22px;}}
  .lb.on{{display:flex;}} .lb img{{max-width:100%;max-height:84vh;border-radius:5px;}}
  .lb .cap{{color:#fff;font-size:12.5px;margin-top:9px;text-align:center;}}
  .lb .x{{position:absolute;top:14px;right:20px;background:none;border:none;color:#fff;font-size:28px;cursor:pointer;}}
  @media(max-width:700px){{.grid2,.signrow{{grid-template-columns:1fr;}}.apgrid{{grid-template-columns:repeat(2,1fr);}}.sheet{{padding:16px 13px;}}}}

  @media print{{
    body{{background:#fff;padding:0;font-size:8.7pt;line-height:1.28;}}
    .sheet{{max-width:none;border:none;border-radius:0;box-shadow:none;padding:0;}}
    .bar,.lb,.noprint{{display:none!important;}}
    h1{{font-size:16pt;}} .sec{{font-size:12pt;}}
    .newpage{{break-before:page;}}
    .wi,.oq,.ap,.box,.warn,.rtot,.rhead,tr{{break-inside:avoid;}}
    .rhead,.tgrp,.sec,.secsub,.rthumbs{{break-after:avoid;}}
    thead{{display:table-header-group;}}
    .wi{{padding:3.6pt 5pt 4pt;margin-bottom:2.6pt;border-color:#bbb6ae;}}
    .wi-t{{font-size:9.4pt;}} .tag{{font-size:6.8pt;}} .dep{{font-size:7.2pt;}} .i-note{{font-size:7.6pt;}}
    .wi-a,.phase-chip{{font-size:6.8pt;}} .fl{{font-size:6.6pt;}} .cb{{font-size:7.6pt;}}
    .cb input{{width:8.6pt;height:8.6pt;}}
    .nl{{height:6.4mm;border-bottom-color:#9d988f;}}
    .wl{{height:3.6mm;border-bottom-color:#8b867e;width:17mm;}} .wl-s{{width:10mm;}}
    .price{{border-bottom-color:#8b867e;width:20mm;height:3.6mm;}}
    .rt{{border-bottom-color:#8b867e;min-width:22mm;}}
    .room{{margin-top:9pt;}}
    .tn{{width:19mm;}} .tn img{{width:19mm;height:13mm;}} .tn figcaption{{font-size:6pt;}}
    .ap img{{height:30mm;}} .ap figcaption{{font-size:6.6pt;}} .photo-slot{{height:30mm;font-size:6.6pt;}}
    .apgrid{{gap:5mm 4mm;}}
    table.log td{{height:10.5mm;}}
    table.day td{{padding:2.6pt 4pt;}}
    body.no-app .appendix{{display:none;}}
  }}
</style></head><body>

<div class="bar noprint">
  <div class="bar-l">
    <span class="runtot">Agreed so far: &euro;<span id="grand">0</span></span>
    <span id="donecount"></span>
    <label class="tgl"><input type="checkbox" id="tApp" checked onchange="tog()"> Print picture appendix</label>
  </div>
  <div><button class="btn btn-2" onclick="clearAll()">Clear entries</button>
  <button class="btn" onclick="window.print()">Print / Save as PDF</button></div>
</div>

<div class="sheet">
  <div class="cover">
    <h1>Zonqor Apartment &mdash; Site Checklist, Prices &amp; Work Plan</h1>
    <div class="sub">Turnkey refurbishment &mdash; Roderick and son, with ULF on carpentry</div>
    <div class="rev"><b>Revision:</b> <span id="revdate"></span> &middot; <b>{TOTAL} items</b> across {len(ROOMS)} areas &middot;
      <b>Start {START.strftime('%a %d %b %Y')}</b>, {len(DAYS)} working days (Mon&ndash;Fri) to <b>{END.strftime('%a %d %b %Y')}</b> &middot;
      Rooms follow the walk-through order &middot; Pictures in the <b>Appendix</b>, referenced A01&hellip;</div>
  </div>

  <div class="grid2">
    <div class="box"><h3>Who does what</h3>
      <div class="who"><b>Roderick</b><span>Gypsum &amp; structural, electrical, plumbing &amp; gas, tiling &mdash; with his son, 2 on site</span></div>
      <div class="who"><b>ULF</b><span>Carpentry, wardrobes, fittings, mirrors, accessories</span></div>
      <div class="who"><b>Christine</b><span>Daily walk-through, sign-off, price agreement</span></div>
      <div class="who"><b>Kurt</b><span>Open questions, specs, ordering decisions</span></div>
    </div>
    <div class="box"><h3>How to use this sheet</h3>
      <ol><li>Find the item by its <b>ID code</b> (e.g. MBR-05). Pictures are the small <b>A</b> numbers &mdash; full size in the appendix.</li>
      <li>Tick <b>Done</b>, <b>Part</b> or <b>Issue</b>; write the date.</li>
      <li>Agree the price, write it in <b>Price &euro;</b>, initial the row.</li>
      <li><b>Ph</b> on each item is its phase in the work plan.</li>
      <li>Carry room totals to the <b>Price Summary</b> at the back.</li></ol>
    </div>
  </div>

  <div class="box" style="margin-top:10px;"><h3>Before signing off an item</h3>
    <p>An item is only <b>Done</b> when the work is finished <em>and</em> the price is agreed and written down. If the price
    is still being discussed, tick <b>Part</b> and say why &mdash; never leave the price blank on a completed item. Anything
    marked <b>Issue</b> goes to Kurt the same evening. An item whose &ldquo;after:&rdquo; line names an <b>OQ</b> number
    cannot start until that question is decided.</p></div>

  <div class="signrow">
    <div class="sig"><div class="sl"></div>Christine &mdash; signature &amp; date</div>
    <div class="sig"><div class="sl"></div>Roderick &mdash; signature &amp; date</div>
    <div class="sig"><div class="sl"></div>ULF &mdash; signature &amp; date</div>
  </div>

  <!-- ================= WORK PLAN ================= -->
  <section class="newpage" style="margin-top:16px;">
    <h2 class="sec">Work Plan &mdash; phases</h2>
    <div class="secsub">Built on the order Roderick proposed, with the missing stages filled in and one sequencing change (below).</div>

    <div class="warn"><h3>&#9888; One change to the proposed order &mdash; gypsum before tiling, not after</h3>
      <p>Roderick's order puts <b>tiling before gypsum</b>. That works for patching and making good, but not for the
      <b>study partition (OFF-01)</b>, the <b>small corridor extension (SCR-01)</b> or the <b>sofa wall (OPN-02)</b> &mdash;
      those are full walls. Tiling under a wall that is then built on top wastes 120x120 tile, and the wall sits on a
      tiled surface instead of the slab.</p>
      <p>So the plan splits it: <b>Phase 4</b> builds the partitions <em>before</em> tiling, and <b>Phase 6</b> keeps the
      patching and making good <em>after</em>. Worth confirming with Roderick on day one &mdash; he may have a reason,
      but if not, this saves tile and gives a better junction.</p></div>

    <div class="warn"><h3>&#9888; What was missing from the proposed order</h3>
      <p>The proposed sequence stopped at gypsum. Six stages were not listed and are now in the plan:
      <b>painting</b> (Ph 7), <b>second fix electrical</b> (Ph 8 &mdash; fitting and terminating every point, hanging
      fittings, then testing and certification), <b>carpentry</b> (Ph 9 &mdash; ULF, with Roderick returning to cut in the
      gyser/pump and ensuite switches), <b>fittings and sanitary</b> (Ph 10), <b>the outdoor/balcony works</b> (Ph 10) and
      <b>snagging and handover</b> (Ph 11).</p>
      <p>Three items were also missing from the work list itself and have been added:
      <b>WHS-01</b> strip out the old parquet, <b>WHS-04</b> level the screed once it is up (120x120 is unforgiving on an
      uneven floor), <b>WHS-02</b> skip hire, and <b>ENS-03</b> tanking the ensuite wet area before it is tiled.</p></div>

    <table class="plan">
      <thead><tr><th>Ph</th><th>Phase</th><th>Days</th><th>Crew</th></tr></thead>
      <tbody>
{phase_rows}
      </tbody>
      <tfoot><tr><td></td><td><b>Total &mdash; {START.strftime('%a %d %b')} to {END.strftime('%a %d %b %Y')}</b></td>
        <td class="s-n"><b>{len(DAYS)} d</b></td><td></td></tr></tfoot>
    </table>

    <div class="box" style="margin-top:10px;"><h3>Assumptions behind these durations</h3>
      <p>Two people on site, Monday to Friday, no public holidays deducted. Roderick's crew does not do painting on some
      jobs &mdash; if painting is not in his price, Phase 7 is a separate trade and the dates after it shift.
      ULF's carpentry (Ph 9) is shown sequentially but can start earlier if the rooms are tiled and painted; that
      would pull the finish date in by roughly a week. Balcony tiling assumes dry weather.
      Nothing here is committed until Roderick agrees it &mdash; walk the plan with him on day one.</p></div>
  </section>

  <section class="newpage" style="margin-top:16px;">
    <h2 class="sec">Day-by-Day Plan</h2>
    <div class="secsub">{START.strftime('%a %d %b %Y')} &rarr; {END.strftime('%a %d %b %Y')} &middot; {len(DAYS)} working days, Mon&ndash;Fri &middot; Roderick + son unless noted.</div>
    <table class="day">
      <thead><tr><th>Day</th><th>Date</th><th>Ph</th><th>Work</th><th>Items</th></tr></thead>
      <tbody>
{day_table}
      </tbody>
    </table>
  </section>

  <section class="newpage" style="margin-top:16px;">
    <h2 class="sec">Open Questions &mdash; decide before ordering</h2>
    <div class="secsub">These block the items that name them. The deadline column is the day in the plan by which the answer is needed.</div>
{oq_rows}
  </section>

{rooms_html}

  <section class="newpage" style="margin-top:16px;">
    <h2 class="sec">Price Summary</h2>
    <div class="secsub">Carry each room total across. Agreed totals only &mdash; anything still under discussion stays out until settled.</div>
    <table class="sum">
      <thead><tr><th>Code</th><th>Area</th><th>Items</th><th style="text-align:right;">Total agreed</th><th>Signed</th></tr></thead>
      <tbody>
{summary_rows}
      </tbody>
      <tfoot><tr><td></td><td>Grand total agreed</td><td></td><td class="s-val">&euro; <span class="rt" id="st-GRAND"></span></td><td></td></tr></tfoot>
    </table>
    <div class="signrow" style="margin-top:16px;">
      <div class="sig"><div class="sl"></div>Christine &mdash; signature &amp; date</div>
      <div class="sig"><div class="sl"></div>Roderick &mdash; signature &amp; date</div>
      <div class="sig"><div class="sl"></div>ULF &mdash; signature &amp; date</div>
    </div>
  </section>

  <section style="margin-top:16px;">
    <h2 class="sec">Daily Site Log</h2>
    <div class="secsub">One line per visit &mdash; deliveries, delays, extra labour, verbal agreements, anything not tied to a single item.</div>
    <table class="log">
      <thead><tr><th class="lg-d">Date</th><th class="lg-w">On site</th><th class="lg-i">Items (IDs)</th>
        <th class="lg-a">Agreed &euro;</th><th class="lg-n">Notes / issues raised</th><th class="lg-s">Init.</th></tr></thead>
      <tbody>{log_rows}</tbody>
    </table>
  </section>

  <section class="newpage appendix" style="margin-top:16px;">
    <h2 class="sec">Appendix &mdash; Reference Pictures</h2>
    <div class="secsub">Full-size versions of the thumbnails shown in each room. The code in brackets is the room(s) each picture serves.</div>
    <div class="apgrid">
{ap}
{ph_html}
    </div>
  </section>
</div>

<div class="lb" id="lb" onclick="if(event.target===this)lbx()">
  <button class="x" onclick="lbx()">&times;</button><img id="lbimg" src="" alt=""><div class="cap" id="lbcap"></div>
</div>

<script>
  document.getElementById('revdate').textContent=new Date().toLocaleDateString('en-GB',{{year:'numeric',month:'long',day:'numeric'}});
  function money(n){{return n.toLocaleString('en-MT',{{minimumFractionDigits:2,maximumFractionDigits:2}});}}
  function recalc(){{
    var byRoom={{}},grand=0;
    document.querySelectorAll('.price').forEach(function(i){{
      var v=parseFloat(String(i.value).replace(/[^0-9.\\-]/g,'')); if(isNaN(v))return;
      var r=i.getAttribute('data-room'); byRoom[r]=(byRoom[r]||0)+v; grand+=v;}});
    document.querySelectorAll('.rt').forEach(function(el){{
      if(el.id==='st-GRAND'){{el.textContent=grand?money(grand):'';return;}}
      var c=el.id.replace(/^(rt|st)-/,''); el.textContent=byRoom[c]?money(byRoom[c]):'';}});
    document.getElementById('grand').textContent=money(grand);
  }}
  function dc(){{var a=document.querySelectorAll('.st-done').length,d=document.querySelectorAll('.st-done:checked').length;
    document.getElementById('donecount').textContent=d+' / '+a+' items done';}}
  function onDone(cb){{cb.closest('.wi').classList.toggle('is-done',cb.checked);dc();}}
  function tog(){{document.body.classList.toggle('no-app',!document.getElementById('tApp').checked);}}
  function clearAll(){{
    document.querySelectorAll('.price').forEach(function(i){{i.value='';}});
    document.querySelectorAll('input[type=checkbox]').forEach(function(c){{
      if(c.id==='tApp')return; c.checked=false;
      if(c.classList.contains('st-done'))c.closest('.wi').classList.remove('is-done');}});
    recalc();dc();}}
  function lb(f){{var i=f.querySelector('img'); if(!i)return;
    document.getElementById('lbimg').src=i.src;
    document.getElementById('lbcap').textContent=i.getAttribute('data-cap')||'';
    document.getElementById('lb').classList.add('on');}}
  function lbx(){{document.getElementById('lb').classList.remove('on');}}
  document.addEventListener('keydown',function(e){{if(e.key==='Escape')lbx();}});
  recalc();dc();
</script></body></html>
'''

with open(os.path.join(os.path.dirname(os.path.abspath(__file__)),'..','output','renovation_checklist.html'), 'w') as f:
    f.write(html)
print("bytes:", len(html))
print("items:", TOTAL, "| appendix:", len(img_order), "+", len(placeholder), "slots")
print("plan:", len(DAYS), "working days,", START, "->", END)

gen_day_plan.py

build/gen_day_plan.py

"""Regenerate docs/04-day-by-day.md from the built checklist.

The day plan lives in build_v3.py (START + DAYS). This mirrors the *rendered* table into
markdown so the doc can never drift from the document Christine prints. Run it after any
change to START or DAYS:

    python3 build_v3.py && python3 gen_day_plan.py
"""
import os, re, sys
from bs4 import BeautifulSoup

HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "..", "output", "renovation_checklist.html")
OUT = os.path.join(HERE, "..", "docs", "04-day-by-day.md")

soup = BeautifulSoup(open(SRC).read(), "html.parser")


def md(cell):
    """Cell text as markdown — <b> becomes **bold**, everything else flattened."""
    for b in cell.find_all("b"):
        b.replace_with(f"**{b.get_text(' ', strip=True)}**")
    return re.sub(r"\s+", " ", cell.get_text(" ", strip=True)).replace(" **", " **")


table = soup.select_one("table.day")
rows = table.select("tr")
days = [r for r in rows if r.select_one(".d-n")]
first, last = days[0].select_one(".d-d").get_text(strip=True), days[-1].select_one(".d-d").get_text(strip=True)
# The day plan's own subtitle — not the first .secsub on the page, which belongs to the Work Plan.
span = table.find_previous(class_="secsub").get_text(" ", strip=True).split("·")[0].strip()

out = ["# Day-by-day plan", "",
       f"**{span} · {len(days)} working days, Mon–Fri.**",
       "Roderick + son unless noted. Generated by `build/gen_day_plan.py` from the built",
       "checklist — regenerate rather than hand-edit.", ""]

for r in rows:
    head = r.find("td", colspan="5")
    if head:
        out += ["", f"### {head.get_text(' ', strip=True)}", "",
                "| Day | Date | Work | Items |", "|---|---|---|---|"]
    elif r.select_one(".d-n"):
        n = r.select_one(".d-n").get_text(strip=True)
        d = r.select_one(".d-d").get_text(strip=True)
        work = md(r.select_one(".d-d").find_next_sibling("td").find_next_sibling("td"))
        items = r.select_one(".d-i").get_text(" ", strip=True)
        out.append(f"| {n} | {d} | {work} | `{items}` |")

open(OUT, "w").write("\n".join(out) + "\n")
print(f"wrote {OUT}: {len(days)} days, {first} -> {last}")

pdfcheck.py

build/pdfcheck.py

"""Render the checklist HTML to A4 PDF via Playwright/Chromium.
Print is the primary medium — always eyeball the rendered pages, don't assume the CSS worked."""
import os
from playwright.sync_api import sync_playwright
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "..", "output", "renovation_checklist.html")
OUT = os.path.join(HERE, "..", "output", "Zonqor Site Checklist.pdf")

# The Cowork sandbox shipped chromium at a fixed path; on a normal machine let
# Playwright resolve its own install (~/Library/Caches/ms-playwright on macOS).
SANDBOX_CHROMIUM = "/opt/pw-browsers/chromium"
launch_kw = {"executable_path": SANDBOX_CHROMIUM} if os.path.exists(SANDBOX_CHROMIUM) else {}

with sync_playwright() as p:
    b = p.chromium.launch(**launch_kw)
    pg = b.new_page()
    pg.goto("file://" + os.path.abspath(SRC))
    pg.wait_for_timeout(900)
    pg.pdf(path=OUT, format="A4", print_background=True,
           margin={"top": "11mm", "bottom": "12mm", "left": "10mm", "right": "10mm"})
    b.close()
print("wrote", OUT)

pdfplan.py

build/pdfplan.py

"""Render the base plan HTML to A4 PDF. Same launcher logic as pdfcheck.py."""
import os
from playwright.sync_api import sync_playwright

HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "..", "output", "base_plan.html")
OUT = os.path.join(HERE, "..", "output", "Zonqor Base Plan.pdf")

SANDBOX_CHROMIUM = "/opt/pw-browsers/chromium"
launch_kw = {"executable_path": SANDBOX_CHROMIUM} if os.path.exists(SANDBOX_CHROMIUM) else {}

with sync_playwright() as p:
    b = p.chromium.launch(**launch_kw)
    pg = b.new_page()
    pg.goto("file://" + os.path.abspath(SRC))
    pg.wait_for_timeout(600)
    pg.pdf(path=OUT, format="A4", print_background=True,
           margin={"top": "11mm", "bottom": "12mm", "left": "10mm", "right": "10mm"})
    b.close()
print("wrote", os.path.normpath(OUT))

plan_data.py

build/plan_data.py

# Zonqor — reconstructed base plan geometry.
#
# SOURCE: images/measurements/study_and_bedroom_revised_p1.jpg, cross-checked against
# images/measurements/kids_bedroom_only_p1.jpg (which carries a printed 1:100 scale bar).
#
# CALIBRATION — two independent routes, agreeing to 0.08%:
#   1. kids_bedroom_only_p1 scale bar: 10 m spans 599.0 px, 1 m ticks exactly 60.0 px,
#      0.1 m sub-ticks exactly 6.0 px  ->  59.90 px/m
#   2. study_and_bedroom_revised_p1 is the same drawing plotted 1.3137x larger
#      ->  78.68 px/m, which at 200 dpi is 9.99 mm per metre on paper = exactly 1:100
#
# ORIGIN: north-west outer corner of the building. x runs east, y runs south.
# All values in METRES, read off the drawing to the nearest ~0.02 m (1 px ~ 13 mm).
#
# ⚠ THIS IS A RECONSTRUCTION, NOT A SURVEY. Nobody has measured the flat. The drawing
# it is traced from is a designer's furniture-layout plot that itself carries the note
# "All dimensions to be verified on site prior to fabrication and installation."
# Confidence is recorded per room below. Do not order materials off these numbers.

SCALE_NOTE = "1:100, calibrated on a printed scale bar; ±3% overall, worse per room where noted"

# ---------------------------------------------------------------- building envelope
# Outer face of the external wall, clockwise from the NW corner.
# The west side steps in twice — two light wells that serve the bathroom, study,
# ensuite and entrance (photos confirm windows in exactly those rooms).
ENVELOPE = [
    (0.00,  0.00), (7.21,  0.00),          # north end
    (7.21, 26.00),                          # east wall, full 26 m run
    (0.00, 26.00),                          # south wall (front balcony beyond)
    (0.00, 19.59), (2.43, 19.59),           # -- light well 2 (entrance / ensuite)
    (2.43, 13.50), (0.00, 13.50),
    (0.00,  8.60), (2.19,  8.60),           # -- light well 1 (bathroom / study)
    (2.19,  4.60), (0.00,  4.60),
]
WALL_T   = 0.22        # external wall thickness, measured consistently on all four sides
CEILING  = 2.61        # dining area: 830 + 1050 + 730 mm off openplan_dimensions_1.jpg

# Front balcony: curved parapet (confirmed by balcony_plan_p1.jpg, which shows the BBQ
# and outdoor table set against a swept edge). Traced as an arc, not a measured curve.
BALCONY_ARC = dict(start=(6.68, 26.26), end=(1.90, 28.78), west_edge=(0.00, 28.78))

# ---------------------------------------------------------------- rooms
# code, name, x0, y0, x1, y1 (inner faces), confidence, note
# confidence: A = read off two sheets consistently, B = single reading, C = inferred
ROOMS = [
 ("BBL", "Back Balcony",        0.22,  0.19, 6.99,  1.04, "C",
  "Full-width strip, only 0.85 m deep. Read as a balcony because tiles_sq_m.jpg excludes "
  "it from the shaded 125 m². Could be a storage recess — walk it with Roderick."),
 ("CYB", "Bedroom — north-west", 0.22,  1.04, 3.05,  4.38, "B",
  "Which of the two north rooms is Cy's and which is Rafe's is NOT established. "
  "CYB is 'with window', RAF is 'with back balcony door' — resolve on site."),
 ("RAF", "Bedroom — north-east", 3.72,  1.04, 6.99,  4.38, "B",
  "See note on the north-west bedroom. The 0.46 m strip between the two rooms is a "
  "wardrobe/partition block, not habitable floor."),
 ("SCR", "Small Corridor",      2.44,  4.60, 5.48,  6.06, "C",
  "LOWEST CONFIDENCE ROOM. A lobby between the kids' rooms and the bathroom. The "
  "checklist's room order implies the small corridor sits nearer the main bedroom, "
  "which does not match the geometry. Confirm what SCR-01 actually refers to."),
 ("BTH", "Main Bathroom",       2.44,  6.28, 5.26,  8.86, "A",
  "The plain unfurnished rectangle on every sheet. West wall is external onto light "
  "well 1 — matches the window in IMG_9873. Already finished in grey marble."),
 ("MCR", "Main Corridor",       5.48,  4.60, 6.86, 15.26, "A",
  "Runs the full length of the east side, entrance to kids' rooms. Unobstructed on "
  "the drawing for 10.66 m."),
 ("MBR", "Main Bedroom",        2.59,  9.07, 5.26, 13.28, "A",
  "Study partition (OFF-01) forms its west wall at x=2.59."),
 ("OFF", "Study / Office",      0.22,  9.07, 2.59, 13.50, "B",
  "Carved off the west side of the main bedroom by the new sound-proof gypsum "
  "partition. Window onto light well 1."),
 ("ENS", "Ensuite",             3.19, 13.50, 5.26, 15.05, "B",
  "Entered from the main bedroom; the 'wardrobe around ensuite door' callout sits "
  "exactly here. Window onto light well 2 — matches IMG_9889. Already tiled."),
 ("ENT", "Entrance",            2.43, 15.26, 6.99, 17.29, "C",
  "A threshold zone at the north end of the open plan rather than a room. Front door "
  "and the existing consumer unit (IMG_9845) are on this stretch."),
 ("OPN", "Open Plan — living",  2.43, 17.29, 6.99, 19.59, "B",
  "Sofa/TV end. Narrower because light well 2 cuts in on the west."),
 ("OP2", "Open Plan — dining & kitchen", 0.25, 19.81, 6.99, 26.00, "A",
  "Full width. Fitted joinery runs along the east wall (0.38 m deep: TV unit, "
  "shelving, enclosed storage)."),
]

# Rooms that photographs show are ALREADY tiled and finished, so are not part of any
# new 120x120 tiling area. See docs/07-site-observations.md.
ALREADY_FINISHED = {"BTH", "ENS", "FBL"}

# ---------------------------------------------------------------- what must be measured
TO_MEASURE = [
 ("Every room", "One diagonal per room", "Confirms the rooms are actually square. Nothing here is squared off."),
 ("BBL",  "Depth and whether it is a balcony at all", "0.85 m is shallow; the room's purpose is unconfirmed."),
 ("CYB / RAF", "Which room is which", "Needed before any item priced per room can be signed off."),
 ("SCR",  "Where the small corridor actually is", "Geometry and the checklist's room order disagree."),
 ("ENS",  "Internal size and whether it is being re-tiled", "Drives OQ-05 and OQ-10."),
 ("FBL",  "The curved parapet — chord and depth", "Traced as an arc; a curve cannot be scaled reliably."),
 ("All",  "Floor-to-ceiling height in each room", "Only the dining area is known (2.61 m)."),
 ("All",  "Door and window positions and widths", "Openings were not traced; they are needed for the electrical plan."),
]

rooms_data2.py

build/rooms_data2.py

# Zonqor refurbishment — rooms in Kurt's walk-through order (Aug 2026 revision).
# Order: Whole House (cross-room) then Front Balcony -> Open Plan -> Entrance ->
# Main Corridor -> Small Corridor -> Main Bedroom -> Study/Office -> Ensuite ->
# Main Bathroom -> Cy -> Rafe -> Back Balcony.

def item(title, group, tags="", notes="", dep_override=None, phase=None):
    return {"title": title, "group": group, "tags": tags, "notes": notes,
            "dep_override": dep_override, "phase": phase}

T_ELEC = '<span class="tag trade-elec">Electrical</span>'
T_GYP = '<span class="tag trade-gypsum">Gypsum</span>'
T_PLU = '<span class="tag trade-plumb">Plumbing</span>'
T_TIL = '<span class="tag trade-tile">Tiling</span>'
T_FIT = '<span class="tag trade-fit">Fittings</span>'
NEW = '<span class="tag status-new">NEW</span>'
KNOWN = '<span class="tag status-known">Known issue</span>'
REMOVE = '<span class="tag status-remove">Remove existing</span>'
TBC = '<span class="tag status-tbc">&#9888; To confirm</span>'

def zone(z):
    return f'<span class="tag room">{z}</span>'

ROOMS = []

# ------------------------------------------------------------------ WHOLE HOUSE
ROOMS.append(("Whole House", "WHS",
  "Cross-room work that is not tied to a single room. Everything here is priced once, for the whole flat.", [
    item("Strip out old parquet flooring throughout", "gypsum",
         T_GYP + REMOVE + NEW,
         "Lift and remove the existing parquet across the flat. First job on site — everything else waits on the floor being clear.",
         dep_override="start", phase=1),
    item("Check and level floor screed after parquet removal", "tile",
         T_TIL + NEW,
         "Once the parquet is up, check the screed for level and damage. Patch/level before tiling — 120x120 tiles are unforgiving on an uneven floor.",
         dep_override="WHS-01 — parquet stripped", phase=4),
    item("Skip hire and waste removal", "gypsum",
         T_GYP + NEW,
         "At least two skips: one after strip-out/demolition, one after first fix and chasing. Confirm who books and pays for the skip.",
         dep_override="WHS-01 — parquet stripped", phase=3),
    item("Fix electricity problems and points", "elec", T_ELEC,
         "Diagnose and repair existing faults; check/replace faulty sockets and switches; add points as needed throughout the flat.",
         dep_override="ENT-01 — new consumer unit", phase=2),
    item("Lay 120x120 tiles — full house, including balconies (~145 sq m)", "tile", T_TIL,
         "120x120 throughout the flat, ~145 sq m per Roderick's quote. &#9888; Site photos show the ensuite, main bathroom and front balcony are ALREADY TILED — re-measure the actual parquet area before this price is agreed (OQ-10).",
         dep_override="WHS-04 — floor levelled; first-fix complete", phase=4),
]))

# --------------------------------------------------------------- FRONT BALCONY
ROOMS.append(("Front Balcony", "FBL",
  "&#9888; Outdoor kitchen design/equipment still to be specified (OQ-02) before ordering or starting those items.", [
    item("2 side lights — BBQ side", "elec", T_ELEC + ' <span class="tag trade-elec">2 light points</span>', phase=2),
    item("1 electrical/USB point — BBQ side (for camera)", "elec", T_ELEC + ' <span class="tag trade-elec">1 point</span>', phase=2),
    item("Central floor lights (under railing) — mini lights or LED strip", "elec", T_ELEC + ' <span class="tag trade-elec">1 light point</span>', phase=2),
    item("Switch for balcony side lights", "elec", T_ELEC + ' <span class="tag trade-elec">1 switch</span>',
         "At the current kitchen/balcony switch position.", phase=2),
    item("Switch for balcony floor lights", "elec", T_ELEC + ' <span class="tag trade-elec">1 switch</span>',
         "At the current kitchen/balcony switch position.", phase=2),
    item("Rope lighting — front (outdoor)", "elec", T_ELEC,
         dep_override="OQ-02 — outdoor kitchen spec", phase=2),
    item("2 outdoor wall lights", "elec", T_ELEC + ' <span class="tag trade-elec">2 points</span>',
         dep_override="OQ-02 — outdoor kitchen spec", phase=2),
    item("2 waterproof plugs + LED kit for outdoor kitchen units", "elec", T_ELEC,
         dep_override="OQ-02 — outdoor kitchen spec", phase=2),
    item("Run gas line for outdoor kitchen", "plumb",
         '<span class="tag trade-plumb">Gas/Plumbing</span>',
         "Gas run must be pressure-tested and certified. Confirm bottle vs. mains supply and where the bottle housing sits.",
         dep_override="OQ-02 — outdoor kitchen spec", phase=2),
    item("Outdoor wall finish — PVC sheet OR tiles", "fit",
         '<span class="tag trade-fit">Fittings/Tiling</span>' + TBC,
         "One or the other, not both — still to be decided. See OQ-01.",
         dep_override="OQ-01 — PVC vs. tiles; elec &amp; gas rough-in (this room)", phase=9),
]))

# ------------------------------------------------------------------- OPEN PLAN
ROOMS.append(("Open Plan — Kitchen &rarr; Dining &rarr; Living", "OPN",
  "One continuous space. Zone tags below show which end of the room each item sits in.", [
    item("Dining Feature Wall + Fireplace Removal", "gypsum",
         zone("Dining") + T_GYP + REMOVE,
         "Remove the existing gypsum feature wall/partition and the fireplace to open the living/dining space. Confirm the final opening size before starting. Check for services in the wall before breaking.",
         dep_override="start", phase=1),
    item("Add gypsum behind sofa", "gypsum", zone("Living") + T_GYP,
         "250mm wide x 1m high.",
         dep_override="OPN-01 — feature wall removed", phase=5),
    item("3 ceiling lights", "elec", zone("Kitchen") + T_ELEC + ' <span class="tag trade-elec">3 light points</span>',
         "Reuses an existing electrical point.", phase=2),
    item("Hood electrical point", "elec", zone("Kitchen") + T_ELEC + ' <span class="tag trade-elec">1 point</span>',
         "Reuses an existing electrical point.", phase=2),
    item("Fix oven electrical point — currently trips the salvavita (RCD)", "elec",
         zone("Kitchen") + T_ELEC + ' <span class="tag trade-elec">2 points</span>' + KNOWN,
         "Oven trips the salvavita when switched on. Check wiring, the connection point, and whether the oven needs its own dedicated circuit off the new unit.",
         dep_override="ENT-01 — new consumer unit", phase=2),
    item("Light points for dining cabinets", "elec", zone("Dining") + T_ELEC + ' <span class="tag trade-elec">1 light point</span>', phase=2),
    item("Rope lighting within the new gypsum wall (behind sofa)", "elec",
         zone("Living") + T_ELEC + ' <span class="tag trade-elec">1 light point</span>',
         dep_override="OPN-02 — gypsum behind sofa", phase=2),
    item("Switch for sofa rope lighting", "elec", zone("Living") + T_ELEC + ' <span class="tag trade-elec">1 switch</span>', phase=2),
    item("2 plugs near sofa", "elec", zone("Living") + T_ELEC + ' <span class="tag trade-elec">2 points</span>', phase=2),
    item("TV unit electrical point", "elec", zone("Living") + T_ELEC + TBC,
         dep_override="OQ-07 — TV unit electrical spec", phase=2),
    item("Ethernet point behind sofa", "elec", zone("Living") + T_ELEC,
         "Run back to the central unit at the entrance — pull with the first-fix cabling, not after tiling.", phase=2),
    item("Relocate double-pole switch when open-plan wall is removed", "elec", zone("Dining") + T_ELEC,
         dep_override="OPN-01 — feature wall removed", phase=2),
    item("Re-plumb kitchen sink onto main water feed (remove tank feed)", "plumb",
         zone("Kitchen") + T_PLU + '<span class="tag">Prep for RO</span>',
         "Switch the sink supply from the tank line to the main feed, and leave it ready for a future reverse-osmosis filter.",
         dep_override="start", phase=2),
    item("Install PVC sheet on wall with TV", "fit", zone("Living") + T_FIT + TBC,
         dep_override="OQ-06 — PVC sheet spec; tiling complete", phase=9),
]))

# -------------------------------------------------------------------- ENTRANCE
ROOMS.append(("Entrance", "ENT",
  "Home of the new electrical central unit — the first-fix hub for the whole flat. Everything electrical downstream waits on ENT-01.", [
    item("Install new consumer unit / RCD (salvavita) panel", "elec",
         T_ELEC + '<span class="tag status-known">Major item</span>',
         "Full refresh of the electrical panel. Should also resolve the kitchen oven tripping the salvavita. Confirm final position and capacity (OQ-03) before ordering. Needs certification on completion.",
         dep_override="OQ-03 — panel location and capacity", phase=2),
    item("Intercom — install / replace unit and wiring", "elec", T_ELEC + NEW,
         "Confirm whether the existing intercom is reused or replaced, and whether it is audio-only or video — this changes the cable pulled at first fix.",
         dep_override="ENT-01 — new consumer unit", phase=2),
]))

# --------------------------------------------------------------- MAIN CORRIDOR
ROOMS.append(("Main Corridor", "MCR", None, [
    item("Update rope lighting to match new dining rope light", "elec", T_ELEC + ' <span class="tag trade-elec">1 light point</span>', phase=2),
    item("New plug near feature wall", "elec", T_ELEC, phase=2),
    item("Test existing wire feed", "elec", T_ELEC, phase=2),
    item("Fix emergency light short circuit", "elec", T_ELEC + KNOWN,
         "Trace and repair the short before the new unit is energised.", phase=2),
    item("Install vent covers", "fit", T_FIT, phase=9),
    item("Install corridor mirror — 3m x 1.5m", "fit", T_FIT,
         "Large and heavy — confirm fixings and who lifts it. Fit after painting.", phase=9),
]))

# -------------------------------------------------------------- SMALL CORRIDOR
ROOMS.append(("Small Corridor", "SCR", None, [
    item("Extend gypsum partition +10cm to fit new cabinets", "gypsum", T_GYP,
         "Existing cabinet LED lighting point to be reused/kept.",
         dep_override="start", phase=5),
]))

# ----------------------------------------------------------------- MAIN BEDROOM
ROOMS.append(("Main Bedroom", "MBR", None, [
    item("Gypsum — close off old ceiling light area to create new layout", "gypsum", T_GYP, phase=5),
    item("Remove 6 existing ceiling lights", "elec", T_ELEC + REMOVE, dep_override="start", phase=1),
    item("Remove 2 bedside hanging lamps", "elec", T_ELEC + REMOVE, dep_override="start", phase=1),
    item("Remove bedside window plugs", "elec", T_ELEC + REMOVE, dep_override="start", phase=1),
    item("4 new ceiling lights above bed", "elec", T_ELEC + ' <span class="tag trade-elec">4 light points</span>', phase=2),
    item("Rope lighting above bed", "elec", T_ELEC + ' <span class="tag trade-elec">1 light point</span>', phase=2),
    item("2 new bedside hanging lamps + 4 switches", "elec", T_ELEC + ' <span class="tag trade-elec">2 lights, 4 switches</span>',
         "Switches positioned near bedside.", phase=2),
    item("2 ceiling points near vanity/window", "elec", T_ELEC + ' <span class="tag trade-elec">2 light points</span>', phase=2),
    item("Wire for wardrobe LED lighting (sensor-activated)", "elec", T_ELEC + ' <span class="tag trade-elec">1 light + sensor</span>',
         "Sensor-operated, not a manual switch.", phase=2),
    item("Cut into wardrobe for gyser/pump switches", "elec", T_ELEC,
         "Gyser and pump switches built into the wardrobe carcass.",
         dep_override="MBR-11 — wardrobe built (ULF)", phase=8),
    item("Build wardrobe around ensuite door for maximum storage", "fit",
         '<span class="tag trade-fit">Carpentry</span>',
         dep_override="tiling and painting complete", phase=8),
    item("Install vanity mirror", "fit", T_FIT + TBC,
         "75cm from floor to vanity surface — fit after the vanity unit is in.",
         dep_override="vanity unit installed (by others)", phase=9),
]))

# ---------------------------------------------------------------- STUDY/OFFICE
ROOMS.append(("Study / Office", "OFF",
  "Sits off the Main Bedroom. &#9888; The new partition is a full wall, not an amendment — build it BEFORE tiling, not after (see the sequencing note in the plan).", [
    item("Build new gypsum partition wall", "gypsum", T_GYP,
         "2 layers of board on the inside, 75mm rockwool insulation in the middle, 1 single board on the outer side.",
         dep_override="start", phase=5),
    item("1 ceiling light point", "elec", T_ELEC + ' <span class="tag trade-elec">1 light point</span>', phase=2),
    item("Wire for LED lighting under desk", "elec", T_ELEC + ' <span class="tag trade-elec">1 light point</span>', phase=2),
    item("Plugs", "elec", T_ELEC + TBC,
         dep_override="OQ-04 — plug quantity", phase=2),
    item("Switch for ceiling light", "elec", T_ELEC + ' <span class="tag trade-elec">1 switch</span>', phase=2),
    item("Switch for under-desk light", "elec", T_ELEC + ' <span class="tag trade-elec">1 switch</span>', phase=2),
    item("Wire LED bar near door", "elec", T_ELEC + ' <span class="tag trade-elec">1 light point</span>', phase=2),
    item("Internet/ethernet point near desk", "elec", T_ELEC,
         "Run back to the central unit at the entrance during first fix.", phase=2),
    item("Install office mirror — 2m x 2m", "fit", T_FIT, phase=9),
]))

# --------------------------------------------------------------------- ENSUITE
ROOMS.append(("Ensuite", "ENS", None, [
    item("Paint ceiling", "gypsum", '<span class="tag trade-gypsum">Painting</span>',
         dep_override="tiling complete", phase=6),
    item("Add ensuite light switch", "elec", T_ELEC + ' <span class="tag trade-elec">1 switch</span>',
         "Positioned via the main bedroom wardrobe cut-out.",
         dep_override="MBR-10 — wardrobe cut-out", phase=8),
    item("Tank/waterproof floor and shower walls before tiling", "tile",
         T_TIL + NEW,
         "Wet-area waterproofing was not in the original list but must happen before the ensuite is tiled — confirm it is in Roderick's price.",
         dep_override="first fix complete", phase=4),
    item("Lay ensuite tiles — 60x30 (dimensions to confirm)", "tile", T_TIL + TBC,
         "&#9888; Site photos show the ensuite is already tiled in large-format travertine. Confirm whether it is being re-tiled at all, or only the shower area (OQ-10).",
         dep_override="OQ-05 &amp; OQ-10 — confirm scope; ENS-03 waterproofing", phase=4),
    item("Change shower glass + tray", "fit", T_FIT,
         dep_override="tiling complete", phase=9),
]))

# --------------------------------------------------------------- MAIN BATHROOM
ROOMS.append(("Main Bathroom", "BTH", None, [
    item("Install water heater (gyser) + electrical point", "plumb",
         T_PLU + '<span class="tag trade-elec">1 point</span>',
         "Confirm gyser size/position before first fix — it sets the pipe and cable runs.", phase=2),
    item("Replace sink accessories with new", "fit", T_FIT, phase=9),
    item("Replace flushing accessories with new", "fit", T_FIT, phase=9),
    item("Attach mirror", "fit", T_FIT, phase=9),
    item("Attach sink", "fit", T_FIT, phase=9),
    item("Attach cabinets", "fit", T_FIT, phase=9),
]))

# ----------------------------------------------------------------- CY BEDROOM
ROOMS.append(("Cy's Bedroom (with window)", "CYB", None, [
    item("Plugs under desk with USB", "elec", T_ELEC + ' <span class="tag trade-elec">1 point</span>', phase=2),
    item("2 bedside plugs", "elec", T_ELEC + ' <span class="tag trade-elec">2 points</span>', phase=2),
    item("Wire for wardrobe LED lighting (sensor-activated)", "elec", T_ELEC + ' <span class="tag trade-elec">1 light + sensor</span>', phase=2),
    item("Wire feed for LED under wall furniture", "elec", T_ELEC, phase=2),
    item("Internet point near desk", "elec", T_ELEC, phase=2),
]))

# --------------------------------------------------------------- RAFE BEDROOM
ROOMS.append(("Rafe's Bedroom (with back balcony door)", "RAF", None, [
    item("Plugs under desk with USB", "elec", T_ELEC + ' <span class="tag trade-elec">1 point</span>', phase=2),
    item("2 bedside plugs", "elec", T_ELEC + ' <span class="tag trade-elec">2 points</span>', phase=2),
    item("Wire for wardrobe LED lighting (sensor-activated)", "elec", T_ELEC + ' <span class="tag trade-elec">1 light + sensor</span>', phase=2),
    item("Wire feed for LED under wall furniture", "elec", T_ELEC, phase=2),
    item("Internet point near desk", "elec", T_ELEC, phase=2),
]))

# ---------------------------------------------------------------- BACK BALCONY
ROOMS.append(("Back Balcony", "BBL",
  "Off Rafe's bedroom. No items listed yet beyond the whole-house tiling — walk it with Roderick on day one and write in anything found.", [
    item("Back balcony — confirm scope on site", "gypsum",
         '<span class="tag status-tbc">&#9888; Scope to confirm</span>' + NEW,
         "Covered by the whole-house 120x120 tiling (WHS-05). Check for: outdoor lighting, a weatherproof socket, drainage falls, railing condition and door threshold. Add items here as found.",
         dep_override="start", phase=1),
]))

# ------------------------------------------------------------------- pictures
ROOM_IMAGES = {
    "WHS": [("tiles_sq_m.jpg", "Floor plan — tiling area reference (125 sqm marked; 145 sqm quoted)")],
    "FBL": [("balcony_1.jpg", "Front balcony / outdoor kitchen — render 1"),
            ("balcony_2.jpg", "Front balcony / outdoor kitchen — render 2"),
            ('site_9826.jpg', 'SITE: front balcony — BBQ corner, existing feed &amp; camera'),
            ('site_9827.jpg', 'SITE: front balcony looking out — adhesive stacked'),
            ('site_9828.jpg', 'SITE: front balcony — already tiled, membrane at edge'),
            ('site_9895.jpg', 'SITE: sliding door to front balcony'),
    ],
    "OPN": [("openplan_dimensions_1.jpg", "Open plan — measurement drawing 1"),
            ("openplan_dimensions_2.jpg", "Open plan — measurement drawing 2"),
            ("openplan_dimensions_3.jpg", "Open plan — measurement drawing 3"),
            ("1__dining__feature_wall_revised.jpg", "Dining — feature wall render"),
            ("7__dining_+_kitchen_revised.jpg", "Kitchen / dining — render"),
            ("15_dining_+_tv_unit_revised.jpg", "Dining — TV unit render"),
            ("2__sofa_revised.jpg", "Living — sofa wall render"),
            ('site_9834.jpg', 'SITE: open plan overview — parquet still down, 120 tiles delivered'),
            ('site_9835.jpg', 'SITE: kitchen island, units wrapped'),
            ('site_9839.jpg', 'SITE: dining feature wall + fireplace, wall opened below'),
            ('site_9893.jpg', 'SITE: fireplace close-up — installed, wall broken beneath'),
            ('site_9840.jpg', 'SITE: living — existing socket run'),
            ('site_9896.jpg', 'SITE: gypsum board delivered on site'),
    ],
    "ENT": [
            ('site_9845.jpg', 'SITE: existing consumer unit, thermostat &amp; entrance door'),
    ],
    "MCR": [("10_corridor_revised.jpg", "Main corridor — render"),
            ("corridor_dimensions_1.jpg", "Corridor — measurement drawing"),
            ('site_9849.jpg', 'SITE: main corridor looking through'),
            ('site_9850.jpg', 'SITE: main corridor — existing cove lighting'),
            ('site_9843.jpg', 'SITE: corridor switch position'),
    ],
    "SCR": [("12_corridor_small_revised.jpg", "Small corridor — render")],
    "MBR": [("main_bedroom_1.jpg", "Main bedroom — render (overview)"),
            ("main_bedroom_6-bed.jpg", "Main bedroom — bed wall render"),
            ("main_bedroom_4-_wardrobe.jpg", "Main bedroom — wardrobe render"),
            ("main_bedroom_5-_vanity.jpg", "Main bedroom — vanity render"),
            ("main_bedroom_dimensions_1.jpg", "Main bedroom — measurement drawing 1"),
            ("main_bedroom_dimensions_2.jpg", "Main bedroom — measurement drawing 2")],
    "OFF": [("study_1.jpg", "Study / office — render 1"),
            ("study_2.jpg", "Study / office — render 2"),
            ("study_3.jpg", "Study / office — render 3")],
    "ENS": [
            ('site_9886.jpg', 'SITE: ensuite — existing shower enclosure'),
            ('site_9888.jpg', 'SITE: ensuite — shower tray &amp; glass to be changed'),
            ('site_9889.jpg', 'SITE: ensuite — WC &amp; window'),
            ('site_9890.jpg', 'SITE: ensuite — overview'),
    ],
    "BTH": [
            ('site_9872.jpg', 'SITE: main bathroom — existing finish'),
            ('site_9873.jpg', 'SITE: main bathroom — WC &amp; window'),
            ('site_9875.jpg', 'SITE: main bathroom — ceiling &amp; downlights'),
    ],
    "CYB": [("Cy_kids_bedroom_7.jpg", "Cy's bedroom — render 1"),
            ("Cy_kids_bedroom_10.jpg", "Cy's bedroom — render 2"),
            ("kids_bedroom_dimensions_2.jpg", "Kids bedroom — measurement drawing")],
    "RAF": [("Rafe_kids_bedroom_1.jpg", "Rafe's bedroom — render 1"),
            ("Rafe_kids_bedroom_5.jpg", "Rafe's bedroom — render 2"),
            ("kids_bedroom_dimensions_1.jpg", "Kids bedroom — measurement drawing")],
    "BBL": [],
}

# ------------------------------------------------------------------- schedule
# Phases follow Roderick's proposed order, with the gaps filled in (6-11).
PHASES = {
    1: ("Strip-out &amp; demolition", "Parquet up, feature wall and fireplace down, old fittings removed."),
    2: ("First fix — plumbing, electrical, ethernet", "Consumer unit, chasing, all cable and pipe runs, gas line. Nothing gets closed up until this is signed off."),
    3: ("Clear site — skip", "Rubble and chasing debris out before tiling starts."),
    4: ("Tiling", "Screed levelled, wet areas tanked, 120x120 laid through the flat and balconies, ensuite in 60x30."),
    5: ("Gypsum — partition &amp; amendments", "Study partition, main bedroom ceiling closure, sofa wall, small corridor extension."),
    6: ("Painting", "Ceilings and walls, including the ensuite ceiling."),
    7: ("Second fix electrical", "Fit and terminate every point, hang light fittings, LED strips and sensors, then test and certify."),
    8: ("Carpentry — ULF", "Wardrobes, cabinets, TV unit. Roderick returns to cut in the gyser/pump and ensuite switches once the wardrobe is up."),
    9: ("Fittings, sanitary &amp; outdoor", "Mirrors, shower glass and tray, sanitary ware, vent covers, PVC/tile outdoor wall finish."),
    10: ("Snagging &amp; handover", "Walk the flat against this checklist, list defects, fix, final clean, hand over certificates."),
}

verify_refs.py

build/verify_refs.py

"""Validate the generated checklist: no broken ID cross-references, no duplicate IDs.
MUST pass after every rebuild — item IDs are positional and reordering silently breaks refs."""
import re, sys, os
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from rooms_data2 import ROOMS
from bs4 import BeautifulSoup

html_path = os.path.join(HERE, "..", "output", "renovation_checklist.html")
soup = BeautifulSoup(open(html_path).read(), "html.parser")

real = set(e.get_text(strip=True) for e in soup.select(".wi-id"))
appendix = set(e.find("b").get_text(strip=True) for e in soup.select(".ap figcaption") if e.find("b"))
oq = {i for i in real if i.startswith("OQ-")}
print(f"item IDs: {len(real)-len(oq)}  OQ: {len(oq)}  appendix: {len(appendix)}")

text = soup.get_text(" ")
codes = {c for _n, c, _b, _i in ROOMS}
tokens = set(re.findall(r"\b(?:" + "|".join(codes) + r"|OQ)-\d{2}\b", text))
atokens = set(re.findall(r"\bA\d{2}\b", text))

bad = sorted(t for t in tokens if t not in real)
bada = sorted(t for t in atokens if t not in appendix)
ids = [e.get_text(strip=True) for e in soup.select(".wi-id")]
dupes = sorted({i for i in ids if ids.count(i) > 1})
items = soup.select(".wi")
nophase = [i.select_one(".wi-id").get_text(strip=True) for i in items if not i.select_one(".phase-chip")]

print("BROKEN item refs :", bad or "none")
print("BROKEN picture refs:", bada or "none")
print("DUPLICATE IDs    :", dupes or "none")
print("missing phase chip:", nophase or "none")
print(f"price inputs: {len(soup.select('.price'))}  work items: {len(items)}")
sys.exit(1 if (bad or bada or dupes) else 0)