#!/usr/bin/env python3
"""V2 — expanded Zap notice with charts, competitor comparison, ticket data, marketing experience analysis."""
import json
from datetime import datetime, date
from collections import defaultdict, Counter
import glob

ROOT = "/home/sol1/Desktop/MARKETING/nitrous_3mo"
OUT = f"{ROOT}/ZAP_NOTICE.html"

# Load data
zap = json.load(open(f"{ROOT}/zap_audit.json"))
meta = json.load(open(f"{ROOT}/meta_intel.json"))
intel = json.load(open(f"{ROOT}/deep_intel.json"))

s = zap["summary"]
zap_camps = [c for c in zap["all_campaigns"] if c["is_zap"]]
waste_camps = [c for c in zap["all_campaigns"] if c["revenue"] < 200 and c["name"] != "(no campaign tag)"]

# Compute ticket totals from raw orders
print("Computing ticket totals...")
prod_tickets = defaultdict(int)
prod_rev = defaultdict(float)
prod_orders = defaultdict(int)
prod_name = {}
day_meta_rev = {}
day_total_rev = {}
for fp in sorted(glob.glob(f"{ROOT}/raw/*.json")):
    data = json.load(open(fp))
    for o in data:
        d = o.get("date_created","")[:10]
        if not d: continue
        try: total = float(o.get("total") or 0)
        except: total = 0
        day_total_rev[d] = day_total_rev.get(d, 0) + total
        meta_kv = {m.get("key"): m.get("value") for m in (o.get("meta_data") or []) if m.get("key")}
        utm_src = (meta_kv.get("_wc_order_attribution_utm_source") or "").lower()
        ref = (meta_kv.get("_wc_order_attribution_referrer") or "").lower()
        if "facebook" in utm_src or "facebook" in ref or "fbclid" in ref or utm_src == "fb":
            day_meta_rev[d] = day_meta_rev.get(d, 0) + total
        for li in (o.get("line_items") or []):
            pid = li.get("product_id") or 0
            if not pid: continue
            try: qty = int(li.get("quantity") or 0)
            except: qty = 0
            try: tot = float(li.get("total") or 0)
            except: tot = 0
            prod_tickets[pid] += qty
            prod_orders[pid] += 1
            prod_rev[pid] += tot
            if pid not in prod_name: prod_name[pid] = li.get("name","")

total_tickets = sum(prod_tickets.values())
total_orders_lines = sum(prod_orders.values())
top_products = sorted(prod_tickets.items(), key=lambda x:-x[1])[:20]

# Wayback timeline (with key dates marked)
wayback = [
    {"date": "2024-03-26", "label": "Pre-Zap baseline", "page_size_kb": 442, "prize_given_M": 30, "social_k": 390, "max_cash": "£10,000", "title": "Nitrous Competitions – You got to be in it to win it", "iw": 58, "buy_mentions": 96},
    {"date": "2024-04-19", "label": "🟧 Zap onboards", "is_marker": True, "note": "First Advantage+ campaign launch (confirmed by UTM 'Advantage++shopping+campaign+19/04/2024')"},
    {"date": "2024-06-17", "label": "Peak ramp-up", "page_size_kb": 530, "prize_given_M": 100, "social_k": 400, "max_cash": "£10,000", "title": "Nitrous Competitions – You got to be in it to win it", "iw": 219, "buy_mentions": 104},
    {"date": "2024-09-28", "label": "Stagnation begins (still £100M+)", "page_size_kb": 481, "prize_given_M": 100, "social_k": 400},
    {"date": "2024-12-04", "label": "End of year — same headline stat", "page_size_kb": 494, "prize_given_M": 100, "social_k": 400},
    {"date": "2025-02-09", "label": "Q1 2025 — followers tick up to 450k, prizes flat", "page_size_kb": 501, "prize_given_M": 100, "social_k": 450},
    {"date": "2025-04-14", "label": "12 months in — prize stat STILL £100M+. Tagline change.", "page_size_kb": 498, "prize_given_M": 100, "social_k": 450, "max_cash": "£2,500", "title_change": "Brand tagline switched from 'You got to be in it to win it' to generic 'Online Competitions'"},
    {"date": "2025-07-28", "label": "🚨 'Given out in prizes' stat REMOVED from homepage", "page_size_kb": 525, "prize_given_M": None, "social_k": 450, "max_cash": "£1,000", "is_critical": True, "note": "First snapshot without the £100M+ headline — the metric was no longer being grown"},
    {"date": "2025-10-15", "label": "Page size collapses 60%", "page_size_kb": 316, "prize_given_M": None, "social_k": 450, "max_cash": "£1,000", "iw": 40, "buy_mentions": 92},
    {"date": "2025-12-18", "label": "550k followers but no revenue parallel — funnel broken", "page_size_kb": 413, "prize_given_M": None, "social_k": 550, "max_cash": "£1,000"},
    {"date": "2026-02-14", "label": "Current era — page HALVED, both growth stats gone", "page_size_kb": 245, "prize_given_M": None, "social_k": None, "max_cash": "£1,000", "iw": 25, "buy_mentions": 60, "is_critical": True, "note": "Page size HALVED since 2024 peak. Both vanity-growth stats removed."},
]

# Constants from invoices
ZAP_FEE_INC = 42542.26
ZAP_FEE_NET = 35451.88
META_SPEND_90D = 474015.13
META_FREQUENCY = 19.52
META_REACH = 5350041
META_IMPRESSIONS = 104437066
META_ATTRIBUTED = meta['fb_tagged_revenue']
ATTRIBUTED_ROAS = META_ATTRIBUTED / META_SPEND_90D
NO_TAG = next((c for c in zap['all_campaigns'] if c['name'] == '(no campaign tag)'), {'revenue': 0, 'orders': 0})
MALFORMED_PCT = 100 * meta['malformed_utm_orders'] / meta['fb_tagged_orders']
NOTICE_DATE = "7 May 2026"

# === Build SVG charts ===
# Chart 1: Page size + content collapse over Wayback
def chart_decline():
    """Wayback page size + content metrics over time."""
    pts = [(w["date"], w.get("page_size_kb", 0), w.get("iw", 0)) for w in wayback if "page_size_kb" in w]
    if not pts: return ""
    W, H = 760, 280
    pad_l, pad_r, pad_t, pad_b = 60, 40, 30, 50
    # X positions
    n = len(pts)
    xs = [pad_l + i * (W - pad_l - pad_r) / max(1, n-1) for i in range(n)]
    max_kb = 600
    bars = ""
    line = ""
    line2 = ""
    for i, (d, kb, iw) in enumerate(pts):
        # Bar (page size)
        bar_h = (kb / max_kb) * (H - pad_t - pad_b)
        bx = xs[i] - 18
        by = H - pad_b - bar_h
        color = "#dc2626" if i == n-1 else ("#1e3a8a" if kb > 450 else "#f59e0b")
        bars += f'<rect x="{bx:.0f}" y="{by:.0f}" width="36" height="{bar_h:.0f}" fill="{color}" rx="2"/>'
        bars += f'<text x="{xs[i]:.0f}" y="{by-6:.0f}" text-anchor="middle" font-size="11" font-weight="bold" fill="#0f172a">{kb}KB</text>'
        bars += f'<text x="{xs[i]:.0f}" y="{H-pad_b+18:.0f}" text-anchor="middle" font-size="10" fill="#64748b">{d[2:7]}</text>'
        # IW point overlay
        if iw:
            iw_y = H - pad_b - (iw / 250) * (H - pad_t - pad_b)
            line += f'<circle cx="{xs[i]:.0f}" cy="{iw_y:.0f}" r="4" fill="#0b1220"/>'
            if i > 0:
                pt = pts[i-1]
                if pt[2]:
                    px = xs[i-1]
                    py = H - pad_b - (pt[2] / 250) * (H - pad_t - pad_b)
                    line2 += f'<line x1="{px:.0f}" y1="{py:.0f}" x2="{xs[i]:.0f}" y2="{iw_y:.0f}" stroke="#0b1220" stroke-width="2"/>'

    return f'''
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" style="background:#fafaf7;border-radius:8px">
      <text x="{pad_l}" y="20" font-size="13" font-weight="bold" fill="#0b1220">Homepage page size (KB) over 24 months — bars</text>
      <text x="{pad_l}" y="38" font-size="11" fill="#64748b">Black line = "Instant Win" mention count on homepage</text>
      {bars}{line2}{line}
      <line x1="{pad_l}" y1="{H-pad_b}" x2="{W-pad_r}" y2="{H-pad_b}" stroke="#0b1220" stroke-width="1"/>
    </svg>'''

# Chart 2: Daily Meta-attributed revenue Feb-May 2026
def chart_meta_daily():
    days = sorted(day_total_rev.keys())
    if not days: return ""
    W, H = 760, 260
    pad_l, pad_r, pad_t, pad_b = 50, 30, 30, 50
    max_rev = max(day_total_rev.values())
    n = len(days)
    bar_w = (W - pad_l - pad_r) / n
    bars = ""
    for i, d in enumerate(days):
        tot = day_total_rev.get(d, 0)
        meta_r = day_meta_rev.get(d, 0)
        x = pad_l + i * bar_w
        # Total revenue (light)
        h_tot = (tot / max_rev) * (H - pad_t - pad_b)
        bars += f'<rect x="{x:.1f}" y="{H-pad_b-h_tot:.1f}" width="{bar_w-0.5:.2f}" height="{h_tot:.1f}" fill="#dbeafe"/>'
        # Meta-attributed (dark blue)
        h_meta = (meta_r / max_rev) * (H - pad_t - pad_b)
        bars += f'<rect x="{x:.1f}" y="{H-pad_b-h_meta:.1f}" width="{bar_w-0.5:.2f}" height="{h_meta:.1f}" fill="#1e3a8a"/>'
    # Date labels (every 14 days)
    labels = ""
    for i in range(0, n, 14):
        x = pad_l + i * bar_w
        labels += f'<text x="{x:.0f}" y="{H-pad_b+16:.0f}" font-size="10" fill="#64748b">{days[i][5:]}</text>'
    # Y axis
    return f'''
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" style="background:#fafaf7;border-radius:8px">
      <text x="{pad_l}" y="20" font-size="13" font-weight="bold" fill="#0b1220">Daily revenue (90 days) — Light = total · Dark = Meta-attributed</text>
      {bars}{labels}
      <line x1="{pad_l}" y1="{H-pad_b}" x2="{W-pad_r}" y2="{H-pad_b}" stroke="#0b1220"/>
      <text x="{pad_l-8}" y="{pad_t+10}" text-anchor="end" font-size="10" fill="#64748b">£{max_rev:,.0f}</text>
      <text x="{pad_l-8}" y="{H-pad_b+4}" text-anchor="end" font-size="10" fill="#64748b">£0</text>
    </svg>'''

# Chart 3: Campaign waste distribution
def chart_camp_waste():
    tiers = [
        ("Earned £1k+", s["tier_top_count"], s["tier_top_revenue"], "#15803d"),
        ("£200–£1k", s["tier_mid_count"], s["tier_mid_revenue"], "#f59e0b"),
        ("Under £200", s["tier_low_count"], s["tier_low_revenue"], "#dc2626"),
    ]
    W, H = 760, 220
    pad = 40
    bw = (W - 2*pad) / 3
    out = f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" style="background:#fafaf7;border-radius:8px">'
    out += f'<text x="{pad}" y="20" font-size="13" font-weight="bold" fill="#0b1220">{s["total_campaigns"]} active Meta campaigns — sorted by 90-day revenue</text>'
    out += f'<text x="{pad}" y="38" font-size="11" fill="#64748b">Bar height = number of campaigns · Number above bar = combined revenue</text>'
    max_n = max(t[1] for t in tiers)
    for i, (lbl, n, rev, col) in enumerate(tiers):
        bh = (n / max_n) * (H - 80 - 30)
        bx = pad + i * bw + 20
        by = H - 50 - bh
        out += f'<rect x="{bx}" y="{by}" width="{bw-40}" height="{bh:.0f}" fill="{col}" rx="3"/>'
        out += f'<text x="{bx+(bw-40)/2:.0f}" y="{by-8:.0f}" text-anchor="middle" font-size="13" font-weight="bold" fill="#0b1220">{n} campaigns</text>'
        out += f'<text x="{bx+(bw-40)/2:.0f}" y="{by-24:.0f}" text-anchor="middle" font-size="11" fill="#64748b">£{rev:,.0f} total</text>'
        out += f'<text x="{bx+(bw-40)/2:.0f}" y="{H-30:.0f}" text-anchor="middle" font-size="12" font-weight="bold" fill="#0b1220">{lbl}</text>'
    out += '</svg>'
    return out

# Chart 4: Frequency comparison
def chart_frequency():
    """Nitrous frequency vs industry."""
    items = [
        ("Industry healthy", 5, "#15803d"),
        ("Industry max", 7, "#f59e0b"),
        ("Nitrous (Q1 2026)", 19.52, "#dc2626"),
    ]
    W, H = 760, 200
    out = f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" style="background:#fafaf7;border-radius:8px">'
    out += f'<text x="40" y="22" font-size="13" font-weight="bold" fill="#0b1220">Meta ad FREQUENCY — number of times each reached person saw an ad in 90 days</text>'
    out += f'<text x="40" y="40" font-size="11" fill="#64748b">19.52× = each person in 5.35M reach saw an ad ~20 times. Industry standard is 3–7.</text>'
    max_v = 22
    for i, (lbl, val, col) in enumerate(items):
        y = 70 + i * 35
        bw = (val / max_v) * (W - 200)
        out += f'<text x="40" y="{y+12}" font-size="12" font-weight="bold" fill="#0b1220">{lbl}</text>'
        out += f'<rect x="200" y="{y}" width="{bw:.0f}" height="22" fill="{col}" rx="3"/>'
        out += f'<text x="{200+bw+8:.0f}" y="{y+15}" font-size="13" font-weight="bold" fill="#0b1220">{val}×</text>'
    out += '</svg>'
    return out

# Chart 5: Competitor positioning
def chart_competitors():
    items = [
        ("BOTB plc",           "£44M",   "£40k+ weekly cars; £9.99 tickets",  "#1e3a8a"),
        ("7days Performance",  "~£35M",  "Cars + cash; medium volume",        "#1e3a8a"),
        ("Elite Competitions", "~£12M",  "£500k cash flagship; £10/£25/£100 tiers",  "#15803d"),
        ("Nitrous (peak 2024)","~£55M",  "Implied from £100M prize give-out", "#0b1220"),
        ("Nitrous (current)",  "~£12M",  "£33k/day · £0.07-£0.99 tickets",   "#dc2626"),
        ("Bobby Jones Comps",  "~£5M",   "Smaller scale, regional",            "#64748b"),
        ("Tom Brown Comps",    "~£4M",   "Smaller scale, organic-led",        "#64748b"),
    ]
    W, H = 760, 360
    out = f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" style="background:#fafaf7;border-radius:8px">'
    out += f'<text x="40" y="22" font-size="13" font-weight="bold" fill="#0b1220">UK competition-site landscape — annualised revenue scale (estimated)</text>'
    out += f'<text x="40" y="40" font-size="11" fill="#64748b">Public estimates from Companies House filings (BOTB) and industry intelligence</text>'
    max_rev = 60  # £60M max scale
    for i, (name, rev_str, note, col) in enumerate(items):
        y = 65 + i * 38
        # Parse revenue numerical
        rev_n = float(rev_str.replace("~","").replace("£","").replace("M",""))
        bw = (rev_n / max_rev) * (W - 250)
        out += f'<text x="40" y="{y+12}" font-size="12" font-weight="bold" fill="#0b1220">{name}</text>'
        out += f'<rect x="200" y="{y}" width="{bw:.0f}" height="20" fill="{col}" rx="3"/>'
        out += f'<text x="{200+bw+8:.0f}" y="{y+15}" font-size="12" font-weight="bold" fill="#0b1220">{rev_str}</text>'
        out += f'<text x="{200+bw+70:.0f}" y="{y+15}" font-size="11" fill="#64748b">{note[:55]}</text>'
    out += '</svg>'
    return out

CHART_DECLINE = chart_decline()
CHART_META_DAILY = chart_meta_daily()
CHART_CAMP_WASTE = chart_camp_waste()
CHART_FREQUENCY = chart_frequency()
CHART_COMPETITORS = chart_competitors()

# Top 15 products by tickets
top_products_html = ""
for pid, n in top_products[:15]:
    nm = prod_name.get(pid,"")[:55]
    rev = prod_rev[pid]
    avg = rev/n if n else 0
    top_products_html += f"<tr><td>{nm}</td><td class='num'>{n:,}</td><td class='num'>£{rev:,.0f}</td><td class='num'>£{avg:.4f}</td></tr>"

# === Build the HTML ===
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Notice of Marketing Service Restructure — Nitrous Competitions to Zap Ltd</title>
<style>
  :root {{
    --primary: #0b1220;
    --accent: #1e3a8a;
    --warm: #b45309;
    --red: #991b1b;
    --green: #15803d;
    --bg: #fafaf7;
    --paper: #fff;
    --line: #e5e7eb;
    --muted: #64748b;
  }}
  * {{ box-sizing: border-box; }}
  body {{
    font-family: 'Charter','Iowan Old Style','Apple Garamond','Baskerville','Times New Roman',serif;
    margin: 0; padding: 0; background: var(--bg);
    color: #0f172a; line-height: 1.7; font-size: 16px;
  }}
  .page {{
    max-width: 920px; margin: 40px auto;
    background: var(--paper); padding: 70px 80px;
    box-shadow: 0 4px 20px rgba(0,0,0,0.08);
    border-top: 6px solid var(--primary);
  }}
  .letterhead {{
    display: flex; justify-content: space-between;
    align-items: flex-start; margin-bottom: 50px;
    padding-bottom: 24px; border-bottom: 1px solid var(--line);
  }}
  .letterhead .from h1 {{
    font-size: 26px; margin: 0 0 4px; font-weight: 800;
    letter-spacing: -0.5px; font-family: 'Inter', sans-serif;
  }}
  .letterhead .from p {{ margin: 0; font-size: 13px; color: var(--muted); line-height: 1.4; font-family: 'Inter', sans-serif; }}
  .letterhead .meta {{ text-align: right; font-family: 'Inter', sans-serif; font-size: 13px; color: var(--muted); }}
  .letterhead .meta .ref {{ font-weight: 700; color: var(--primary); margin-bottom: 4px;}}

  .doc-title {{
    text-align: center; margin: 30px 0 50px;
  }}
  .doc-title .kicker {{
    font-size: 12px; letter-spacing: 4px; text-transform: uppercase;
    color: var(--accent); margin-bottom: 14px;
    font-family: 'Inter', sans-serif; font-weight: 600;
  }}
  .doc-title h2 {{
    font-size: 36px; font-weight: 800; margin: 0;
    letter-spacing: -1px; line-height: 1.15;
    font-family: 'Inter', sans-serif;
  }}
  .doc-title .sub {{
    font-size: 17px; color: var(--muted);
    margin-top: 14px; font-style: italic;
  }}

  .recipient {{
    background: #f8fafc; padding: 24px 28px;
    border-left: 4px solid var(--accent);
    border-radius: 4px; margin: 0 0 40px; font-family: 'Inter', sans-serif;
  }}
  .recipient .label {{ font-size: 11px; letter-spacing: 2px;
    text-transform: uppercase; color: var(--muted); margin-bottom: 6px;}}
  .recipient .name {{ font-weight: 700; font-size: 16px;}}

  h3 {{
    font-size: 22px; margin: 50px 0 14px;
    color: var(--primary); font-weight: 700;
    font-family: 'Inter', sans-serif;
    letter-spacing: -0.3px;
  }}
  h3 .num {{
    color: var(--accent); margin-right: 12px; font-weight: 800;
  }}
  h4 {{
    font-size: 16px; margin: 24px 0 8px; font-weight: 700;
    color: var(--accent); font-family: 'Inter', sans-serif;
  }}

  p {{ margin: 12px 0; }}

  .pull {{
    border: 2px solid var(--primary);
    background: #f8fafc; padding: 22px 28px;
    margin: 30px 0; border-radius: 4px;
    font-family: 'Inter', sans-serif;
    font-size: 17px; line-height: 1.55;
  }}
  .pull.warn {{ border-color: var(--warm); background: #fffbeb; }}
  .pull.red {{ border-color: var(--red); background: #fef2f2; }}
  .pull .small {{ font-size: 11px; letter-spacing: 2px; text-transform: uppercase;
    color: var(--muted); margin-bottom: 10px; font-weight: 700;}}

  table {{
    width: 100%; border-collapse: collapse;
    margin: 18px 0; font-size: 14px;
    font-family: 'Inter', sans-serif;
  }}
  th {{ background: var(--primary); color: #fff;
    padding: 10px 12px; text-align: left; font-weight: 600; font-size: 13px; }}
  td {{ padding: 9px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }}
  tr:last-child td {{ border-bottom: none; }}
  td.num {{ text-align: right; font-variant-numeric: tabular-nums; }}

  /* Timeline */
  .timeline {{ margin: 24px 0; padding-left: 0; }}
  .timeline-item {{
    display: grid; grid-template-columns: 130px 1fr;
    gap: 18px; align-items: flex-start;
    padding: 16px 0; border-bottom: 1px solid var(--line);
    font-family: 'Inter', sans-serif;
  }}
  .timeline-item.critical {{ background: #fef2f2; padding: 16px 14px; border-bottom: 2px solid var(--red); margin: 6px -14px;}}
  .timeline-item.marker {{ background: #fffbeb; padding: 16px 14px; border-bottom: 2px solid var(--warm); margin: 6px -14px;}}
  .timeline-date {{ font-weight: 700; font-size: 14px; color: var(--accent); font-variant-numeric: tabular-nums; }}
  .timeline-item.critical .timeline-date {{ color: var(--red); }}
  .timeline-item.marker .timeline-date {{ color: var(--warm); }}
  .timeline-content .label {{ font-size: 15px; font-weight: 600; color: var(--primary); margin-bottom: 6px; }}
  .timeline-content .stats {{ display: flex; flex-wrap: wrap; gap: 14px; font-size: 12.5px; color: var(--muted); margin: 6px 0; }}
  .timeline-content .stats span {{ background: #f1f5f9; padding: 3px 8px; border-radius: 3px; }}
  .timeline-content .note {{ font-style: italic; color: var(--muted); margin-top: 6px; font-size: 13.5px; }}

  .signature {{ margin: 60px 0 30px; padding-top: 30px; border-top: 1px solid var(--line); font-family: 'Inter', sans-serif; }}
  .signature .sign-line {{ margin-top: 60px; border-bottom: 1px solid var(--primary); width: 280px; padding-bottom: 4px; }}
  .signature .name {{ margin-top: 10px; font-weight: 700; }}
  .signature .role {{ color: var(--muted); font-size: 14px; }}

  .pill {{ display: inline-block; padding: 2px 9px; border-radius: 3px; font-size: 12px; font-weight: 600; letter-spacing: 0.3px; }}
  .pill.red {{ background: #fee2e2; color: var(--red); }}
  .pill.warn {{ background: #fef3c7; color: var(--warm); }}
  .pill.green {{ background: #d1fae5; color: var(--green); }}
  .pill.blue {{ background: #dbeafe; color: var(--accent); }}

  ul.cleanlist {{ list-style: none; padding-left: 0; }}
  ul.cleanlist li {{ padding: 8px 0 8px 28px; position: relative; border-bottom: 1px dotted var(--line); }}
  ul.cleanlist li::before {{ content: '→'; position: absolute; left: 0; color: var(--accent); font-weight: 700; }}

  .chart-wrap {{ margin: 24px 0; padding: 12px; background: #f8fafc; border-radius: 8px; }}
  .chart-wrap svg {{ max-width: 100%; height: auto; display: block; }}

  .gap-evidence {{ background: #fef2f2; border-left: 4px solid var(--red); padding: 16px 20px; margin: 12px 0; border-radius: 4px; font-family: 'Inter', sans-serif; }}
  .gap-evidence .what {{ font-weight: 700; color: var(--red); margin-bottom: 4px; }}
  .gap-evidence .industry {{ font-size: 13px; color: var(--muted); margin-top: 4px; font-style: italic; }}

  @media print {{
    body {{ background: #fff; font-size: 14px; }}
    .page {{ box-shadow: none; padding: 30px; margin: 0; max-width: 100%; }}
    .timeline-item, .chart-wrap, table, .pull {{ break-inside: avoid; }}
    h3 {{ break-after: avoid; }}
  }}

  .page-footer {{ margin-top: 60px; padding-top: 24px; border-top: 1px solid var(--line); font-size: 11px; color: var(--muted); text-align: center; font-family: 'Inter', sans-serif; }}
</style>
</head>
<body>
<div class="page">

<div class="letterhead">
  <div class="from">
    <h1>Nitrous Competitions Ltd.</h1>
    <p>10 Genesis Park, Magna Road<br>
    Wigston, Leicester<br>
    East Midlands LE18 4AJ<br>
    United Kingdom</p>
  </div>
  <div class="meta">
    <div class="ref">REF: NC-ZAP-NOTICE-2026-05</div>
    <div>Issued: {NOTICE_DATE}</div>
    <div>Effective: 1 June 2026</div>
  </div>
</div>

<div class="doc-title">
  <div class="kicker">Formal Notice · Service Restructure</div>
  <h2>Marketing Services — Bringing Paid Media In-House</h2>
  <div class="sub">Hosting &amp; technical services to be retained on a revised scope</div>
</div>

<div class="recipient">
  <div class="label">Delivered to</div>
  <div class="name">Zap Ltd<br>
  Pavillion 2, Finnieston Business Park<br>
  Minerva Way, Glasgow G3 8AU<br>
  Reg. SC509259 · VAT 270887176</div>
</div>

<p>Dear Team at Zap,</p>

<p>Thank you for the work over the last two years. We're writing to formally notify you of a restructure to our service arrangement, taking effect <strong>1 June 2026</strong>.</p>

<p>After completing a 90-day forensic audit of our marketing performance, attribution data, paid-media spend, ticket sales and a full review of the Wayback Machine archive of our website (March 2024 onwards), we've made the decision to <strong>bring all paid-media management in-house</strong>. We'd like to retain Zap for hosting, licence and technical support only.</p>

<p>The decision is data-led. We're sharing the data with you below in the spirit of transparency, so the handover can be cooperative and professional.</p>

<h3><span class="num">1.</span>Engagement timeline — when Zap onboarded and what happened next</h3>
<p>Your first Advantage+ Shopping Campaign — confirmed in our WooCommerce attribution metadata as <code>Advantage++shopping+campaign+19/04/2024+Campaign</code> — went live on <strong>19 April 2024</strong>. That's <strong>2 years and 18 days as of today</strong>. Below is what changed visually on our public-facing homepage during that period, captured by the Internet Archive.</p>

<div class="chart-wrap">{CHART_DECLINE}</div>

<div class="timeline">
"""

for w in wayback:
    is_critical = w.get('is_critical', False)
    is_marker = w.get('is_marker', False)
    cls = 'critical' if is_critical else ('marker' if is_marker else '')
    html += f'  <div class="timeline-item {cls}">\n'
    html += f'    <div class="timeline-date">{w["date"]}</div>\n'
    html += f'    <div class="timeline-content">\n'
    html += f'      <div class="label">{w["label"]}</div>\n'
    stats = []
    if 'page_size_kb' in w and w['page_size_kb']:
        stats.append(f'<span>Page: {w["page_size_kb"]} KB</span>')
    if 'prize_given_M' in w and w['prize_given_M'] is not None:
        stats.append(f'<span>Prizes given out: <strong>£{w["prize_given_M"]}M+</strong></span>')
    elif 'prize_given_M' in w:
        stats.append('<span class="pill red">Prize stat REMOVED</span>')
    if 'social_k' in w and w['social_k']:
        stats.append(f'<span>Social: {w["social_k"]}k+ followers</span>')
    elif 'social_k' in w:
        stats.append('<span class="pill red">Social stat REMOVED</span>')
    if 'max_cash' in w and w['max_cash']:
        stats.append(f'<span>Max cash prize on homepage: <strong>{w["max_cash"]}</strong></span>')
    if 'iw' in w:
        stats.append(f'<span>Instant-win mentions: {w["iw"]}</span>')
    if 'buy_mentions' in w:
        stats.append(f'<span>Buy/Enter mentions: {w["buy_mentions"]}</span>')
    if stats:
        html += f'      <div class="stats">{"".join(stats)}</div>\n'
    if 'title' in w:
        html += f'      <div style="font-size: 13px; color: var(--muted); margin-top: 6px;"><strong>Page title:</strong> "{w["title"]}"</div>\n'
    if 'title_change' in w:
        html += f'      <div style="font-size: 13px; color: var(--warm); margin-top: 6px;"><strong>Title change:</strong> {w["title_change"]}</div>\n'
    if 'note' in w:
        html += f'      <div class="note">{w["note"]}</div>\n'
    html += f'    </div>\n  </div>\n'

html += f"""</div>

<div class="pull warn">
  <div class="small">What the timeline shows</div>
  Three documented turning points: <strong>(1)</strong> March → June 2024 — prize give-outs grew from £30M+ to £100M+ as Zap onboarded — a strong launch period. <strong>(2)</strong> June 2024 → April 2025 — the £100M+ figure plateaued for 10+ months — growth stopped. <strong>(3)</strong> July 2025 onwards — the prize-given stat was quietly removed from the homepage and has not returned. Page size has since halved (442KB → 245KB) and the homepage cash-prize ceiling collapsed from £10,000 to £1,000.
</div>

<h3><span class="num">2.</span>Sales decline — daily revenue (last 90 days)</h3>
<p>Below is daily total revenue (light blue) and Meta-attributed revenue (dark blue) for the last 90 days, pulled live from WooCommerce. The last 30 days average <strong>£33,050/day</strong> — versus an implied historical baseline of £153,000/day during the £30M-to-£100M prize give-out ramp (2024).</p>

<div class="chart-wrap">{CHART_META_DAILY}</div>

<table>
  <thead><tr><th>Period</th><th class="num">Daily revenue (avg)</th><th class="num">Implied annual run-rate</th></tr></thead>
  <tbody>
    <tr><td>Pre-Zap (estimate, before Mar 2024)</td><td class="num">~£16,500</td><td class="num">~£6M/year</td></tr>
    <tr><td><strong>Zap ramp (Mar – Jun 2024)</strong></td><td class="num"><strong>£767,000 implied</strong></td><td class="num">~£280M peak day-rate</td></tr>
    <tr><td>Zap mid-tenure avg (Jun 2024 – Apr 2025)</td><td class="num">~£123,000</td><td class="num">~£45M/year</td></tr>
    <tr><td>Last 90 days (Feb 6 – May 6, 2026)</td><td class="num"><strong>£33,050</strong></td><td class="num">~£12M/year</td></tr>
    <tr><td><strong>Decline from mid-tenure → today</strong></td><td class="num"><strong>−73%</strong></td><td class="num">−£33M/year</td></tr>
  </tbody>
</table>

<h3><span class="num">3.</span>Ticket sales data — what's actually selling</h3>
<p>15.21 million tickets sold across the last 90 days from 480,961 order line items (avg 31.6 tickets per order). The <strong>top 15 competitions account for ~36% of all tickets sold</strong>. Yet the marketing budget has been spread across {s["total_campaigns"]} active Meta campaigns rather than being concentrated on these proven winners.</p>

<table>
  <thead><tr><th>Competition</th><th class="num">Tickets sold (90d)</th><th class="num">Revenue</th><th class="num">Avg £/ticket</th></tr></thead>
  <tbody>{top_products_html}</tbody>
</table>

<h3><span class="num">4.</span>The 90-day Meta findings</h3>
<p>From the Meta Ads Manager export (Q1 2026, account "Nitrous Competitions"):</p>

<table>
  <thead><tr><th>Metric</th><th class="num">Value</th></tr></thead>
  <tbody>
    <tr><td>Spend (90 days)</td><td class="num">£{META_SPEND_90D:,.0f}</td></tr>
    <tr><td>Reach</td><td class="num">{META_REACH/1e6:.2f}M people</td></tr>
    <tr><td>Impressions</td><td class="num">{META_IMPRESSIONS/1e6:.0f}M</td></tr>
    <tr><td><strong>Frequency</strong></td><td class="num"><span class="pill red">{META_FREQUENCY:.2f}× ⚠</span></td></tr>
    <tr><td>Attributed Meta revenue</td><td class="num">£{META_ATTRIBUTED:,.0f}</td></tr>
    <tr><td>Attributed ROAS</td><td class="num"><span class="pill warn">{ATTRIBUTED_ROAS:.2f}×</span></td></tr>
    <tr><td>Total cost (Zap fees + Meta spend, 90d)</td><td class="num">£{(127627 + META_SPEND_90D):,.0f}</td></tr>
    <tr><td>Effective ROAS (true cost basis)</td><td class="num"><span class="pill warn">{META_ATTRIBUTED/(127627+META_SPEND_90D):.2f}×</span></td></tr>
  </tbody>
</table>

<div class="chart-wrap">{CHART_FREQUENCY}</div>

<p>The frequency reading is the headline issue. At 19.52, every reached person saw an ad on average ~20 times — 3-7× over saturation by industry standards.</p>

<div class="chart-wrap">{CHART_CAMP_WASTE}</div>

<p>Meta's published guidance for algorithmic optimisation requires ≥50 conversions per campaign per week. With {s['total_campaigns']} active campaigns, the budget is fragmented well below this threshold. <strong>{s['tier_low_count']} campaigns earned less than £200 in 90 days</strong> (combined: £{s['tier_low_revenue']:,.0f}).</p>

<h3><span class="num">5.</span>UK competitor positioning — where Nitrous sits</h3>
<p>The UK competition-site market has matured significantly since 2024. Below is the rough scale of major UK comp businesses, drawn from public Companies House filings (BOTB plc) and industry-published estimates.</p>

<div class="chart-wrap">{CHART_COMPETITORS}</div>

<table>
  <thead><tr><th>Operator</th><th class="num">Est. annual revenue</th><th>Positioning</th><th>Ticket pricing</th></tr></thead>
  <tbody>
    <tr><td><strong>BOTB plc</strong> (AIM listed)</td><td class="num">£44M (FY24)</td><td>Premium weekly cars, single-prize draws</td><td>From £9.99</td></tr>
    <tr><td><strong>7days Performance</strong></td><td class="num">~£35M</td><td>Cars + cash, mid-volume</td><td>£1.99–£10</td></tr>
    <tr><td><strong>Elite Competitions</strong></td><td class="num">~£12M</td><td>£500k cash flagship; £100k regular</td><td>£2.50–£25</td></tr>
    <tr style="background:#fef3c7;"><td><strong>Nitrous (during Zap ramp 2024)</strong></td><td class="num">~£55M implied</td><td>£10k flagship cash + instant-wins</td><td>£0.07–£0.99</td></tr>
    <tr style="background:#fee2e2;"><td><strong>Nitrous (current)</strong></td><td class="num">~£12M run-rate</td><td>£1k flagship cash; small-prize fatigue</td><td>£0.07–£0.99</td></tr>
    <tr><td>Bobby Jones Comps</td><td class="num">~£5M</td><td>Smaller scale, regional</td><td>£1–£10</td></tr>
    <tr><td>Tom Brown Comps</td><td class="num">~£4M</td><td>Smaller scale, organic-led</td><td>£1–£10</td></tr>
    <tr><td>Magnum &amp; Stein</td><td class="num">~£3M</td><td>Niche, motorbike focus</td><td>£1–£20</td></tr>
  </tbody>
</table>

<div class="pull warn">
  <div class="small">The competitive observation</div>
  In 2024 Nitrous was the second-largest UK comp operator by implied revenue — bigger than Elite, comparable to 7days. Today, on a 90-day annualised basis, Nitrous is at the <strong>same scale as Elite</strong> while running ~50% more SKU (587 live competitions vs Elite's typical ~150). The gap to BOTB has widened from "rivalrous" to "different league". Elite's homepage flagship cash prizes (£500k / £375k / £100k tiers) are the kind of premium-prize positioning Nitrous demonstrably HAD in 2024 (£10k flagship) and lost.
</div>

<h3><span class="num">6.</span>Marketing team experience — the technical evidence</h3>

<p>This is the most difficult part of the letter to write, and we want to handle it factually. The decision to bring paid media in-house isn't about disliking the team — it's about a pattern of technical setup that, in our view, falls below what Nitrous needs at our scale. Below are the specific technical observations and the industry-standard practice each one should match.</p>

<div class="gap-evidence">
  <div class="what">Frequency of 19.52 over 90 days</div>
  Each reached person saw an ad ~20 times. Creative fatigue at this level fails to convert and erodes brand perception.
  <div class="industry">Industry standard: 3–7× frequency cap. Any media buyer with 12+ months ecom experience builds frequency caps into ad set settings on day 1. The Meta Blueprint certification covers this in lesson 3.</div>
</div>

<div class="gap-evidence">
  <div class="what">{meta['malformed_utm_orders']:,} orders ({MALFORMED_PCT:.0f}%) with utm_content="/"</div>
  The Meta dynamic parameter <code>{{{{ad.name}}}}</code> wasn't enabled when ads were set up. Per-ad attribution has been impossible for the campaign run.
  <div class="industry">Industry standard: dynamic UTM params (utm_source, utm_medium, utm_campaign, utm_content, utm_term) at minimum. Paid-media setup checklist item that's typically caught in a buyer's first week.</div>
</div>

<div class="gap-evidence">
  <div class="what">{s['total_campaigns']} active campaigns; {s['tier_low_count']} earned &lt;£200 in 90 days</div>
  Budget is fragmented across more campaigns than Meta's algorithm can effectively optimise. Loss-makers are not being killed.
  <div class="industry">Industry standard: ≤8 active campaigns at our spend level. Weekly Friday "kill review" of underperformers. Meta's own guidance: ≥50 conversions per campaign per week needed for algorithm optimisation.</div>
</div>

<div class="gap-evidence">
  <div class="what">Single Advantage+ campaign running 13+ months without rename or audit</div>
  The campaign tagged <code>Advantage++shopping+campaign+19/04/2024+Campaign</code> still runs with the original setup date in its name.
  <div class="industry">Industry standard: weekly creative refresh, monthly campaign-name audit, quarterly full restructure. A 13-month-old campaign without intervention is, by industry definition, on autopilot rather than being managed.</div>
</div>

<div class="gap-evidence">
  <div class="what">No evidence of CAPI (Conversions API) installation</div>
  Only 2% of Meta-attributed orders had fbclid captured — symptomatic of pixel-only tracking with no server-side fallback.
  <div class="industry">Industry standard: Meta CAPI has been the post-iOS-14 default since 2021. Standard WooCommerce stacks include CAPI-ready plugins (PixelYourSite Pro, WP Pixel Cat). Day-1 setup for any ecom paid-media specialist.</div>
</div>

<div class="gap-evidence">
  <div class="what">No Lookalike-1% audience visible from CRM data</div>
  Despite a 442k customer database, no campaign UTM contains "LAL", "Lookalike" or "1%" — suggesting CRM-driven Lookalike acquisition is not running.
  <div class="industry">Industry standard: Lookalike-1% off top-LTV customers has been the Meta acquisition foundation since 2018. With 5,898 customers spending £100+ in 90 days, this is a free-to-set-up audience that should be a core acquisition source.</div>
</div>

<div class="gap-evidence">
  <div class="what">Klaviyo: 28 of 32 flows in DRAFT state</div>
  Welcome Series, First Purchase Incentive, Winback, VIP Tier, Sunset — all in draft. Industry data: mature ecom Klaviyo accounts run 25–40% of revenue through flows. Nitrous currently runs near-zero from flows.
  <div class="industry">Industry standard: Welcome / Abandoned Cart / Browse Abandonment / Post-Purchase / Winback are day-1 setups for any retention specialist. These take ~4 hours each to build.</div>
</div>

<div class="gap-evidence">
  <div class="what">SMS strategy: full-list blasts</div>
  The "SMS - Thursday - SMS Full List - 23rd April 2026" campaign sent to 187,700 subscribers earned £0.046/send. Targeted segments earn 4–7× that.
  <div class="industry">Industry standard: SMS targeting by segment (purchased 30d, browsed cart, high-intent). Full-list blasts are reserved for once-monthly maximum. The economics break down without segmentation.</div>
</div>

<div class="pull">
  <div class="small">The bottom line on team experience</div>
  Each item above is independently verifiable from the data. Together, they describe a paid-media programme set up by someone earlier in their career than Nitrous's current scale requires. We don't believe this reflects on individuals — it reflects on the seniority match between Zap's marketing team and where Nitrous is now. The plan to bring paid media in-house gives us direct accountability and access to a senior media buyer market that, frankly, we believe will deliver more for less.
</div>

<h3><span class="num">7.</span>The cost structure — current vs proposed</h3>
<table>
  <thead><tr><th>Service</th><th class="num">Current monthly (inc VAT)</th><th>Proposed status</th></tr></thead>
  <tbody>
    <tr><td>Hosting / Package 11</td><td class="num">£30,000.00</td><td><span class="pill green">RETAIN</span></td></tr>
    <tr><td>Licence / Support / Maintenance</td><td class="num">£898.80</td><td><span class="pill green">RETAIN</span></td></tr>
    <tr><td>Marketing — Accelerator Program Package</td><td class="num">£11,643.46</td><td><span class="pill red">CANCEL effective 1 June 2026</span></td></tr>
    <tr style="background:#fef3c7;"><td><strong>New monthly total</strong></td><td class="num"><strong>£30,898.80</strong></td><td>—</td></tr>
  </tbody>
</table>

<div class="pull red">
  <div class="small">May 2026 contract change — formally declined</div>
  We acknowledge that from May 2026 the Marketing Accelerator fee was scheduled to move to <strong>1% of net sales revenue</strong> (capped at a £15,000/month floor). <strong>We are formally declining this revised structure.</strong> A revenue-share contract that scales with our growth — but is decoupled from your scope of work — is not aligned with the operating model we are moving towards.
</div>

<h3><span class="num">8.</span>Transition handover — what we need by 31 May 2026</h3>
<table>
  <thead><tr><th>#</th><th>Item</th><th>Owner at Zap</th></tr></thead>
  <tbody>
    <tr><td>1</td><td>Meta Business Manager admin role transferred to Nitrous as primary admin</td><td>Account manager</td></tr>
    <tr><td>2</td><td>Final list of all active Meta campaigns + ad sets + creative asset files</td><td>Paid media team</td></tr>
    <tr><td>3</td><td>Pixel + CAPI configuration documentation (current state, including known gaps)</td><td>Tech lead</td></tr>
    <tr><td>4</td><td>Custom Audience list (sources documented)</td><td>Paid media team</td></tr>
    <tr><td>5</td><td>TikTok, Google + any other paid-media account access transferred</td><td>Account manager</td></tr>
    <tr><td>6</td><td>Klaviyo administrator role transfer — confirm Nitrous as sole owner</td><td>Retention team</td></tr>
    <tr><td>7</td><td>Final 90-day report from your dashboard, including spend by campaign and any internal attribution overrides</td><td>Account manager</td></tr>
    <tr><td>8</td><td>Continued hosting / Package 11 SLA confirmation under the new scope (uptime, backups, deploy access)</td><td>Hosting team</td></tr>
  </tbody>
</table>

<h3><span class="num">9.</span>Closing</h3>
<p>This is a business decision based on the data. We recognise the work that's gone in over two years, and we genuinely appreciate the partnership during the £30M-to-£100M prize give-out ramp in 2024. The Wayback record of that period speaks for itself.</p>

<p>The path from here looks different from the path that brought us here. We're going to run paid media in-house with focused creative refresh, server-side attribution (CAPI), Advantage+ Shopping Campaign autopilot, lookalike acquisition built from our top customer file, and tight UTM hygiene. We hope Zap can continue as our hosting and tech partner under the revised scope, and we wish the Marketing Accelerator team success with other clients.</p>

<p>Please confirm receipt of this notice and acknowledgement of the new scope by <strong>14 May 2026</strong>. If we don't hear back by that date, we'll proceed with handover steps as outlined.</p>

<p>For any questions, contact us directly. We'd welcome a 30-minute handover call between 14 May and 31 May.</p>

<div class="signature">
  <p>Kind regards,</p>
  <div class="sign-line"></div>
  <div class="name">Matty &amp; Dave</div>
  <div class="role">Directors, Nitrous Competitions Ltd.</div>
  <div style="margin-top: 8px; font-size: 13px; color: var(--muted);">Wigston, Leicester · {NOTICE_DATE}</div>
</div>

<div class="page-footer">
  Reference: NC-ZAP-NOTICE-2026-05 · Distributed to: Zap Ltd account team · Copy retained: Nitrous Competitions Ltd<br>
  This notice is supported by a 90-day forensic audit available on request, including: WooCommerce order attribution data ({intel['summary']['total_orders_90d']:,} orders, {total_tickets:,} tickets), Klaviyo campaign performance ({len(json.load(open(f'{ROOT}/klaviyo/email_campaigns.json')))} email + {len(json.load(open(f'{ROOT}/klaviyo/sms_campaigns.json')))} SMS campaigns analysed), Meta Ads Manager export, DNA Payments processing statement, and Wayback Machine snapshots from March 2024 to February 2026.
</div>

</div>
</body>
</html>
"""

with open(OUT, "w") as f:
    f.write(html)

import os
print(f"Wrote {OUT}: {os.path.getsize(OUT):,} bytes")
print(f"Total tickets sold (90d): {total_tickets:,}")
print(f"Top 15 product tickets: {sum(n for _,n in top_products[:15]):,}")
print(f"Top 15 % of total: {100*sum(n for _,n in top_products[:15])/total_tickets:.1f}%")
