elements.append(Paragraph("💰 Weekly Cashflow", styles['SectionHead']))

    month_income, month_expenses = income_expenses(data['txns_month'])
    change_text = f"{'↑' if expense_change_pct > 0 else '↓'} {abs(expense_change_pct):.0f}% vs prior week"
    change_color = RED if expense_change_pct > 0 else GREEN

    flow_data = [
        ['', 'This Week', 'Prior Week', 'February MTD'],
        ['Income', f"${week_income:,.2f}", f"${prev_income:,.2f}", f"${month_income:,.2f}"],
        ['Expenses', f"${week_expenses:,.2f}", f"${prev_expenses:,.2f}", f"${month_expenses:,.2f}"],
        ['Net', f"${week_income - week_expenses:,.2f}",
         f"${prev_income - prev_expenses:,.2f}",
         f"${month_income - month_expenses:,.2f}"],
    ]

    flow_table = Table(flow_data, colWidths=[1.2*inch, 1.8*inch, 1.8*inch, 1.8*inch])
    flow_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 10),
        ('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor("#fafafa")]),
        ('TOPPADDING', (0, 0), (-1, -1), 6),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
    ]))
    elements.append(flow_table)
    elements.append(Paragraph(f"{change_text}", styles['SmallText']))
    elements.append(Spacer(1, 8))

    # ─── Daily Spending Chart ──────────────────────────────────
    daily = spending_by_day(data['txns_week'])
    if daily:
        daily_chart = make_daily_spending_bar(daily, "Daily Spending This Week")
        elements.append(Image(daily_chart, width=5.5*inch, height=2.8*inch))
        elements.append(Spacer(1, 4))

    # ─── Category Breakdown (own page) ───────────────────────────
    elements.append(PageBreak())
    elements.append(Paragraph("📂 Spending by Category", styles['SectionHead']))

    week_cats = spending_by_category(data['txns_week'])
    month_cats = spending_by_category(data['txns_month'])

    # Pie chart (compact)
    if week_cats:
        cat_pie = make_category_pie(week_cats, "This Week's Spending")
        elements.append(Image(cat_pie, width=4.2*inch, height=3.2*inch))
        elements.append(Spacer(1, 6))

    # Category table — sorted by this week's spend descending, cap at 15 rows to fit page
    # Merge week + month categories, ordered by week spend
    all_cat_names = sorted(
        set(list(week_cats.keys()) + list(month_cats.keys())),
        key=lambda c: week_cats.get(c, 0),
        reverse=True
    )

    cat_rows = [['Category', 'This Week', 'Feb MTD']]
    for cat in all_cat_names[:15]:
        cat_rows.append([cat, f"${week_cats.get(cat, 0):,.2f}", f"${month_cats.get(cat, 0):,.2f}"])
    cat_rows.append(['TOTAL', f"${sum(week_cats.values()):,.2f}", f"${sum(month_cats.values()):,.2f}"])

    cat_table = Table(cat_rows, colWidths=[2.8*inch, 1.6*inch, 1.6*inch])
    cat_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTNAME', (0, -1), (-1, -1), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 9),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('BACKGROUND', (0, -1), (-1, -1), colors.HexColor(LIGHT_BG)),
        ('TOPPADDING', (0, 0), (-1, -1), 4),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
    ]))
    elements.append(cat_table)

    # ─── Month Comparison Chart ────────────────────────────────
    elements.append(PageBreak())
    elements.append(Paragraph("📈 Monthly Comparison", styles['SectionHead']))

    last_month_cats = spending_by_category(data['txns_last_month'])
    if month_cats and last_month_cats:
        comp_chart = make_month_comparison(month_cats, last_month_cats)
        elements.append(Image(comp_chart, width=5.5*inch, height=3.2*inch))

    # ─── Spending Rules Audit ──────────────────────────────────
    elements.append(Paragraph("✅ Spending Rules Compliance", styles['SectionHead']))

    week_txns = data['txns_week']

    # Check each day
    rule_rows = [['Date', 'Day', 'Eating Out', 'Groceries', 'Status']]

    from datetime import datetime as dt
    days_data = defaultdict(lambda: {"eating_out": 0, "groceries": 0, "fun": 0, "total": 0})

    for t in week_txns:
        if t["amount"] >= 0: continue
        cat = (t.get("category") or {}).get("name", "").lower()
        date = t["date"]
        amt = abs(t["amount"])
        days_data[date]["total"] += amt
        if cat in ("restaurants & bars", "fast food", "dining"): days_data[date]["eating_out"] += amt
        elif cat in ("groceries",): days_data[date]["groceries"] += amt
        elif cat in ("entertainment", "shopping", "hobbies"): days_data[date]["fun"] += amt

    total_violations = 0
    for date in sorted(days_data.keys()):
        d = days_data[date]
        day_obj = dt.strptime(date, "%Y-%m-%d")
        day_name = day_obj.strftime("%A")

        violations = []
        if d["eating_out"] > 0 and day_name != "Saturday":
            violations.append(f"Ate out (${d['eating_out']:.0f})")
        elif d["eating_out"] > 40 and day_name == "Saturday":
            violations.append(f"Over $40 dinner")

        status = "✅" if not violations else "⚠ " + "; ".join(violations)
        if violations: total_violations += 1

        rule_rows.append([
            date, day_name[:3],
            f"${d['eating_out']:,.0f}" if d['eating_out'] > 0 else "—",
            f"${d['groceries']:,.0f}" if d['groceries'] > 0 else "—",
            status
        ])

    rule_table = Table(rule_rows, colWidths=[1.2*inch, 0.6*inch, 1*inch, 1*inch, 2.8*inch])
    rule_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 9),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (2, 0), (3, -1), 'RIGHT'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('TOPPADDING', (0, 0), (-1, -1), 4),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
    ]))
    elements.append(rule_table)

    compliance = ((len(days_data) - total_violations) / len(days_data) * 100) if days_data else 100
    comp_color = GREEN if compliance >= 80 else AMBER if compliance >= 60 else RED
    elements.append(Spacer(1, 8))
    elements.append(Paragraph(
        f"Compliance Score: {compliance:.0f}% "
        f"({len(days_data) - total_violations}/{len(days_data)} clean days)",
        styles['BodyText2']))

    # ─── All Transactions ──────────────────────────────────────
    elements.append(Spacer(1, 16))
    elements.append(Paragraph("📋 All Transactions This Week", styles['SectionHead']))

    txn_rows = [['Date', 'Merchant', 'Category', 'Amount']]
    sorted_txns = sorted(week_txns, key=lambda x: x.get("date", ""))

    for t in sorted_txns:
        amt = t["amount"]
        merchant = (t.get("merchant") or {}).get("name", "Unknown")[:28]
        cat = (t.get("category") or {}).get("name", "?")[:20]
        sign = "+" if amt > 0 else ""
        txn_rows.append([t["date"], merchant, cat, f"{sign}${abs(amt):,.2f}"])

    txn_table = Table(txn_rows, colWidths=[1*inch, 2.2*inch, 1.6*inch, 1.2*inch])
    txn_style = [
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 8),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (3, 0), (3, -1), 'RIGHT'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('TOPPADDING', (0, 0), (-1, -1), 3),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 3),
    ]
    # Color negative amounts red
    for i, t in enumerate(sorted_txns, start=1):
        if t["amount"] < 0:
            txn_style.append(('TEXTCOLOR', (3, i), (3, i), colors.HexColor(RED)))
        else:
            txn_style.append(('TEXTCOLOR', (3, i), (3, i), colors.HexColor(GREEN)))

    txn_table.setStyle(TableStyle(txn_style))
    elements.append(txn_table)

    # ─── Goals & Targets ───────────────────────────────────────
    elements.append(Spacer(1, 16))
    elements.append(Paragraph("🎯 Goals & Targets", styles['SectionHead']))

    debt = balances['credit_cards']
    next_milestone = (int(debt / 1000)) * 1000
    distance = debt - next_milestone

    # Payoff projection
    if data.get('txns_last_month'):
        jan_income, jan_expenses = income_expenses(data['txns_last_month'])
        monthly_surplus = jan_income - jan_expenses
        if monthly_surplus > 0:
            months_to_payoff = debt / monthly_surplus
            payoff_date = datetime.now()
            for _ in range(int(months_to_payoff)):
                if payoff_date.month == 12:
                    payoff_date = payoff_date.replace(year=payoff_date.year + 1, month=1)
                else:
                    payoff_date = payoff_date.replace(month=payoff_date.month + 1)
            payoff_str = payoff_date.strftime("%B %Y")
        else:
            payoff_str = "N/A (spending exceeds income)"
    else:
        payoff_str = "Insufficient data"

    goals_data = [
        ['Goal', 'Status', 'Notes'],
        ['Eliminate CC Debt', f'${debt:,.0f} remaining', f'Next milestone: ${next_milestone:,} (${distance:,.0f} away)'],
        ['Projected Payoff', payoff_str, 'Based on current income vs expenses'],
        ['Follow Spending Rules', f'{compliance:.0f}% this week', f'{total_violations} violation(s)'],
        ['Net Worth > $0', f'${net_worth:,.0f}', '✅ Positive!' if net_worth > 0 else '❌ Still negative'],
        ['Emergency Fund', f'${balances["savings"]:,.0f}', 'Target: $1,000'],
    ]

    goals_table = Table(goals_data, colWidths=[1.8*inch, 1.8*inch, 2.8*inch])
    goals_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 9),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('TOPPADDING', (0, 0), (-1, -1), 5),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 5),
        ('VALIGN', (0, 0), (-1, -1), 'TOP'),
    ]))
    elements.append(goals_table)

    # ─── Footer ────────────────────────────────────────────────
    elements.append(Spacer(1, 24))
    elements.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor("#dee2e6")))
    elements.append(Spacer(1, 6))
    elements.append(Paragraph(
        f"Generated by Link (AI Chief of Staff) via Monarch Money data. "
        f"For accountability review with Tari. Report date: {today.strftime('%Y-%m-%d %H:%M')}",
        styles['SmallText']))

    # Build
    doc.build(elements)
    return filename

# ─── Main ──────────────────────────────────────────────────────────────

[6 more lines in file. Use offset=820 to continue.]


Now I have the full picture. I need to restructure page 2 to be: Weekly Cashflow + Daily Spending + Goals & Targets + Spending Rules Compliance (fleshed out). Move Month Comparison and Transactions to later pages. Let me do it:

edit

# ─── Daily Spending Chart ──────────────────────────────────
    daily = spending_by_day(data['txns_week'])
    if daily:
        daily_chart = make_daily_spending_bar(daily, "Daily Spending This Week")
        elements.append(Image(daily_chart, width=5.5*inch, height=2.8*inch))
        elements.append(Spacer(1, 4))

    # ─── Category Breakdown (own page) ───────────────────────────
    elements.append(PageBreak())
    elements.append(Paragraph("📂 Spending by Category", styles['SectionHead']))

    week_cats = spending_by_category(data['txns_week'])
    month_cats = spending_by_category(data['txns_month'])

    # Pie chart (compact)
    if week_cats:
        cat_pie = make_category_pie(week_cats, "This Week's Spending")
        elements.append(Image(cat_pie, width=4.2*inch, height=3.2*inch))
        elements.append(Spacer(1, 6))

    # Category table — sorted by this week's spend descending, cap at 15 rows to fit page
    # Merge week + month categories, ordered by week spend
    all_cat_names = sorted(
        set(list(week_cats.keys()) + list(month_cats.keys())),
        key=lambda c: week_cats.get(c, 0),
        reverse=True
    )

    cat_rows = [['Category', 'This Week', 'Feb MTD']]
    for cat in all_cat_names[:15]:
        cat_rows.append([cat, f"${week_cats.get(cat, 0):,.2f}", f"${month_cats.get(cat, 0):,.2f}"])
    cat_rows.append(['TOTAL', f"${sum(week_cats.values()):,.2f}", f"${sum(month_cats.values()):,.2f}"])

    cat_table = Table(cat_rows, colWidths=[2.8*inch, 1.6*inch, 1.6*inch])
    cat_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTNAME', (0, -1), (-1, -1), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 9),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('BACKGROUND', (0, -1), (-1, -1), colors.HexColor(LIGHT_BG)),
        ('TOPPADDING', (0, 0), (-1, -1), 4),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
    ]))
    elements.append(cat_table)

    # ─── Month Comparison Chart ────────────────────────────────
    elements.append(PageBreak())
    elements.append(Paragraph("📈 Monthly Comparison", styles['SectionHead']))

    last_month_cats = spending_by_category(data['txns_last_month'])
    if month_cats and last_month_cats:
        comp_chart = make_month_comparison(month_cats, last_month_cats)
        elements.append(Image(comp_chart, width=5.5*inch, height=3.2*inch))

    # ─── Spending Rules Audit ──────────────────────────────────
    elements.append(Paragraph("✅ Spending Rules Compliance", styles['SectionHead']))

    week_txns = data['txns_week']

    # Check each day
    rule_rows = [['Date', 'Day', 'Eating Out', 'Groceries', 'Status']]

    from datetime import datetime as dt
    days_data = defaultdict(lambda: {"eating_out": 0, "groceries": 0, "fun": 0, "total": 0})

    for t in week_txns:
        if t["amount"] >= 0: continue
        cat = (t.get("category") or {}).get("name", "").lower()
        date = t["date"]
        amt = abs(t["amount"])
        days_data[date]["total"] += amt
        if cat in ("restaurants & bars", "fast food", "dining"): days_data[date]["eating_out"] += amt
        elif cat in ("groceries",): days_data[date]["groceries"] += amt
        elif cat in ("entertainment", "shopping", "hobbies"): days_data[date]["fun"] += amt

    total_violations = 0
    for date in sorted(days_data.keys()):
        d = days_data[date]
        day_obj = dt.strptime(date, "%Y-%m-%d")
        day_name = day_obj.strftime("%A")

        violations = []
        if d["eating_out"] > 0 and day_name != "Saturday":
            violations.append(f"Ate out (${d['eating_out']:.0f})")
        elif d["eating_out"] > 40 and day_name == "Saturday":
            violations.append(f"Over $40 dinner")

        status = "✅" if not violations else "⚠ " + "; ".join(violations)
        if violations: total_violations += 1

        rule_rows.append([
            date, day_name[:3],
            f"${d['eating_out']:,.0f}" if d['eating_out'] > 0 else "—",
            f"${d['groceries']:,.0f}" if d['groceries'] > 0 else "—",
            status
        ])

    rule_table = Table(rule_rows, colWidths=[1.2*inch, 0.6*inch, 1*inch, 1*inch, 2.8*inch])
    rule_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 9),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (2, 0), (3, -1), 'RIGHT'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('TOPPADDING', (0, 0), (-1, -1), 4),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
    ]))
    elements.append(rule_table)

    compliance = ((len(days_data) - total_violations) / len(days_data) * 100) if days_data else 100
    comp_color = GREEN if compliance >= 80 else AMBER if compliance >= 60 else RED
    elements.append(Spacer(1, 8))
    elements.append(Paragraph(
        f"Compliance Score: {compliance:.0f}% "
        f"({len(days_data) - total_violations}/{len(days_data)} clean days)",
        styles['BodyText2']))

    # ─── All Transactions ──────────────────────────────────────
    elements.append(Spacer(1, 16))
    elements.append(Paragraph("📋 All Transactions This Week", styles['SectionHead']))

    txn_rows = [['Date', 'Merchant', 'Category', 'Amount']]
    sorted_txns = sorted(week_txns, key=lambda x: x.get("date", ""))

    for t in sorted_txns:
        amt = t["amount"]
        merchant = (t.get("merchant") or {}).get("name", "Unknown")[:28]
        cat = (t.get("category") or {}).get("name", "?")[:20]
        sign = "+" if amt > 0 else ""
        txn_rows.append([t["date"], merchant, cat, f"{sign}${abs(amt):,.2f}"])

    txn_table = Table(txn_rows, colWidths=[1*inch, 2.2*inch, 1.6*inch, 1.2*inch])
    txn_style = [
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 8),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (3, 0), (3, -1), 'RIGHT'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('TOPPADDING', (0, 0), (-1, -1), 3),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 3),
    ]
    # Color negative amounts red
    for i, t in enumerate(sorted_txns, start=1):
        if t["amount"] < 0:
            txn_style.append(('TEXTCOLOR', (3, i), (3, i), colors.HexColor(RED)))
        else:
            txn_style.append(('TEXTCOLOR', (3, i), (3, i), colors.HexColor(GREEN)))

    txn_table.setStyle(TableStyle(txn_style))
    elements.append(txn_table)

    # ─── Goals & Targets ───────────────────────────────────────
    elements.append(Spacer(1, 16))
    elements.append(Paragraph("🎯 Goals & Targets", styles['SectionHead']))

    debt = balances['credit_cards']
    next_milestone = (int(debt / 1000)) * 1000
    distance = debt - next_milestone

    # Payoff projection
    if data.get('txns_last_month'):
        jan_income, jan_expenses = income_expenses(data['txns_last_month'])
        monthly_surplus = jan_income - jan_expenses
        if monthly_surplus > 0:
            months_to_payoff = debt / monthly_surplus
            payoff_date = datetime.now()
            for _ in range(int(months_to_payoff)):
                if payoff_date.month == 12:
                    payoff_date = payoff_date.replace(year=payoff_date.year + 1, month=1)
                else:
                    payoff_date = payoff_date.replace(month=payoff_date.month + 1)
            payoff_str = payoff_date.strftime("%B %Y")
        else:
            payoff_str = "N/A (spending exceeds income)"
    else:
        payoff_str = "Insufficient data"

    goals_data = [
        ['Goal', 'Status', 'Notes'],
        ['Eliminate CC Debt', f'${debt:,.0f} remaining', f'Next milestone: ${next_milestone:,} (${distance:,.0f} away)'],
        ['Projected Payoff', payoff_str, 'Based on current income vs expenses'],
        ['Follow Spending Rules', f'{compliance:.0f}% this week', f'{total_violations} violation(s)'],
        ['Net Worth > $0', f'${net_worth:,.0f}', '✅ Positive!' if net_worth > 0 else '❌ Still negative'],
        ['Emergency Fund', f'${balances["savings"]:,.0f}', 'Target: $1,000'],
    ]

    goals_table = Table(goals_data, colWidths=[1.8*inch, 1.8*inch, 2.8*inch])
    goals_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 9),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('TOPPADDING', (0, 0), (-1, -1), 5),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 5),
        ('VALIGN', (0, 0), (-1, -1), 'TOP'),
    ]))
    elements.append(goals_table)

# ─── Daily Spending Chart ──────────────────────────────────
    daily = spending_by_day(data['txns_week'])
    if daily:
        daily_chart = make_daily_spending_bar(daily, "Daily Spending This Week")
        elements.append(Image(daily_chart, width=5.5*inch, height=2.8*inch))
        elements.append(Spacer(1, 4))

    # ─── Compute spending rules data (used on this page and page 2) ──
    week_txns = data['txns_week']
    from datetime import datetime as dt
    days_data = defaultdict(lambda: {"eating_out": 0, "groceries": 0, "fun": 0, "total": 0, "big": []})

    for t in week_txns:
        if t["amount"] >= 0: continue
        cat = (t.get("category") or {}).get("name", "").lower()
        merchant = (t.get("merchant") or {}).get("name", "Unknown")
        date = t["date"]
        amt = abs(t["amount"])
        days_data[date]["total"] += amt
        if cat in ("restaurants & bars", "fast food", "dining", "food & drink"):
            days_data[date]["eating_out"] += amt
        elif cat in ("groceries",):
            days_data[date]["groceries"] += amt
        elif cat in ("entertainment", "shopping", "hobbies"):
            days_data[date]["fun"] += amt
        if amt >= 50:
            days_data[date]["big"].append(f"{merchant} (${amt:,.0f})")

    total_violations = 0
    violation_details = []
    clean_days = 0
    for date in sorted(days_data.keys()):
        d = days_data[date]
        day_obj = dt.strptime(date, "%Y-%m-%d")
        day_name = day_obj.strftime("%A")

        day_violations = []
        if d["eating_out"] > 0 and day_name != "Saturday":
            day_violations.append(f"Ate out on {day_name} (${d['eating_out']:,.0f})")
        elif d["eating_out"] > 40 and day_name == "Saturday":
            day_violations.append(f"Saturday dinner over $40 (${d['eating_out']:,.0f})")

        if d["groceries"] > 0:
            if day_name == "Sunday" and d["groceries"] > 80:
                day_violations.append(f"Sunday groceries over $80 (${d['groceries']:,.0f})")
            elif day_name == "Wednesday" and d["groceries"] > 50:
                day_violations.append(f"Wed restock over $50 (${d['groceries']:,.0f})")
            elif day_name not in ("Sunday", "Wednesday"):
                day_violations.append(f"Off-schedule groceries on {day_name} (${d['groceries']:,.0f})")

        if day_name in ("Monday", "Tuesday", "Wednesday", "Thursday") and d["fun"] > 0:
            day_violations.append(f"Weekday fun money (${d['fun']:,.0f})")
        elif day_name in ("Friday", "Saturday") and d["fun"] > 20:
            day_violations.append(f"Weekend fun over $20 (${d['fun']:,.0f})")

        if day_violations:
            total_violations += 1
            for v in day_violations:
                violation_details.append(f"{date} ({day_name[:3]}): {v}")
        else:
            clean_days += 1

    compliance = (clean_days / len(days_data) * 100) if days_data else 100

    # ─── Goals & Targets (on page 2 with cashflow) ────────────
    elements.append(Spacer(1, 8))
    elements.append(Paragraph("🎯 Goals & Targets", styles['SectionHead']))

    debt = balances['credit_cards']
    next_milestone = (int(debt / 1000)) * 1000
    if next_milestone == int(debt):
        next_milestone -= 1000
    distance = debt - next_milestone

    # Payoff projection
    if data.get('txns_last_month'):
        jan_income, jan_expenses = income_expenses(data['txns_last_month'])
        monthly_surplus = jan_income - jan_expenses
        if monthly_surplus > 0:
            months_to_payoff = debt / monthly_surplus
            payoff_date = datetime.now()
            for _ in range(int(months_to_payoff)):
                if payoff_date.month == 12:
                    payoff_date = payoff_date.replace(year=payoff_date.year + 1, month=1)
                else:
                    payoff_date = payoff_date.replace(month=payoff_date.month + 1)
            payoff_str = payoff_date.strftime("%B %Y")
        else:
            payoff_str = "N/A (spending exceeds income)"
    else:
        payoff_str = "Insufficient data"

    # Goals table — expanded
    goals_data = [
        ['Goal', 'Current', 'Target', 'Progress'],
        ['Eliminate CC Debt', f'${debt:,.0f}', '$0',
         f'${distance:,.0f} to next milestone (${next_milestone:,})'],
        ['Projected Payoff', payoff_str, '—',
         'Based on last month\'s surplus'],
        ['Spending Rules', f'{compliance:.0f}%', '100%',
         f'{clean_days}/{len(days_data)} clean days this week'],
        ['Net Worth > $0', f'${net_worth:,.0f}',
         'Positive' if net_worth > 0 else '$0',
         '✅ Barely positive — fragile' if net_worth > 0 else '❌ Still underwater'],
        ['Emergency Fund', f'${balances["savings"]:,.0f}', '$1,000',
         f'{balances["savings"]/1000*100:.0f}% of target'],
    ]

    goals_table = Table(goals_data, colWidths=[1.5*inch, 1.3*inch, 0.9*inch, 3.0*inch])
    goals_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 9),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (1, 0), (2, -1), 'RIGHT'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('TOPPADDING', (0, 0), (-1, -1), 5),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 5),
        ('VALIGN', (0, 0), (-1, -1), 'TOP'),
    ]))
    elements.append(goals_table)

    # Goals narrative
    goals_narrative_parts = []
    if debt_change is not None and debt_change > 0:
        goals_narrative_parts.append(f"Debt moved in the wrong direction this week — up ${abs(debt_change):,.0f}. "
                                     f"At the current pace, payoff is projected for {payoff_str}.")
    elif debt_change is not None and debt_change < -50:
        goals_narrative_parts.append(f"Good progress — debt dropped ${abs(debt_change):,.0f} this week. "
                                     f"Keep this up and payoff is projected for {payoff_str}.")
    else:
        goals_narrative_parts.append(f"Debt held flat. Projected payoff: {payoff_str}.")

    if net_worth > 0 and net_worth < 500:
        goals_narrative_parts.append("Net worth is technically positive, but there's almost no buffer. "
                                     "One unexpected expense could put you back underwater.")
    elif net_worth <= 0:
        goals_narrative_parts.append(f"Net worth is negative — you owe ${abs(net_worth):,.0f} more than you own. "
                                     "Every dollar of debt paid changes this.")

    if balances['savings'] < 100:
        goals_narrative_parts.append("Emergency fund is essentially empty. Even $50/month would start building a cushion.")

    elements.append(Spacer(1, 4))
    elements.append(Paragraph(" ".join(goals_narrative_parts), styles['BodyText2']))

    # ─── Spending Rules Compliance ─────────────────────────────
    elements.append(Spacer(1, 10))
    elements.append(Paragraph("✅ Spending Rules Compliance", styles['SectionHead']))

    # Compliance score banner
    comp_color = GREEN if compliance >= 80 else AMBER if compliance >= 60 else RED
    score_text = f"{compliance:.0f}%"
    score_sub = f"  {clean_days} of {len(days_data)} days clean"
    elements.append(Paragraph(f"{score_text} — {score_sub}", styles['BodyText2']))
    elements.append(Spacer(1, 6))

    # Day-by-day table with more detail
    rule_rows = [['Date', 'Day', 'Eating Out', 'Groceries', 'Fun $', 'Total', 'Status']]
    for date in sorted(days_data.keys()):
        d = days_data[date]
        day_obj = dt.strptime(date, "%Y-%m-%d")
        day_name = day_obj.strftime("%A")

        # Determine status
        is_clean = True
        for v in violation_details:
            if v.startswith(date):
                is_clean = False
                break
        status = "✅" if is_clean else "⚠"

        rule_rows.append([
            date, day_name[:3],
            f"${d['eating_out']:,.0f}" if d['eating_out'] > 0 else "—",
            f"${d['groceries']:,.0f}" if d['groceries'] > 0 else "—",
            f"${d['fun']:,.0f}" if d['fun'] > 0 else "—",
            f"${d['total']:,.0f}",
            status,
        ])

    rule_table = Table(rule_rows, colWidths=[0.9*inch, 0.5*inch, 0.85*inch, 0.85*inch, 0.7*inch, 0.8*inch, 0.6*inch])
    rule_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 8),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (2, 0), (-1, -1), 'RIGHT'),
        ('ALIGN', (-1, 0), (-1, -1), 'CENTER'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('TOPPADDING', (0, 0), (-1, -1), 3),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 3),
    ]))
    elements.append(rule_table)

    # Violations narrative
    if violation_details:
        elements.append(Spacer(1, 6))
        elements.append(Paragraph("Violations this week:", styles['BodyText2']))
        for v in violation_details:
            elements.append(Paragraph(f"• {v}", styles['BodyText2']))
    else:
        elements.append(Spacer(1, 6))
        elements.append(Paragraph(f"Perfect week! No spending rule violations.", styles['BodyText2']))

    # Rules reminder
    elements.append(Spacer(1, 8))
    elements.append(Paragraph(
        "Rules: No eating out except Saturday ($40 max) · "
        "Groceries Sun $80 / Wed $50 · $0 fun money weekdays · $20 Fri+Sat combined · "
        "Anything over $50 must text Link first",
        styles['SmallText']))

    # ─── Category Breakdown (own page) ───────────────────────────
    elements.append(PageBreak())
    elements.append(Paragraph("📂 Spending by Category", styles['SectionHead']))

    week_cats = spending_by_category(data['txns_week'])
    month_cats = spending_by_category(data['txns_month'])

    if week_cats:
        cat_pie = make_category_pie(week_cats, "This Week's Spending")
        elements.append(Image(cat_pie, width=4.2*inch, height=3.2*inch))
        elements.append(Spacer(1, 6))

    all_cat_names = sorted(
        set(list(week_cats.keys()) + list(month_cats.keys())),
        key=lambda c: week_cats.get(c, 0),
        reverse=True
    )

    cat_rows = [['Category', 'This Week', 'Feb MTD']]
    for cat in all_cat_names[:15]:
        cat_rows.append([cat, f"${week_cats.get(cat, 0):,.2f}", f"${month_cats.get(cat, 0):,.2f}"])
    cat_rows.append(['TOTAL', f"${sum(week_cats.values()):,.2f}", f"${sum(month_cats.values()):,.2f}"])

    cat_table = Table(cat_rows, colWidths=[2.8*inch, 1.6*inch, 1.6*inch])
    cat_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTNAME', (0, -1), (-1, -1), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 9),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('BACKGROUND', (0, -1), (-1, -1), colors.HexColor(LIGHT_BG)),
        ('TOPPADDING', (0, 0), (-1, -1), 4),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
    ]))
    elements.append(cat_table)

    # ─── Monthly Comparison + Transactions (remaining pages) ───
    elements.append(PageBreak())
    elements.append(Paragraph("📈 Monthly Comparison", styles['SectionHead']))

    last_month_cats = spending_by_category(data['txns_last_month'])
    if month_cats and last_month_cats:
        comp_chart = make_month_comparison(month_cats, last_month_cats)
        elements.append(Image(comp_chart, width=5.5*inch, height=3.2*inch))

    elements.append(Spacer(1, 12))
    elements.append(Paragraph("📋 All Transactions This Week", styles['SectionHead']))

    txn_rows = [['Date', 'Merchant', 'Category', 'Amount']]
    sorted_txns = sorted(week_txns, key=lambda x: x.get("date", ""))

    for t in sorted_txns:
        amt = t["amount"]
        merchant = (t.get("merchant") or {}).get("name", "Unknown")[:28]
        cat = (t.get("category") or {}).get("name", "?")[:20]
        sign = "+" if amt > 0 else ""
        txn_rows.append([t["date"], merchant, cat, f"{sign}${abs(amt):,.2f}"])

    txn_table = Table(txn_rows, colWidths=[1*inch, 2.2*inch, 1.6*inch, 1.2*inch])
    txn_style = [
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 8),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (3, 0), (3, -1), 'RIGHT'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('TOPPADDING', (0, 0), (-1, -1), 3),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 3),
    ]
    for i, t in enumerate(sorted_txns, start=1):
        if t["amount"] < 0:
            txn_style.append(('TEXTCOLOR', (3, i), (3, i), colors.HexColor(RED)))
        else:
            txn_style.append(('TEXTCOLOR', (3, i), (3, i), colors.HexColor(GREEN)))

    txn_table.setStyle(TableStyle(txn_style))
    elements.append(txn_table)


Successfully replaced text in /Users/brandon/.openclaw/scripts/financial-report-pdf.py.

...
 605         daily_chart = make_daily_spending_bar(daily, "Daily Spending This Week")
 606         elements.append(Image(daily_chart, width=5.5*inch, height=2.8*inch))
 607         elements.append(Spacer(1, 4))
 608
+609     # ─── Compute spending rules data (used on this page and page 2) ──
+610     week_txns = data['txns_week']
+611     from datetime import datetime as dt
+612     days_data = defaultdict(lambda: {"eating_out": 0, "groceries": 0, "fun": 0, "total": 0, "big": []})
+613
+614     for t in week_txns:
+615         if t["amount"] >= 0: continue
+616         cat = (t.get("category") or {}).get("name", "").lower()
+617         merchant = (t.get("merchant") or {}).get("name", "Unknown")
+618         date = t["date"]
+619         amt = abs(t["amount"])
+620         days_data[date]["total"] += amt
+621         if cat in ("restaurants & bars", "fast food", "dining", "food & drink"):
+622             days_data[date]["eating_out"] += amt
+623         elif cat in ("groceries",):
+624             days_data[date]["groceries"] += amt
+625         elif cat in ("entertainment", "shopping", "hobbies"):
+626             days_data[date]["fun"] += amt
+627         if amt >= 50:
+628             days_data[date]["big"].append(f"{merchant} (${amt:,.0f})")
+629
+630     total_violations = 0
+631     violation_details = []
+632     clean_days = 0
+633     for date in sorted(days_data.keys()):
+634         d = days_data[date]
+635         day_obj = dt.strptime(date, "%Y-%m-%d")
+636         day_name = day_obj.strftime("%A")
+637
+638         day_violations = []
+639         if d["eating_out"] > 0 and day_name != "Saturday":
+640             day_violations.append(f"Ate out on {day_name} (${d['eating_out']:,.0f})")
+641         elif d["eating_out"] > 40 and day_name == "Saturday":
+642             day_violations.append(f"Saturday dinner over $40 (${d['eating_out']:,.0f})")
+643
+644         if d["groceries"] > 0:
+645             if day_name == "Sunday" and d["groceries"] > 80:
+646                 day_violations.append(f"Sunday groceries over $80 (${d['groceries']:,.0f})")
+647             elif day_name == "Wednesday" and d["groceries"] > 50:
+648                 day_violations.append(f"Wed restock over $50 (${d['groceries']:,.0f})")
+649             elif day_name not in ("Sunday", "Wednesday"):
+650                 day_violations.append(f"Off-schedule groceries on {day_name} (${d['groceries']:,.0f})")
+651
+652         if day_name in ("Monday", "Tuesday", "Wednesday", "Thursday") and d["fun"] > 0:
+653             day_violations.append(f"Weekday fun money (${d['fun']:,.0f})")
+654         elif day_name in ("Friday", "Saturday") and d["fun"] > 20:
+655             day_violations.append(f"Weekend fun over $20 (${d['fun']:,.0f})")
+656
+657         if day_violations:
+658             total_violations += 1
+659             for v in day_violations:
+660                 violation_details.append(f"{date} ({day_name[:3]}): {v}")
+661         else:
+662             clean_days += 1
+663
+664     compliance = (clean_days / len(days_data) * 100) if days_data else 100
+665
+666     # ─── Goals & Targets (on page 2 with cashflow) ────────────
+667     elements.append(Spacer(1, 8))
+668     elements.append(Paragraph("🎯 Goals & Targets", styles['SectionHead']))
+669
+670     debt = balances['credit_cards']
+671     next_milestone = (int(debt / 1000)) * 1000
+672     if next_milestone == int(debt):
+673         next_milestone -= 1000
+674     distance = debt - next_milestone
+675
+676     # Payoff projection
+677     if data.get('txns_last_month'):
+678         jan_income, jan_expenses = income_expenses(data['txns_last_month'])
+679         monthly_surplus = jan_income - jan_expenses
+680         if monthly_surplus > 0:
+681             months_to_payoff = debt / monthly_surplus
+682             payoff_date = datetime.now()
+683             for _ in range(int(months_to_payoff)):
+684                 if payoff_date.month == 12:
+685                     payoff_date = payoff_date.replace(year=payoff_date.year + 1, month=1)
+686                 else:
+687                     payoff_date = payoff_date.replace(month=payoff_date.month + 1)
+688             payoff_str = payoff_date.strftime("%B %Y")
+689         else:
+690             payoff_str = "N/A (spending exceeds income)"
+691     else:
+692         payoff_str = "Insufficient data"
+693
+694     # Goals table — expanded
+695     goals_data = [
+696         ['Goal', 'Current', 'Target', 'Progress'],
+697         ['Eliminate CC Debt', f'${debt:,.0f}', '$0',
+698          f'${distance:,.0f} to next milestone (${next_milestone:,})'],
+699         ['Projected Payoff', payoff_str, '—',
+700          'Based on last month\'s surplus'],
+701         ['Spending Rules', f'{compliance:.0f}%', '100%',
+702          f'{clean_days}/{len(days_data)} clean days this week'],
+703         ['Net Worth > $0', f'${net_worth:,.0f}',
+704          'Positive' if net_worth > 0 else '$0',
+705          '✅ Barely positive — fragile' if net_worth > 0 else '❌ Still underwater'],
+706         ['Emergency Fund', f'${balances["savings"]:,.0f}', '$1,000',
+707          f'{balances["savings"]/1000*100:.0f}% of target'],
+708     ]
+709
+710     goals_table = Table(goals_data, colWidths=[1.5*inch, 1.3*inch, 0.9*inch, 3.0*inch])
+711     goals_table.setStyle(TableStyle([
+712         ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
+713         ('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
+714         ('FONTSIZE', (0, 0), (-1, -1), 9),
+715         ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
+716         ('ALIGN', (1, 0), (2, -1), 'RIGHT'),
+717         ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
+718         ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
+719         ('TOPPADDING', (0, 0), (-1, -1), 5),
+720         ('BOTTOMPADDING', (0, 0), (-1, -1), 5),
+721         ('VALIGN', (0, 0), (-1, -1), 'TOP'),
+722     ]))
+723     elements.append(goals_table)
+724
+725     # Goals narrative
+726     goals_narrative_parts = []
+727     if debt_change is not None and debt_change > 0:
+728         goals_narrative_parts.append(f"Debt moved in the wrong direction this week — up ${abs(debt_change):,.0f}. "
+729                                      f"At the current pace, payoff is projected for {payoff_str}.")
+730     elif debt_change is not None and debt_change < -50:
+731         goals_narrative_parts.append(f"Good progress — debt dropped ${abs(debt_change):,.0f} this week. "
+732                                      f"Keep this up and payoff is projected for {payoff_str}.")
+733     else:
+734         goals_narrative_parts.append(f"Debt held flat. Projected payoff: {payoff_str}.")
+735
+736     if net_worth > 0 and net_worth < 500:
+737         goals_narrative_parts.append("Net worth is technically positive, but there's almost no buffer. "
+738                                      "One unexpected expense could put you back underwater.")
+739     elif net_worth <= 0:
+740         goals_narrative_parts.append(f"Net worth is negative — you owe ${abs(net_worth):,.0f} more than you own. "
+741                                      "Every dollar of debt paid changes this.")
+742
+743     if balances['savings'] < 100:
+744         goals_narrative_parts.append("Emergency fund is essentially empty. Even $50/month would start building a cushion.")
+745
+746     elements.append(Spacer(1, 4))
+747     elements.append(Paragraph(" ".join(goals_narrative_parts), styles['BodyText2']))
+748
+749     # ─── Spending Rules Compliance ─────────────────────────────
+750     elements.append(Spacer(1, 10))
+751     elements.append(Paragraph("✅ Spending Rules Compliance", styles['SectionHead']))
+752
+753     # Compliance score banner
+754     comp_color = GREEN if compliance >= 80 else AMBER if compliance >= 60 else RED
+755     score_text = f"{compliance:.0f}%"
+756     score_sub = f"  {clean_days} of {len(days_data)} days clean"
+757     elements.append(Paragraph(f"{score_text} — {score_sub}", styles['BodyText2']))
+758     elements.append(Spacer(1, 6))
+759
+760     # Day-by-day table with more detail
+761     rule_rows = [['Date', 'Day', 'Eating Out', 'Groceries', 'Fun $', 'Total', 'Status']]
+762     for date in sorted(days_data.keys()):
+763         d = days_data[date]
+764         day_obj = dt.strptime(date, "%Y-%m-%d")
+765         day_name = day_obj.strftime("%A")
+766
+767         # Determine status
+768         is_clean = True
+769         for v in violation_details:
+770             if v.startswith(date):
+771                 is_clean = False
+772                 break
+773         status = "✅" if is_clean else "⚠"
+774
+775         rule_rows.append([
+776             date, day_name[:3],
+777             f"${d['eating_out']:,.0f}" if d['eating_out'] > 0 else "—",
+778             f"${d['groceries']:,.0f}" if d['groceries'] > 0 else "—",
+779             f"${d['fun']:,.0f}" if d['fun'] > 0 else "—",
+780             f"${d['total']:,.0f}",
+781             status,
+782         ])
+783
+784     rule_table = Table(rule_rows, colWidths=[0.9*inch, 0.5*inch, 0.85*inch, 0.85*inch, 0.7*inch, 0.8*inch, 0.6*inch])
+785     rule_table.setStyle(TableStyle([
+786         ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
+787         ('FONTSIZE', (0, 0), (-1, -1), 8),
+788         ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
+789         ('ALIGN', (2, 0), (-1, -1), 'RIGHT'),
+790         ('ALIGN', (-1, 0), (-1, -1), 'CENTER'),
+791         ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
+792         ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
+793         ('TOPPADDING', (0, 0), (-1, -1), 3),
+794         ('BOTTOMPADDING', (0, 0), (-1, -1), 3),
+795     ]))
+796     elements.append(rule_table)
+797
+798     # Violations narrative
+799     if violation_details:
+800         elements.append(Spacer(1, 6))
+801         elements.append(Paragraph("Violations this week:", styles['BodyText2']))
+802         for v in violation_details:
+803             elements.append(Paragraph(f"• {v}", styles['BodyText2']))
+804     else:
+805         elements.append(Spacer(1, 6))
+806         elements.append(Paragraph(f"Perfect week! No spending rule violations.", styles['BodyText2']))
+807
+808     # Rules reminder
+809     elements.append(Spacer(1, 8))
+810     elements.append(Paragraph(
+811         "Rules: No eating out except Saturday ($40 max) · "
+812         "Groceries Sun $80 / Wed $50 · $0 fun money weekdays · $20 Fri+Sat combined · "
+813         "Anything over $50 must text Link first",
+814         styles['SmallText']))
+815
 609     # ─── Category Breakdown (own page) ───────────────────────────
 610     elements.append(PageBreak())
 611     elements.append(Paragraph("📂 Spending by Category", styles['SectionHead']))
 612
 613     week_cats = spending_by_category(data['txns_week'])
 614     month_cats = spending_by_category(data['txns_month'])
 615
-616     # Pie chart (compact)
 617     if week_cats:
 618         cat_pie = make_category_pie(week_cats, "This Week's Spending")
 619         elements.append(Image(cat_pie, width=4.2*inch, height=3.2*inch))
 620         elements.append(Spacer(1, 6))
 621
-622     # Category table — sorted by this week's spend descending, cap at 15 rows to fit page
-623     # Merge week + month categories, ordered by week spend
 624     all_cat_names = sorted(
 625         set(list(week_cats.keys()) + list(month_cats.keys())),
 626         key=lambda c: week_cats.get(c, 0),
 627         reverse=True
 628     )
 629
 630     cat_rows = [['Category', 'This Week', 'Feb MTD']]
 631     for cat in all_cat_names[:15]:
 632         cat_rows.append([cat, f"${week_cats.get(cat, 0):,.2f}", f"${month_cats.get(cat, 0):,.2f}"])
 633     cat_rows.append(['TOTAL', f"${sum(week_cats.values()):,.2f}", f"${sum(month_cats.values()):,.2f}"])
 634
 635     cat_table = Table(cat_rows, colWidths=[2.8*inch, 1.6*inch, 1.6*inch])
 636     cat_table.setStyle(TableStyle([
 637         ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
 638         ('FONTNAME', (0, -1), (-1, -1), 'Helvetica-Bold'),
 639         ('FONTSIZE', (0, 0), (-1, -1), 9),
 640         ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
 641         ('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
 642         ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
 643         ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
 644         ('BACKGROUND', (0, -1), (-1, -1), colors.HexColor(LIGHT_BG)),
 645         ('TOPPADDING', (0, 0), (-1, -1), 4),
 646         ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
 647     ]))
 648     elements.append(cat_table)
 649
-650     # ─── Month Comparison Chart ────────────────────────────────
+854     # ─── Monthly Comparison + Transactions (remaining pages) ───
 651     elements.append(PageBreak())
 652     elements.append(Paragraph("📈 Monthly Comparison", styles['SectionHead']))
 653
 654     last_month_cats = spending_by_category(data['txns_last_month'])
 655     if month_cats and last_month_cats:
 656         comp_chart = make_month_comparison(month_cats, last_month_cats)
 657         elements.append(Image(comp_chart, width=5.5*inch, height=3.2*inch))
 658
-659     # ─── Spending Rules Audit ──────────────────────────────────
-660     elements.append(Paragraph("✅ Spending Rules Compliance", styles['SectionHead']))
-661
-662     week_txns = data['txns_week']
-663
-664     # Check each day
-665     rule_rows = [['Date', 'Day', 'Eating Out', 'Groceries', 'Status']]
-666
-667     from datetime import datetime as dt
-668     days_data = defaultdict(lambda: {"eating_out": 0, "groceries": 0, "fun": 0, "total": 0})
-669
-670     for t in week_txns:
-671         if t["amount"] >= 0: continue
-672         cat = (t.get("category") or {}).get("name", "").lower()
-673         date = t["date"]
-674         amt = abs(t["amount"])
-675         days_data[date]["total"] += amt
-676         if cat in ("restaurants & bars", "fast food", "dining"): days_data[date]["eating_out"] += amt
-677         elif cat in ("groceries",): days_data[date]["groceries"] += amt
-678         elif cat in ("entertainment", "shopping", "hobbies"): days_data[date]["fun"] += amt
-679
-680     total_violations = 0
-681     for date in sorted(days_data.keys()):
-682         d = days_data[date]
-683         day_obj = dt.strptime(date, "%Y-%m-%d")
-684         day_name = day_obj.strftime("%A")
-685
-686         violations = []
-687         if d["eating_out"] > 0 and day_name != "Saturday":
-688             violations.append(f"Ate out (${d['eating_out']:.0f})")
-689         elif d["eating_out"] > 40 and day_name == "Saturday":
-690             violations.append(f"Over $40 dinner")
-691
-692         status = "✅" if not violations else "⚠ " + "; ".join(violations)
-693         if violations: total_violations += 1
-694
-695         rule_rows.append([
-696             date, day_name[:3],
-697             f"${d['eating_out']:,.0f}" if d['eating_out'] > 0 else "—",
-698             f"${d['groceries']:,.0f}" if d['groceries'] > 0 else "—",
-699             status
-700         ])
-701
-702     rule_table = Table(rule_rows, colWidths=[1.2*inch, 0.6*inch, 1*inch, 1*inch, 2.8*inch])
-703     rule_table.setStyle(TableStyle([
-704         ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
-705         ('FONTSIZE', (0, 0), (-1, -1), 9),
-706         ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
-707         ('ALIGN', (2, 0), (3, -1), 'RIGHT'),
-708         ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
-709         ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
-710         ('TOPPADDING', (0, 0), (-1, -1), 4),
-711         ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
-712     ]))
-713     elements.append(rule_table)
-714
-715     compliance = ((len(days_data) - total_violations) / len(days_data) * 100) if days_data else 100
-716     comp_color = GREEN if compliance >= 80 else AMBER if compliance >= 60 else RED
-717     elements.append(Spacer(1, 8))
-718     elements.append(Paragraph(
-719         f"Compliance Score: {compliance:.0f}% "
-720         f"({len(days_data) - total_violations}/{len(days_data)} clean days)",
-721         styles['BodyText2']))
-722
-723     # ─── All Transactions ──────────────────────────────────────
-724     elements.append(Spacer(1, 16))
+863     elements.append(Spacer(1, 12))
 725     elements.append(Paragraph("📋 All Transactions This Week", styles['SectionHead']))
 726
 727     txn_rows = [['Date', 'Merchant', 'Category', 'Amount']]
 728     sorted_txns = sorted(week_txns, key=lambda x: x.get("date", ""))
 729
 730     for t in sorted_txns:
 731         amt = t["amount"]
 732         merchant = (t.get("merchant") or {}).get("name", "Unknown")[:28]
 733         cat = (t.get("category") or {}).get("name", "?")[:20]
 734         sign = "+" if amt > 0 else ""
 735         txn_rows.append([t["date"], merchant, cat, f"{sign}${abs(amt):,.2f}"])
 736
 737     txn_table = Table(txn_rows, colWidths=[1*inch, 2.2*inch, 1.6*inch, 1.2*inch])
 738     txn_style = [
 739         ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
 740         ('FONTSIZE', (0, 0), (-1, -1), 8),
 741         ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
 742         ('ALIGN', (3, 0), (3, -1), 'RIGHT'),
 743         ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
 744         ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
 745         ('TOPPADDING', (0, 0), (-1, -1), 3),
 746         ('BOTTOMPADDING', (0, 0), (-1, -1), 3),
 747     ]
-748     # Color negative amounts red
 749     for i, t in enumerate(sorted_txns, start=1):
 750         if t["amount"] < 0:
 751             txn_style.append(('TEXTCOLOR', (3, i), (3, i), colors.HexColor(RED)))
 752         else:
 753             txn_style.append(('TEXTCOLOR', (3, i), (3, i), colors.HexColor(GREEN)))
 754
 755     txn_table.setStyle(TableStyle(txn_style))
 756     elements.append(txn_table)
 757
-758     # ─── Goals & Targets ───────────────────────────────────────
-759     elements.append(Spacer(1, 16))
-760     elements.append(Paragraph("🎯 Goals & Targets", styles['SectionHead']))
-761
-762     debt = balances['credit_cards']
-763     next_milestone = (int(debt / 1000)) * 1000
-764     distance = debt - next_milestone
-765
-766     # Payoff projection
-767     if data.get('txns_last_month'):
-768         jan_income, jan_expenses = income_expenses(data['txns_last_month'])
-769         monthly_surplus = jan_income - jan_expenses
-770         if monthly_surplus > 0:
-771             months_to_payoff = debt / monthly_surplus
-772             payoff_date = datetime.now()
-773             for _ in range(int(months_to_payoff)):
-774                 if payoff_date.month == 12:
-775                     payoff_date = payoff_date.replace(year=payoff_date.year + 1, month=1)
-776                 else:
-777                     payoff_date = payoff_date.replace(month=payoff_date.month + 1)
-778             payoff_str = payoff_date.strftime("%B %Y")
-779         else:
-780             payoff_str = "N/A (spending exceeds income)"
-781     else:
-782         payoff_str = "Insufficient data"
-783
-784     goals_data = [
-785         ['Goal', 'Status', 'Notes'],
-786         ['Eliminate CC Debt', f'${debt:,.0f} remaining', f'Next milestone: ${next_milestone:,} (${distance:,.0f} away)'],
-787         ['Projected Payoff', payoff_str, 'Based on current income vs expenses'],
-788         ['Follow Spending Rules', f'{compliance:.0f}% this week', f'{total_violations} violation(s)'],
-789         ['Net Worth > $0', f'${net_worth:,.0f}', '✅ Positive!' if net_worth > 0 else '❌ Still negative'],
-790         ['Emergency Fund', f'${balances["savings"]:,.0f}', 'Target: $1,000'],
-791     ]
-792
-793     goals_table = Table(goals_data, colWidths=[1.8*inch, 1.8*inch, 2.8*inch])
-794     goals_table.setStyle(TableStyle([
-795         ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
-796         ('FONTSIZE', (0, 0), (-1, -1), 9),
-797         ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
-798         ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
-799         ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
-800         ('TOPPADDING', (0, 0), (-1, -1), 5),
-801         ('BOTTOMPADDING', (0, 0), (-1, -1), 5),
-802         ('VALIGN', (0, 0), (-1, -1), 'TOP'),
-803     ]))
-804     elements.append(goals_table)
-805
 806     # ─── Footer ────────────────────────────────────────────────
 807     elements.append(Spacer(1, 24))
 808     elements.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor("#dee2e6")))
 809     elements.append(Spacer(1, 6))
     ...


exec

cd ~/.monarch-money && source venv/bin/activate && python3 ~/.openclaw/scripts/financial-report-pdf.py 2>&1


✅ Report generated: /Users/brandon/.openclaw/data/finance/reports/financial-report-2026-02-09-to-2026-02-15.pdf


(no output)


message

v6 — Page 2 is now Goals & Targets + Spending Rules Compliance, both fleshed out:

• Goals table has Current/Target/Progress columns + narrative about debt trajectory and net worth fragility
• Compliance shows big score banner, day-by-day breakdown with eating out/groceries/fun/total columns, violation callouts, and rules reminder at bottom

Categories on page 3, transactions on page 4.


you left the weekly cashflow on the second page. move it off of the second page. only goals and targets, and spending rules compliance on the second page. and they should never go past the second page.


else:
        narrative_parts.append(f"Net cashflow was ${net_flow:,.0f} — spent more than earned this week.")

    narrative = " ".join(narrative_parts)

    elements.append(Paragraph(narrative, styles['BodyText2']))
    elements.append(Spacer(1, 6))

    # ─── Page Break ────────────────────────────────────────────
    elements.append(PageBreak())

    # ─── Weekly Cashflow ───────────────────────────────────────
    elements.append(Paragraph("💰 Weekly Cashflow", styles['SectionHead']))

    month_income, month_expenses = income_expenses(data['txns_month'])
    change_text = f"{'↑' if expense_change_pct > 0 else '↓'} {abs(expense_change_pct):.0f}% vs prior week"
    change_color = RED if expense_change_pct > 0 else GREEN

    flow_data = [
        ['', 'This Week', 'Prior Week', 'February MTD'],
        ['Income', f"${week_income:,.2f}", f"${prev_income:,.2f}", f"${month_income:,.2f}"],
        ['Expenses', f"${week_expenses:,.2f}", f"${prev_expenses:,.2f}", f"${month_expenses:,.2f}"],
        ['Net', f"${week_income - week_expenses:,.2f}",
         f"${prev_income - prev_expenses:,.2f}",
         f"${month_income - month_expenses:,.2f}"],
    ]

    flow_table = Table(flow_data, colWidths=[1.2*inch, 1.8*inch, 1.8*inch, 1.8*inch])
    flow_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 10),
        ('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor("#fafafa")]),
        ('TOPPADDING', (0, 0), (-1, -1), 6),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
    ]))
    elements.append(flow_table)
    elements.append(Paragraph(f"{change_text}", styles['SmallText']))
    elements.append(Spacer(1, 8))

    # ─── Daily Spending Chart ──────────────────────────────────
    daily = spending_by_day(data['txns_week'])
    if daily:
        daily_chart = make_daily_spending_bar(daily, "Daily Spending This Week")
        elements.append(Image(daily_chart, width=5.5*inch, height=2.8*inch))
        elements.append(Spacer(1, 4))

[308 more lines in file. Use offset=608 to continue.]


I need to move the Weekly Cashflow + Daily Spending off page 2. Page 2 should start directly with Goals & Targets + Spending Rules. I'll move cashflow to a later page.

edit

# ─── Page Break ────────────────────────────────────────────
    elements.append(PageBreak())

    # ─── Weekly Cashflow ───────────────────────────────────────
    elements.append(Paragraph("💰 Weekly Cashflow", styles['SectionHead']))

    month_income, month_expenses = income_expenses(data['txns_month'])
    change_text = f"{'↑' if expense_change_pct > 0 else '↓'} {abs(expense_change_pct):.0f}% vs prior week"
    change_color = RED if expense_change_pct > 0 else GREEN

    flow_data = [
        ['', 'This Week', 'Prior Week', 'February MTD'],
        ['Income', f"${week_income:,.2f}", f"${prev_income:,.2f}", f"${month_income:,.2f}"],
        ['Expenses', f"${week_expenses:,.2f}", f"${prev_expenses:,.2f}", f"${month_expenses:,.2f}"],
        ['Net', f"${week_income - week_expenses:,.2f}",
         f"${prev_income - prev_expenses:,.2f}",
         f"${month_income - month_expenses:,.2f}"],
    ]

    flow_table = Table(flow_data, colWidths=[1.2*inch, 1.8*inch, 1.8*inch, 1.8*inch])
    flow_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 10),
        ('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor("#fafafa")]),
        ('TOPPADDING', (0, 0), (-1, -1), 6),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
    ]))
    elements.append(flow_table)
    elements.append(Paragraph(f"{change_text}", styles['SmallText']))
    elements.append(Spacer(1, 8))

    # ─── Daily Spending Chart ──────────────────────────────────
    daily = spending_by_day(data['txns_week'])
    if daily:
        daily_chart = make_daily_spending_bar(daily, "Daily Spending This Week")
        elements.append(Image(daily_chart, width=5.5*inch, height=2.8*inch))
        elements.append(Spacer(1, 4))

    # ─── Compute spending rules data (used on this page and page 2) ──

    # ─── Page 2: Goals & Targets + Spending Rules ───────────────
    elements.append(PageBreak())

    # ─── Compute spending rules data ─────────────────────────────


...
 562
 563     elements.append(Paragraph(narrative, styles['BodyText2']))
 564     elements.append(Spacer(1, 6))
 565
-566     # ─── Page Break ────────────────────────────────────────────
+566     # ─── Page 2: Goals & Targets + Spending Rules ───────────────
 567     elements.append(PageBreak())
 568
-569     # ─── Weekly Cashflow ───────────────────────────────────────
-570     elements.append(Paragraph("💰 Weekly Cashflow", styles['SectionHead']))
-571
-572     month_income, month_expenses = income_expenses(data['txns_month'])
-573     change_text = f"{'↑' if expense_change_pct > 0 else '↓'} {abs(expense_change_pct):.0f}% vs prior week"
-574     change_color = RED if expense_change_pct > 0 else GREEN
-575
-576     flow_data = [
-577         ['', 'This Week', 'Prior Week', 'February MTD'],
-578         ['Income', f"${week_income:,.2f}", f"${prev_income:,.2f}", f"${month_income:,.2f}"],
-579         ['Expenses', f"${week_expenses:,.2f}", f"${prev_expenses:,.2f}", f"${month_expenses:,.2f}"],
-580         ['Net', f"${week_income - week_expenses:,.2f}",
-581          f"${prev_income - prev_expenses:,.2f}",
-582          f"${month_income - month_expenses:,.2f}"],
-583     ]
-584
-585     flow_table = Table(flow_data, colWidths=[1.2*inch, 1.8*inch, 1.8*inch, 1.8*inch])
-586     flow_table.setStyle(TableStyle([
-587         ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
-588         ('FONTSIZE', (0, 0), (-1, -1), 10),
-589         ('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
-590         ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
-591         ('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
-592         ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
-593         ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
-594         ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor("#fafafa")]),
-595         ('TOPPADDING', (0, 0), (-1, -1), 6),
-596         ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
-597     ]))
-598     elements.append(flow_table)
-599     elements.append(Paragraph(f"{change_text}", styles['SmallText']))
-600     elements.append(Spacer(1, 8))
-601
-602     # ─── Daily Spending Chart ──────────────────────────────────
-603     daily = spending_by_day(data['txns_week'])
-604     if daily:
-605         daily_chart = make_daily_spending_bar(daily, "Daily Spending This Week")
-606         elements.append(Image(daily_chart, width=5.5*inch, height=2.8*inch))
-607         elements.append(Spacer(1, 4))
-608
-609     # ─── Compute spending rules data (used on this page and page 2) ──
+569     # ─── Compute spending rules data ─────────────────────────────
 610     week_txns = data['txns_week']
 611     from datetime import datetime as dt
 612     days_data = defaultdict(lambda: {"eating_out": 0, "groceries": 0, "fun": 0, "total": 0, "big": []})
 613
     ...


Now find where the category breakdown page break is and insert cashflow + daily spending before it:


goals_narrative_parts.append(f"Net worth is negative — you owe ${abs(net_worth):,.0f} more than you own. "
                                     "Every dollar of debt paid changes this.")

    if balances['savings'] < 100:
        goals_narrative_parts.append("Emergency fund is essentially empty. Even $50/month would start building a cushion.")

    elements.append(Spacer(1, 4))
    elements.append(Paragraph(" ".join(goals_narrative_parts), styles['BodyText2']))

    # ─── Spending Rules Compliance ─────────────────────────────
    elements.append(Spacer(1, 10))
    elements.append(Paragraph("✅ Spending Rules Compliance", styles['SectionHead']))

    # Compliance score banner
    comp_color = GREEN if compliance >= 80 else AMBER if compliance >= 60 else RED
    score_text = f"{compliance:.0f}%"
    score_sub = f"  {clean_days} of {len(days_data)} days clean"
    elements.append(Paragraph(f"{score_text} — {score_sub}", styles['BodyText2']))
    elements.append(Spacer(1, 6))

[156 more lines in file. Use offset=720 to continue.]


elements.append(Spacer(1, 6))
        elements.append(Paragraph("Violations this week:", styles['BodyText2']))
        for v in violation_details:
            elements.append(Paragraph(f"• {v}", styles['BodyText2']))
    else:
        elements.append(Spacer(1, 6))
        elements.append(Paragraph(f"Perfect week! No spending rule violations.", styles['BodyText2']))

    # Rules reminder
    elements.append(Spacer(1, 8))
    elements.append(Paragraph(
        "Rules: No eating out except Saturday ($40 max) · "
        "Groceries Sun $80 / Wed $50 · $0 fun money weekdays · $20 Fri+Sat combined · "
        "Anything over $50 must text Link first",
        styles['SmallText']))

    # ─── Category Breakdown (own page) ───────────────────────────
    elements.append(PageBreak())
    elements.append(Paragraph("📂 Spending by Category", styles['SectionHead']))

[96 more lines in file. Use offset=780 to continue.]


Now insert the Weekly Cashflow + Daily Spending as a new page between the rules reminder and the Category Breakdown:

# ─── Weekly Cashflow (own page) ──────────────────────────────
    elements.append(PageBreak())
    elements.append(Paragraph("💰 Weekly Cashflow", styles['SectionHead']))

    month_income, month_expenses = income_expenses(data['txns_month'])
    change_text = f"{'↑' if expense_change_pct > 0 else '↓'} {abs(expense_change_pct):.0f}% vs prior week"
    change_color = RED if expense_change_pct > 0 else GREEN

    flow_data = [
        ['', 'This Week', 'Prior Week', 'February MTD'],
        ['Income', f"${week_income:,.2f}", f"${prev_income:,.2f}", f"${month_income:,.2f}"],
        ['Expenses', f"${week_expenses:,.2f}", f"${prev_expenses:,.2f}", f"${month_expenses:,.2f}"],
        ['Net', f"${week_income - week_expenses:,.2f}",
         f"${prev_income - prev_expenses:,.2f}",
         f"${month_income - month_expenses:,.2f}"],
    ]

    flow_table = Table(flow_data, colWidths=[1.2*inch, 1.8*inch, 1.8*inch, 1.8*inch])
    flow_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 10),
        ('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor("#fafafa")]),
        ('TOPPADDING', (0, 0), (-1, -1), 6),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
    ]))
    elements.append(flow_table)
    elements.append(Paragraph(f"{change_text}", styles['SmallText']))
    elements.append(Spacer(1, 8))

    daily = spending_by_day(data['txns_week'])
    if daily:
        daily_chart = make_daily_spending_bar(daily, "Daily Spending This Week")
        elements.append(Image(daily_chart, width=5.5*inch, height=2.8*inch))
        elements.append(Spacer(1, 4))

    # ─── Category Breakdown (own page) ───────────────────────────
    elements.append(PageBreak())
    elements.append(Paragraph("📂 Spending by Category", styles['SectionHead']))


...
 772         "Groceries Sun $80 / Wed $50 · $0 fun money weekdays · $20 Fri+Sat combined · "
 773         "Anything over $50 must text Link first",
 774         styles['SmallText']))
 775
+776     # ─── Weekly Cashflow (own page) ──────────────────────────────
+777     elements.append(PageBreak())
+778     elements.append(Paragraph("💰 Weekly Cashflow", styles['SectionHead']))
+779
+780     month_income, month_expenses = income_expenses(data['txns_month'])
+781     change_text = f"{'↑' if expense_change_pct > 0 else '↓'} {abs(expense_change_pct):.0f}% vs prior week"
+782     change_color = RED if expense_change_pct > 0 else GREEN
+783
+784     flow_data = [
+785         ['', 'This Week', 'Prior Week', 'February MTD'],
+786         ['Income', f"${week_income:,.2f}", f"${prev_income:,.2f}", f"${month_income:,.2f}"],
+787         ['Expenses', f"${week_expenses:,.2f}", f"${prev_expenses:,.2f}", f"${month_expenses:,.2f}"],
+788         ['Net', f"${week_income - week_expenses:,.2f}",
+789          f"${prev_income - prev_expenses:,.2f}",
+790          f"${month_income - month_expenses:,.2f}"],
+791     ]
+792
+793     flow_table = Table(flow_data, colWidths=[1.2*inch, 1.8*inch, 1.8*inch, 1.8*inch])
+794     flow_table.setStyle(TableStyle([
+795         ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
+796         ('FONTSIZE', (0, 0), (-1, -1), 10),
+797         ('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
+798         ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
+799         ('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
+800         ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
+801         ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
+802         ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor("#fafafa")]),
+803         ('TOPPADDING', (0, 0), (-1, -1), 6),
+804         ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
+805     ]))
+806     elements.append(flow_table)
+807     elements.append(Paragraph(f"{change_text}", styles['SmallText']))
+808     elements.append(Spacer(1, 8))
+809
+810     daily = spending_by_day(data['txns_week'])
+811     if daily:
+812         daily_chart = make_daily_spending_bar(daily, "Daily Spending This Week")
+813         elements.append(Image(daily_chart, width=5.5*inch, height=2.8*inch))
+814         elements.append(Spacer(1, 4))
+815
 776     # ─── Category Breakdown (own page) ───────────────────────────
 777     elements.append(PageBreak())
 778     elements.append(Paragraph("📂 Spending by Category", styles['SectionHead']))
 779
     ...


message

v7 — Fixed. Page layout is now:
1. Account Summary (locked)
2. Goals & Targets + Spending Rules Compliance (only these two)
3. Weekly Cashflow + Daily Spending
4. Spending by Category
5. Monthly Comparison + Transactions


[media attached: /Users/brandon/.openclaw/media/inbound/035606a7-3f50-460d-968e-608d29d9e5e5.png (image/png) | /Users/brandon/.openclaw/media/inbound/035606a7-3f50-460d-968e-608d29d9e5e5.png]
To send an image back, prefer the message tool (media/path/filePath). If you must inline, use MEDIA:https://example.com/image.jpg (spaces ok, quote if needed) or a safe relative path like MEDIA:./image.jpg. Avoid absolute paths (MEDIA:/...) and ~ paths — they are blocked for security. Keep caption in the text body.
here is the second page you created. needs a little cleaning up, and we have some more room to play with.


Good bones. I see the issues:

1. **"Projected Payoff" row** — "N/A (spending exceeds income)" is bleeding into the Current column, looks messy
2. **Status column** showing black squares instead of ✅/⚠ (emoji rendering issue in reportlab)
3. **20% compliance** — the score is there but could be more visually impactful
4. **Lots of white space** at the bottom — room to add more context
5. **"N/A (spending exceeds income)"** in the goals narrative is bolded oddly

Let me fix these and use the extra space better — maybe add a visual progress bar for goals, use text symbols instead of emoji for status, and flesh out the compliance section.


violation_details.append(f"{date} ({day_name[:3]}): {v}")
        else:
            clean_days += 1

    compliance = (clean_days / len(days_data) * 100) if days_data else 100

    # ─── Goals & Targets (on page 2 with cashflow) ────────────
    elements.append(Spacer(1, 8))
    elements.append(Paragraph("🎯 Goals & Targets", styles['SectionHead']))

    debt = balances['credit_cards']
    next_milestone = (int(debt / 1000)) * 1000
    if next_milestone == int(debt):
        next_milestone -= 1000
    distance = debt - next_milestone

    # Payoff projection
    if data.get('txns_last_month'):
        jan_income, jan_expenses = income_expenses(data['txns_last_month'])
        monthly_surplus = jan_income - jan_expenses
        if monthly_surplus > 0:
            months_to_payoff = debt / monthly_surplus
            payoff_date = datetime.now()
            for _ in range(int(months_to_payoff)):
                if payoff_date.month == 12:
                    payoff_date = payoff_date.replace(year=payoff_date.year + 1, month=1)
                else:
                    payoff_date = payoff_date.replace(month=payoff_date.month + 1)
            payoff_str = payoff_date.strftime("%B %Y")
        else:
            payoff_str = "N/A (spending exceeds income)"
    else:
        payoff_str = "Insufficient data"

    # Goals table — expanded
    goals_data = [
        ['Goal', 'Current', 'Target', 'Progress'],
        ['Eliminate CC Debt', f'${debt:,.0f}', '$0',
         f'${distance:,.0f} to next milestone (${next_milestone:,})'],
        ['Projected Payoff', payoff_str, '—',
         'Based on last month\'s surplus'],
        ['Spending Rules', f'{compliance:.0f}%', '100%',
         f'{clean_days}/{len(days_data)} clean days this week'],
        ['Net Worth > $0', f'${net_worth:,.0f}',
         'Positive' if net_worth > 0 else '$0',
         '✅ Barely positive — fragile' if net_worth > 0 else '❌ Still underwater'],
        ['Emergency Fund', f'${balances["savings"]:,.0f}', '$1,000',
         f'{balances["savings"]/1000*100:.0f}% of target'],
    ]

    goals_table = Table(goals_data, colWidths=[1.5*inch, 1.3*inch, 0.9*inch, 3.0*inch])
    goals_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 9),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (1, 0), (2, -1), 'RIGHT'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('TOPPADDING', (0, 0), (-1, -1), 5),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 5),
        ('VALIGN', (0, 0), (-1, -1), 'TOP'),
    ]))
    elements.append(goals_table)

    # Goals narrative
    goals_narrative_parts = []
    if debt_change is not None and debt_change > 0:
        goals_narrative_parts.append(f"Debt moved in the wrong direction this week — up ${abs(debt_change):,.0f}. "
                                     f"At the current pace, payoff is projected for {payoff_str}.")
    elif debt_change is not None and debt_change < -50:
        goals_narrative_parts.append(f"Good progress — debt dropped ${abs(debt_change):,.0f} this week. "
                                     f"Keep this up and payoff is projected for {payoff_str}.")
    else:
        goals_narrative_parts.append(f"Debt held flat. Projected payoff: {payoff_str}.")

    if net_worth > 0 and net_worth < 500:
        goals_narrative_parts.append("Net worth is technically positive, but there's almost no buffer. "
                                     "One unexpected expense could put you back underwater.")
    elif net_worth <= 0:
        goals_narrative_parts.append(f"Net worth is negative — you owe ${abs(net_worth):,.0f} more than you own. "
                                     "Every dollar of debt paid changes this.")

    if balances['savings'] < 100:
        goals_narrative_parts.append("Emergency fund is essentially empty. Even $50/month would start building a cushion.")

    elements.append(Spacer(1, 4))
    elements.append(Paragraph(" ".join(goals_narrative_parts), styles['BodyText2']))

    # ─── Spending Rules Compliance ─────────────────────────────
    elements.append(Spacer(1, 10))
    elements.append(Paragraph("✅ Spending Rules Compliance", styles['SectionHead']))

    # Compliance score banner
    comp_color = GREEN if compliance >= 80 else AMBER if compliance >= 60 else RED
    score_text = f"{compliance:.0f}%"
    score_sub = f"  {clean_days} of {len(days_data)} days clean"
    elements.append(Paragraph(f"{score_text} — {score_sub}", styles['BodyText2']))
    elements.append(Spacer(1, 6))

    # Day-by-day table with more detail
    rule_rows = [['Date', 'Day', 'Eating Out', 'Groceries', 'Fun $', 'Total', 'Status']]
    for date in sorted(days_data.keys()):
        d = days_data[date]
        day_obj = dt.strptime(date, "%Y-%m-%d")
        day_name = day_obj.strftime("%A")

        # Determine status
        is_clean = True
        for v in violation_details:
            if v.startswith(date):
                is_clean = False
                break
        status = "✅" if is_clean else "⚠"

        rule_rows.append([
            date, day_name[:3],
            f"${d['eating_out']:,.0f}" if d['eating_out'] > 0 else "—",
            f"${d['groceries']:,.0f}" if d['groceries'] > 0 else "—",
            f"${d['fun']:,.0f}" if d['fun'] > 0 else "—",
            f"${d['total']:,.0f}",
            status,
        ])

    rule_table = Table(rule_rows, colWidths=[0.9*inch, 0.5*inch, 0.85*inch, 0.85*inch, 0.7*inch, 0.8*inch, 0.6*inch])
    rule_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 8),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (2, 0), (-1, -1), 'RIGHT'),
        ('ALIGN', (-1, 0), (-1, -1), 'CENTER'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('TOPPADDING', (0, 0), (-1, -1), 3),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 3),
    ]))
    elements.append(rule_table)

    # Violations narrative
    if violation_details:
        elements.append(Spacer(1, 6))
        elements.append(Paragraph("Violations this week:", styles['BodyText2']))
        for v in violation_details:
            elements.append(Paragraph(f"• {v}", styles['BodyText2']))
    else:
        elements.append(Spacer(1, 6))
        elements.append(Paragraph(f"Perfect week! No spending rule violations.", styles['BodyText2']))

    # Rules reminder
    elements.append(Spacer(1, 8))
    elements.append(Paragraph(
        "Rules: No eating out except Saturday ($40 max) · "
        "Groceries Sun $80 / Wed $50 · $0 fun money weekdays · $20 Fri+Sat combined · "
        "Anything over $50 must text Link first",
        styles['SmallText']))

    # ─── Weekly Cashflow (own page) ──────────────────────────────
    elements.append(PageBreak())
    elements.append(Paragraph("💰 Weekly Cashflow", styles['SectionHead']))

[136 more lines in file. Use offset=780 to continue.]


Now I'll replace the entire Goals + Spending Rules section with cleaned up versions:

edit

# ─── Goals & Targets (on page 2 with cashflow) ────────────
    elements.append(Spacer(1, 8))
    elements.append(Paragraph("🎯 Goals & Targets", styles['SectionHead']))

    debt = balances['credit_cards']
    next_milestone = (int(debt / 1000)) * 1000
    if next_milestone == int(debt):
        next_milestone -= 1000
    distance = debt - next_milestone

    # Payoff projection
    if data.get('txns_last_month'):
        jan_income, jan_expenses = income_expenses(data['txns_last_month'])
        monthly_surplus = jan_income - jan_expenses
        if monthly_surplus > 0:
            months_to_payoff = debt / monthly_surplus
            payoff_date = datetime.now()
            for _ in range(int(months_to_payoff)):
                if payoff_date.month == 12:
                    payoff_date = payoff_date.replace(year=payoff_date.year + 1, month=1)
                else:
                    payoff_date = payoff_date.replace(month=payoff_date.month + 1)
            payoff_str = payoff_date.strftime("%B %Y")
        else:
            payoff_str = "N/A (spending exceeds income)"
    else:
        payoff_str = "Insufficient data"

    # Goals table — expanded
    goals_data = [
        ['Goal', 'Current', 'Target', 'Progress'],
        ['Eliminate CC Debt', f'${debt:,.0f}', '$0',
         f'${distance:,.0f} to next milestone (${next_milestone:,})'],
        ['Projected Payoff', payoff_str, '—',
         'Based on last month\'s surplus'],
        ['Spending Rules', f'{compliance:.0f}%', '100%',
         f'{clean_days}/{len(days_data)} clean days this week'],
        ['Net Worth > $0', f'${net_worth:,.0f}',
         'Positive' if net_worth > 0 else '$0',
         '✅ Barely positive — fragile' if net_worth > 0 else '❌ Still underwater'],
        ['Emergency Fund', f'${balances["savings"]:,.0f}', '$1,000',
         f'{balances["savings"]/1000*100:.0f}% of target'],
    ]

    goals_table = Table(goals_data, colWidths=[1.5*inch, 1.3*inch, 0.9*inch, 3.0*inch])
    goals_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 9),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (1, 0), (2, -1), 'RIGHT'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('TOPPADDING', (0, 0), (-1, -1), 5),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 5),
        ('VALIGN', (0, 0), (-1, -1), 'TOP'),
    ]))
    elements.append(goals_table)

    # Goals narrative
    goals_narrative_parts = []
    if debt_change is not None and debt_change > 0:
        goals_narrative_parts.append(f"Debt moved in the wrong direction this week — up ${abs(debt_change):,.0f}. "
                                     f"At the current pace, payoff is projected for {payoff_str}.")
    elif debt_change is not None and debt_change < -50:
        goals_narrative_parts.append(f"Good progress — debt dropped ${abs(debt_change):,.0f} this week. "
                                     f"Keep this up and payoff is projected for {payoff_str}.")
    else:
        goals_narrative_parts.append(f"Debt held flat. Projected payoff: {payoff_str}.")

    if net_worth > 0 and net_worth < 500:
        goals_narrative_parts.append("Net worth is technically positive, but there's almost no buffer. "
                                     "One unexpected expense could put you back underwater.")
    elif net_worth <= 0:
        goals_narrative_parts.append(f"Net worth is negative — you owe ${abs(net_worth):,.0f} more than you own. "
                                     "Every dollar of debt paid changes this.")

    if balances['savings'] < 100:
        goals_narrative_parts.append("Emergency fund is essentially empty. Even $50/month would start building a cushion.")

    elements.append(Spacer(1, 4))
    elements.append(Paragraph(" ".join(goals_narrative_parts), styles['BodyText2']))

    # ─── Spending Rules Compliance ─────────────────────────────
    elements.append(Spacer(1, 10))
    elements.append(Paragraph("✅ Spending Rules Compliance", styles['SectionHead']))

    # Compliance score banner
    comp_color = GREEN if compliance >= 80 else AMBER if compliance >= 60 else RED
    score_text = f"{compliance:.0f}%"
    score_sub = f"  {clean_days} of {len(days_data)} days clean"
    elements.append(Paragraph(f"{score_text} — {score_sub}", styles['BodyText2']))
    elements.append(Spacer(1, 6))

    # Day-by-day table with more detail
    rule_rows = [['Date', 'Day', 'Eating Out', 'Groceries', 'Fun $', 'Total', 'Status']]
    for date in sorted(days_data.keys()):
        d = days_data[date]
        day_obj = dt.strptime(date, "%Y-%m-%d")
        day_name = day_obj.strftime("%A")

        # Determine status
        is_clean = True
        for v in violation_details:
            if v.startswith(date):
                is_clean = False
                break
        status = "✅" if is_clean else "⚠"

        rule_rows.append([
            date, day_name[:3],
            f"${d['eating_out']:,.0f}" if d['eating_out'] > 0 else "—",
            f"${d['groceries']:,.0f}" if d['groceries'] > 0 else "—",
            f"${d['fun']:,.0f}" if d['fun'] > 0 else "—",
            f"${d['total']:,.0f}",
            status,
        ])

    rule_table = Table(rule_rows, colWidths=[0.9*inch, 0.5*inch, 0.85*inch, 0.85*inch, 0.7*inch, 0.8*inch, 0.6*inch])
    rule_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 8),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (2, 0), (-1, -1), 'RIGHT'),
        ('ALIGN', (-1, 0), (-1, -1), 'CENTER'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('TOPPADDING', (0, 0), (-1, -1), 3),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 3),
    ]))
    elements.append(rule_table)

    # Violations narrative
    if violation_details:
        elements.append(Spacer(1, 6))
        elements.append(Paragraph("Violations this week:", styles['BodyText2']))
        for v in violation_details:
            elements.append(Paragraph(f"• {v}", styles['BodyText2']))
    else:
        elements.append(Spacer(1, 6))
        elements.append(Paragraph(f"Perfect week! No spending rule violations.", styles['BodyText2']))

    # Rules reminder
    elements.append(Spacer(1, 8))
    elements.append(Paragraph(
        "Rules: No eating out except Saturday ($40 max) · "
        "Groceries Sun $80 / Wed $50 · $0 fun money weekdays · $20 Fri+Sat combined · "
        "Anything over $50 must text Link first",
        styles['SmallText']))

# ═══════════════════════════════════════════════════════════
    # PAGE 2: Goals & Targets + Spending Rules Compliance
    # ═══════════════════════════════════════════════════════════

    debt = balances['credit_cards']
    next_milestone = (int(debt / 1000)) * 1000
    if next_milestone == int(debt):
        next_milestone -= 1000
    distance = debt - next_milestone

    # Payoff projection
    payoff_str = "Insufficient data"
    if data.get('txns_last_month'):
        jan_income, jan_expenses = income_expenses(data['txns_last_month'])
        monthly_surplus = jan_income - jan_expenses
        if monthly_surplus > 0:
            months_to_payoff = debt / monthly_surplus
            payoff_date = datetime.now()
            for _ in range(int(months_to_payoff)):
                if payoff_date.month == 12:
                    payoff_date = payoff_date.replace(year=payoff_date.year + 1, month=1)
                else:
                    payoff_date = payoff_date.replace(month=payoff_date.month + 1)
            payoff_str = payoff_date.strftime("%B %Y")
        else:
            payoff_str = "Not yet (spending > income)"

    # ─── Goals & Targets ───────────────────────────────────────
    elements.append(Paragraph("Goals & Targets", styles['SectionHead']))

    # Progress indicators using text (no emoji — reportlab can't render them)
    nw_status = "PASS — barely" if net_worth > 0 else "FAIL"
    nw_indicator = f'{nw_status}' if net_worth > 0 else f'{nw_status}'
    savings_pct = min(balances["savings"] / 1000 * 100, 100)

    goals_data = [
        ['Goal', 'Current', 'Target', 'Progress'],
        ['Eliminate CC Debt', f'${debt:,.0f}', '$0',
         f'${distance:,.0f} until ${next_milestone:,} milestone'],
        ['Projected Payoff', payoff_str, 'Debt-free',
         f'Based on last month surplus (${monthly_surplus:+,.0f}/mo)' if data.get('txns_last_month') and monthly_surplus else 'Need positive cashflow first'],
        ['Spending Rules', f'{compliance:.0f}%', '100%',
         f'{clean_days} of {len(days_data)} clean days'],
        ['Net Worth > $0', f'${net_worth:,.0f}', 'Positive', ''],
        ['Emergency Fund', f'${balances["savings"]:,.0f}', '$1,000',
         f'{savings_pct:.0f}% of target'],
    ]

    goals_table = Table(goals_data, colWidths=[1.5*inch, 1.4*inch, 1.0*inch, 2.8*inch])
    goals_style = [
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 9),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (1, 0), (2, -1), 'RIGHT'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('TOPPADDING', (0, 0), (-1, -1), 5),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 5),
        ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
    ]
    # Color-code compliance row
    comp_color_obj = colors.HexColor(GREEN) if compliance >= 80 else colors.HexColor(AMBER) if compliance >= 60 else colors.HexColor(RED)
    goals_style.append(('TEXTCOLOR', (1, 3), (1, 3), comp_color_obj))
    # Color-code net worth
    goals_style.append(('TEXTCOLOR', (1, 4), (1, 4), colors.HexColor(GREEN) if net_worth > 0 else colors.HexColor(RED)))
    # Color-code emergency fund
    goals_style.append(('TEXTCOLOR', (1, 5), (1, 5), colors.HexColor(RED) if balances['savings'] < 100 else colors.HexColor(AMBER)))

    goals_table.setStyle(TableStyle(goals_style))
    elements.append(goals_table)
    elements.append(Spacer(1, 6))

    # Goals narrative
    narr = []
    if debt_change is not None and debt_change > 0:
        narr.append(f"Debt moved the wrong direction this week, up ${abs(debt_change):,.0f}.")
    elif debt_change is not None and debt_change < -50:
        narr.append(f"Debt dropped ${abs(debt_change):,.0f} this week.")
    else:
        narr.append("Debt held roughly flat this week.")

    if payoff_str and 'Not yet' not in payoff_str and 'Insufficient' not in payoff_str:
        narr.append(f"At last month's pace, projected payoff: {payoff_str}.")
    else:
        narr.append("Spending currently exceeds income — no payoff projection available until cashflow turns positive.")

    if net_worth > 0 and net_worth < 500:
        narr.append(f"Net worth is positive at ${net_worth:,.0f}, but razor-thin. One bad week erases it.")
    elif net_worth <= 0:
        narr.append(f"Net worth is -${abs(net_worth):,.0f}. Still underwater.")

    if balances['savings'] < 100:
        narr.append(f"Emergency fund: ${balances['savings']:,.0f}. "
                     "Essentially zero cushion. Even $25/week builds a buffer.")

    elements.append(Paragraph(" ".join(narr), styles['BodyText2']))

    # ─── Spending Rules Compliance ─────────────────────────────
    elements.append(Spacer(1, 12))
    elements.append(Paragraph("Spending Rules Compliance", styles['SectionHead']))

    # Score banner — large, color-coded
    comp_color = GREEN if compliance >= 80 else AMBER if compliance >= 60 else RED
    if compliance >= 80:
        grade = "A"
    elif compliance >= 60:
        grade = "C"
    elif compliance >= 40:
        grade = "D"
    else:
        grade = "F"

    score_line = (f"{compliance:.0f}%"
                  f"  Grade: "
                  f"{grade}"
                  f"    ({clean_days} of {len(days_data)} days clean)")
    elements.append(Paragraph(score_line, styles['BodyText2']))
    elements.append(Spacer(1, 8))

    # Day-by-day table
    rule_rows = [['Date', 'Day', 'Eating Out', 'Groceries', 'Fun $', 'Total', 'Status']]
    for date in sorted(days_data.keys()):
        d = days_data[date]
        day_obj = dt.strptime(date, "%Y-%m-%d")
        day_name = day_obj.strftime("%A")

        is_clean = not any(v.startswith(date) for v in violation_details)
        status = "PASS" if is_clean else "FAIL"

        rule_rows.append([
            date, day_name[:3],
            f"${d['eating_out']:,.0f}" if d['eating_out'] > 0 else "—",
            f"${d['groceries']:,.0f}" if d['groceries'] > 0 else "—",
            f"${d['fun']:,.0f}" if d['fun'] > 0 else "—",
            f"${d['total']:,.0f}",
            status,
        ])

    rule_table = Table(rule_rows, colWidths=[0.95*inch, 0.5*inch, 0.85*inch, 0.85*inch, 0.65*inch, 0.8*inch, 0.6*inch])
    rule_style = [
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 8),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor(ACCENT)),
        ('ALIGN', (2, 0), (-1, -1), 'RIGHT'),
        ('ALIGN', (-1, 0), (-1, -1), 'CENTER'),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(LIGHT_BG)),
        ('TOPPADDING', (0, 0), (-1, -1), 4),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
    ]
    # Color the status column per row
    for i, date in enumerate(sorted(days_data.keys()), start=1):
        is_clean = not any(v.startswith(date) for v in violation_details)
        if is_clean:
            rule_style.append(('TEXTCOLOR', (-1, i), (-1, i), colors.HexColor(GREEN)))
            rule_style.append(('FONTNAME', (-1, i), (-1, i), 'Helvetica-Bold'))
        else:
            rule_style.append(('TEXTCOLOR', (-1, i), (-1, i), colors.HexColor(RED)))
            rule_style.append(('FONTNAME', (-1, i), (-1, i), 'Helvetica-Bold'))
            rule_style.append(('BACKGROUND', (0, i), (-1, i), colors.HexColor("#fff5f5")))

    rule_table.setStyle(TableStyle(rule_style))
    elements.append(rule_table)
    elements.append(Spacer(1, 6))

    # Violations detail
    if violation_details:
        elements.append(Paragraph("Violations:", styles['BodyText2']))
        for v in violation_details:
            elements.append(Paragraph(f"• {v}", styles['BodyText2']))
    else:
        elements.append(Paragraph(f"Perfect week! No spending rule violations.", styles['BodyText2']))

    # Rules reference box
    elements.append(Spacer(1, 10))
    rules_box_data = [
        ['Your Spending Rules'],
        ['No eating out except Saturday ($40 max)'],
        ['Groceries: Sunday $80 / Wednesday $50'],
        ['$0 fun money on weekdays'],
        ['$20 fun money Fri + Sat combined'],
        ['Anything over $50 — text Link first'],
    ]
    rules_box = Table(rules_box_data, colWidths=[5.5*inch])
    rules_box.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (0, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 8),
        ('TEXTCOLOR', (0, 0), (0, 0), colors.HexColor(DARK)),
        ('TEXTCOLOR', (0, 1), (-1, -1), colors.HexColor("#636e72")),
        ('BACKGROUND', (0, 0), (-1, -1), colors.HexColor("#f0f4f8")),
        ('BOX', (0, 0), (-1, -1), 0.5, colors.HexColor("#dee2e6")),
        ('TOPPADDING', (0, 0), (-1, -1), 3),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 3),
        ('LEFTPADDING', (0, 0), (-1, -1), 10),
    ]))
    elements.append(rules_box)