diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..d0088dd
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,230 @@
+# PMPro Donations — Agent Instructions
+
+> Paid Memberships Pro add-on for accepting donations at checkout. This file is the primary instruction reference for any AI agent working in this repo.
+
+## What This Is
+
+PMPro Donations adds donation functionality to the Paid Memberships Pro checkout flow. Members can add a one-time donation on top of their membership fee, or donate without a membership via "donation-only" levels.
+
+- **Plugin Slug**: `pmpro-donations`
+- **Current Version**: 2.2
+- **Requires**: Paid Memberships Pro (core plugin)
+- **WordPress Tested Up To**: 6.8
+- **PHP**: 7.4+ (supports up to 8.5)
+- **Text Domain**: `pmpro-donations`
+- **License**: GPL-2.0+
+- **Repo**: https://github.com/strangerstudios/pmpro-donations
+
+## Repo Layout
+
+```
+pmpro-donations/
+├── pmpro-donations.php # Main plugin file (bootstrap, includes, meta links)
+├── includes/
+│ ├── common.php # Shared utilities (get settings, get price components)
+│ ├── checkout.php # Checkout flow: form display, validation, price modification
+│ ├── donation-only-level.php # Donation-only level logic (keep member's existing level)
+│ ├── level-settings.php # Admin: level edit page donation settings UI
+│ └── admin.php # Admin: order editing, CSV export column
+├── languages/
+│ ├── pmpro-donations.pot # Translation template
+│ ├── pmpro-donations.po # Translation source
+│ ├── pmpro-donations.mo # Compiled translations
+│ └── gettext.sh # Translation extraction script
+├── readme.txt # WordPress.org plugin readme
+├── readme.md # GitHub readme
+├── AGENTS.md # This file
+├── PLAN.md # Current roadmap and next steps
+└── .github/ # GitHub templates and workflows
+```
+
+**Key fact**: This is a lightweight plugin (~600 lines of PHP across 5 files). No standalone JavaScript or CSS files — all JS is inline in PHP, all styling uses PMPro core CSS classes.
+
+## Architecture
+
+### Checkout Flow (Execution Order)
+
+When a user checks out on a level with donations enabled:
+
+| # | Hook | Function | File | Purpose |
+|---|------|----------|------|---------|
+| 1 | `pmpro_checkout_preheader_before_get_level_at_checkout` (pri 1) | `pmprodon_init_dropdown_values()` | checkout.php | Copy dropdown selection to `$_REQUEST['donation']` |
+| 2 | `pmpro_checkout_preheader_before_get_level_at_checkout` (pri 10) | `pmprodon_ppe_add_donation_to_request()` | checkout.php | PayPal Express: restore donation from order meta on return |
+| 3 | `pmpro_checkout_preheader` | `pmprodon_pmpro_checkout_preheader()` | checkout.php | Force SSL for donation levels |
+| 4 | `pmpro_checkout_before_form` | `pmprodon_hook_pmpro_level_cost_text()` | checkout.php | Add cost text filter |
+| 5 | `pmpro_checkout_after_user_fields` | `pmprodon_pmpro_checkout_after_user_fields()` | checkout.php | **Render donation form UI** (dropdown or text input) |
+| 6 | `pmpro_checkout_level` (pri 99) | `pmprodon_pmpro_checkout_level()` | checkout.php | **Add donation to `$level->initial_payment`** |
+| 7 | `pmpro_checkout_after_level_cost` | `pmprodon_unhook_pmpro_level_cost_text()` | checkout.php | Remove cost text filter |
+| 8 | `pmpro_registration_checks` | `pmprodon_pmpro_registration_checks()` | checkout.php | **Validate** donation (min, max, negative) |
+| 9 | `pmpro_checkout_before_change_membership_level` (pri 1) | `pmprodon_pmpro_checkout_before_change_membership_level()` | donation-only-level.php | Donation-only: prevent level switching |
+| 10 | `pmpro_after_checkout` | `pmprodon_store_donation_amount_in_order_meta()` | checkout.php | **Save donation to order meta** |
+| 11 | `pmpro_after_checkout` | `pmprodon_pmpro_after_checkout()` | donation-only-level.php | Donation-only: restore member's previous level |
+
+### Data Storage
+
+**Donation Settings** (per level):
+```php
+// Option: pmprodon_{level_id}
+get_option( 'pmprodon_' . $level_id );
+// Returns:
+array(
+ 'donations' => 0|1, // Enable donations
+ 'donations_only' => 0|1, // Donation-only level
+ 'min_price' => '5.00', // Minimum donation
+ 'max_price' => '500.00', // Maximum donation
+ 'dropdown_prices' => '10,25,50,other', // Comma-separated
+ 'text' => '
Help text HTML
',
+ 'confirmation_message' => '
Thank you HTML
'
+)
+```
+
+**Donation Amount** (per order):
+```php
+// Primary (v2.0+): Order meta
+update_pmpro_membership_order_meta( $order->id, 'donation_amount', $amount );
+get_pmpro_membership_order_meta( $order->id, 'donation_amount', true );
+
+// Legacy (v1.x): Order notes — auto-migrated on first access
+// Pattern: "Donation: 25.00" in $order->notes
+```
+
+### Key Utility Functions
+
+| Function | File | Purpose |
+|----------|------|---------|
+| `pmprodon_get_level_settings( $level_id )` | common.php | Get donation settings for a level (returns array with defaults) |
+| `pmprodon_is_donations_only( $level_id )` | common.php | Check if level is donation-only |
+| `pmprodon_get_price_components( $order )` | common.php | Split order total into `['price' => float, 'donation' => float]` |
+
+### Filters for Customization
+
+| Filter | Purpose |
+|--------|---------|
+| `pmpro_donations_get_price_components` | Customize how order total is split into price + donation |
+| `pmpro_donations_invoice_bullets` | Customize invoice bullet points |
+
+### Email Integration
+
+- **Variable**: `!!donation!!` — replaced with formatted donation amount in all checkout emails
+- **Auto-injection**: If email template doesn't use `!!donation!!`, donation info is injected before the "Invoice" section
+- **Hook**: `pmpro_email_data` and `pmpro_email_filter`
+
+### Donation-Only Levels
+
+Special levels where existing members can donate without losing their current membership:
+
+1. Admin enables "Donations-Only Level" checkbox in level settings
+2. At checkout, plugin prevents cancellation of existing subscriptions (`pmpro_cancel_previous_subscriptions` → `__return_false`)
+3. After checkout, the donation-only level row is removed from `pmpro_memberships_users`
+4. User keeps their original level; donation is recorded on the order
+
+### Inline JavaScript
+
+All JS lives in two places:
+
+**checkout.php** (lines 124-204):
+- `pmprodon_toggleOther()` — show/hide custom amount input when dropdown changes
+- `pmprodon_checkForFree()` — show/hide billing fields based on donation amount and gateway
+- Event listeners on `#donation_dropdown`, `#donation`, and `input[name=gateway]`
+
+**level-settings.php** (lines 85-104):
+- `toggleDonFields()` — show/hide admin settings based on "Enable Donations" checkbox
+
+### CSS Classes Used (from PMPro core)
+
+The plugin uses PMPro's built-in CSS classes — no custom stylesheet:
+- `pmpro_form_fieldset`, `pmpro_card`, `pmpro_card_content`
+- `pmpro_form_field-donation`, `pmpro_form_input-select`, `pmpro_form_input-text`
+- `pmpro_alter_price` — triggers PMPro's price change detection (v2.0+)
+
+## Development Setup
+
+### Local WordPress Environment
+
+This plugin requires an active WordPress installation with Paid Memberships Pro:
+
+```bash
+# Clone into wp-content/plugins/
+cd /path/to/wordpress/wp-content/plugins/
+git clone https://github.com/strangerstudios/pmpro-donations.git
+
+# Or symlink from repos dir
+ln -s ~/repos/strangerstudios/pmpro-donations /path/to/wordpress/wp-content/plugins/pmpro-donations
+
+# Activate via WP CLI
+wp plugin activate pmpro-donations
+```
+
+### Testing Donations
+
+1. Create a membership level in PMPro (Memberships > Settings > Levels)
+2. Edit the level, scroll to "Donation Settings"
+3. Check "Enable Donations", set min/max/dropdown values
+4. Visit the checkout page for that level
+5. For donation-only testing: also check "Donations-Only Level"
+
+### Branching
+
+- **`dev`** — Main development branch (default). PRs target this.
+- **Feature branches** — Named descriptively (e.g., `keep-level`, `payments-free-level`)
+- **No `main`/`master`** — This repo uses `dev` as the default branch
+
+## Coding Conventions
+
+### WordPress Coding Standards
+
+This plugin follows [WordPress PHP Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/):
+
+- **Indentation**: Tabs (not spaces)
+- **Naming**: `snake_case` for functions and variables, prefixed with `pmprodon_`
+- **Strings**: Single quotes for simple strings, double quotes when interpolation needed
+- **Sanitization**: All `$_REQUEST`/`$_GET`/`$_POST` values sanitized with `sanitize_text_field()` and `preg_replace()` for numeric values
+- **Escaping**: Output escaped with `esc_html()`, `esc_attr()`, `esc_url()`, `wp_kses_post()`
+- **i18n**: All user-facing strings wrapped in `__()`, `_e()`, `esc_html_e()`, etc. with text domain `pmpro-donations`
+- **DocBlocks**: PHPDoc on all functions with `@since`, `@param`, `@return`
+
+### Function Prefix
+
+All functions use the `pmprodon_` prefix:
+```php
+pmprodon_get_level_settings()
+pmprodon_pmpro_checkout_level()
+pmprodon_is_donations_only()
+```
+
+### Hook Naming
+
+When hooking into PMPro, the function name mirrors the hook:
+```php
+// Hook: pmpro_after_checkout → Function: pmprodon_pmpro_after_checkout
+add_action( 'pmpro_after_checkout', 'pmprodon_pmpro_after_checkout' );
+```
+
+### No External Dependencies
+
+The plugin has zero composer or npm dependencies. No build step. PHP only.
+
+## Gateway Considerations
+
+Different payment gateways behave differently with donations:
+
+| Gateway | Behavior | Special Handling |
+|---------|----------|-----------------|
+| **Stripe** | Works normally — donation added to total | None |
+| **PayPal Express** | Donation lost on redirect return | `pmprodon_ppe_add_donation_to_request()` restores from order meta |
+| **Pay by Check** | Known bug: donation amount not saved | Issue #86 — needs fix |
+| **PayPal Standard** | No billing fields needed | JS hides billing fieldset |
+
+Gateways that don't require billing info (handled in checkout JS):
+```javascript
+var no_billing_gateways = ['paypalexpress', 'twocheckout', 'check', 'paypalstandard'];
+```
+
+## Key Issue Patterns
+
+Common areas where bugs appear:
+
+1. **Donation-only levels + level groups** — The interaction between donation-only levels and PMPro's level group system (which controls how many levels a user can hold) is complex and fragile.
+2. **Gateway-specific behavior** — Each payment gateway handles the checkout flow differently, especially around redirects (PayPal) and free levels.
+3. **Price display vs. actual charge** — The plugin modifies `$level->initial_payment` at priority 99 but needs to show the original price in the cost text, requiring careful hook/unhook timing.
+4. **Data format consistency** — Donation amounts should always be stored as clean numeric values. Watch for whitespace and string formatting issues.
diff --git a/PLAN.md b/PLAN.md
new file mode 100644
index 0000000..503f352
--- /dev/null
+++ b/PLAN.md
@@ -0,0 +1,381 @@
+# PMPro Donations — Roadmap (v2.3)
+
+> Target version: **2.3**
+> Branch: `dev-2.3` (create from `dev`)
+> Last updated: 2026-02-23
+
+This plan covers all open issues, ideas meeting items, and open PRs. Work is organized into phases. Each ticket includes acceptance criteria for HomurAI ingestion.
+
+---
+
+## Phase 0: PR Review & Merge
+
+Merge existing open PRs into `dev-2.3` after review. These have been open since early-mid 2025 with no review comments.
+
+### TICKET-001: Merge PR #90 — PHP 8.5 deprecation fix
+- **Type**: Bug Fix
+- **PR**: #90 by @dwanjuki (2026-01-09)
+- **Branch**: `fix-php-8.5-notices` → `dev`
+- **Changes**: Replace `(double)` casts with `(float)` in `checkout.php` (3 occurrences)
+- **Risk**: Low — trivial cast rename, no behavior change
+- **Action**: Merge as-is after confirming no conflicts with `dev`
+- **Acceptance Criteria**:
+ - [ ] No PHP deprecation notices on PHP 8.5+
+ - [ ] All `(double)` casts replaced with `(float)` throughout the plugin
+ - [ ] Checkout validation still works (min/max amounts, price formatting)
+
+### TICKET-002: Merge PR #84 — Payment gateways for free donation levels
+- **Type**: Bug Fix
+- **PR**: #84 by @ipokkel (2025-04-16)
+- **Resolves**: Issue #81 (PayPal Express not shown for free levels with donations)
+- **Branch**: `payments-free-level` → `dev`
+- **Changes**:
+ - New function `pmprodon_enable_payments_for_free_level_donations()` hooked to `pmpro_is_level_free`
+ - If a free level has donations enabled (with min_price > 0 or valid dropdown values), returns `false` so gateways render
+ - Adds validation: if donation is required on free level, donation must be > 0
+- **Review Notes**:
+ - Logic is sound but the edge case where `dropdown_prices` contains only "other" needs testing — should still allow $0 donation in that case
+ - The additional validation (`intval($level->initial_payment) === 0 && donation <= 0`) should only apply when `min_price > 0`, otherwise users should be able to donate $0 on a free level
+- **Acceptance Criteria**:
+ - [ ] PayPal Express and other gateways appear on checkout for free levels with donations enabled
+ - [ ] Billing fields shown when donation amount > 0
+ - [ ] Free level with donations but no min_price still allows $0 checkout
+ - [ ] Free level with min_price > 0 requires a valid donation amount
+
+### TICKET-003: Merge PR #85 — Restore level after donation-only checkout
+- **Type**: Bug Fix
+- **PR**: #85 by @ipokkel (2025-04-18)
+- **Resolves**: Issue #72 (member level changes to donation level), Issue #91 (users keep donation level)
+- **Branch**: `keep-level` → `dev`
+- **Changes**: Complete rewrite of `donation-only-level.php`
+ - Stores user's previous levels in user meta before checkout
+ - After checkout, finds which level was in the same group as donation level
+ - Cancels donation-only level via `pmpro_cancelMembershipLevel()`
+ - Restores original level via `pmpro_changeMembershipLevel()` with full level data
+- **Review Notes**:
+ - The old approach (DELETE SQL query) was fragile. New approach using PMPro API functions is much better.
+ - Need to verify: does `pmpro_cancelMembershipLevel()` trigger unwanted side effects (emails, hooks)?
+ - The stored `pmprodon_previous_levels` user meta should be cleaned up even on failure paths
+ - Test with: single level group, multiple level groups, no level groups, recurring subscription levels
+- **Acceptance Criteria**:
+ - [ ] User with Gold membership donates via donation-only level → keeps Gold membership
+ - [ ] Donation-only level in same level group as user's level → user keeps original level
+ - [ ] Donation-only level in separate level group → user keeps original level
+ - [ ] Recurring subscription not cancelled at gateway after donation-only checkout
+ - [ ] No orphaned `pmprodon_previous_levels` user meta after checkout
+
+### TICKET-004: Review PR #83 — Donations report page
+- **Type**: Feature
+- **PR**: #83 by @vbuster01 (2025-04-04)
+- **Branch**: `dev` → `dev` (⚠️ same branch name — needs rebase)
+- **Changes**: New file `pmpro-donations-report.php` — adds a donations report to PMPro Reports dashboard with filtering and CSV export
+- **Review Notes — Issues to Fix Before Merge**:
+ - File should be in `includes/` directory, not root (follow existing structure)
+ - Functions use `MY_pmpro_` prefix — must be renamed to `pmprodon_` prefix per conventions
+ - Missing `$wpdb->prepare()` on some query parts — security review needed
+ - Uses `date()` instead of `wp_date()` or `gmdate()` — WordPress standards
+ - CSV export uses `$_GET` without nonce verification — needs `wp_verify_nonce()`
+ - File is not `require_once`'d from main plugin file — needs integration
+ - Report should use PMPro's existing report page patterns and styles
+ - Hard-coded `'pmpro'` text domain should be `'pmpro-donations'`
+- **Acceptance Criteria**:
+ - [ ] Report widget appears on PMPro Reports dashboard
+ - [ ] Report page shows total donations with month/year filtering
+ - [ ] Individual donations listed with member details
+ - [ ] CSV export works with proper nonce verification
+ - [ ] All functions use `pmprodon_` prefix
+ - [ ] File located in `includes/reports.php`
+ - [ ] Proper escaping and sanitization on all output and input
+ - [ ] Text domain is `pmpro-donations` throughout
+
+---
+
+## Phase 1: Bug Fixes
+
+### TICKET-005: Fix donation amount not saved with Pay by Check gateway
+- **Type**: Bug Fix
+- **Issue**: #86
+- **Priority**: High
+- **Description**: When using Pay by Check gateway, donation amount is stored as 0 in order meta. Works correctly with Stripe and other gateways.
+- **Root Cause Investigation**: The `pmprodon_store_donation_amount_in_order_meta()` function hooks `pmpro_after_checkout` and reads from `$_REQUEST['donation']`. Pay by Check may handle the checkout flow differently, clearing `$_REQUEST` before this hook fires. Compare the checkout flow for PBC vs Stripe to find where the donation value is lost.
+- **Files**: `includes/checkout.php` (function `pmprodon_store_donation_amount_in_order_meta`)
+- **Acceptance Criteria**:
+ - [ ] Donation amount correctly saved to order meta when using Pay by Check gateway
+ - [ ] Donation amount appears in "Additional Order Information" on admin order page
+ - [ ] Invoice shows correct donation breakdown
+ - [ ] No regression with Stripe, PayPal Express, or other gateways
+
+### TICKET-006: Sanitize donation amounts — trim whitespace
+- **Type**: Bug Fix
+- **Issue**: #89
+- **Priority**: Medium
+- **Description**: Donation amounts with trailing whitespace (e.g., "10 ") are stored as strings with the space, causing them to be excluded from reports and numeric queries.
+- **Fix**: Add `trim()` before storing donation amount. Apply in all storage paths:
+ 1. `pmprodon_store_donation_amount_in_order_meta()` in checkout.php
+ 2. `pmprodon_pmpro_updated_order()` in admin.php (admin order editing)
+ 3. `pmprodon_pmpro_checkout_level()` where donation is read from `$_REQUEST`
+- **Files**: `includes/checkout.php`, `includes/admin.php`
+- **Acceptance Criteria**:
+ - [ ] Donation amounts are trimmed before storage (no leading/trailing whitespace)
+ - [ ] Existing code's `preg_replace('/[^0-9\.]/', ...)` sanitization also strips whitespace — verify this catches the issue or add explicit `trim()`
+ - [ ] Reports correctly count donations after fix
+ - [ ] Admin-entered donation amounts also trimmed
+
+### TICKET-007: Fix donation-only level replacing user's membership
+- **Type**: Bug Fix
+- **Issues**: #72, #91
+- **Priority**: High
+- **Description**: When a user with an existing membership checks out for a donation-only level in a level group, they end up keeping the donation level instead of their original. Two related reports: #72 (level changes to donation level) and #91 (users keep both levels).
+- **Fix**: PR #85 addresses this. See TICKET-003 for merge plan.
+- **Acceptance Criteria**: Same as TICKET-003
+
+---
+
+## Phase 2: Enhancements (from Issues)
+
+### TICKET-008: Donation-specific confirmation email and page
+- **Type**: Enhancement
+- **Issue**: #62
+- **Priority**: Medium
+- **Description**: Checkout confirmation page and emails reference "membership" even for donation-only levels. Should reference "donation" instead.
+- **Implementation**:
+ - Filter `pmpro_confirmation_message` to replace membership language with donation language when checkout is for a donation-only level
+ - Add a new email template (or filter existing checkout emails) for donation-only checkouts
+ - Filter `pmpro_email_data` to provide donation-specific subject lines and body content
+ - Use the existing `confirmation_message` level setting as a starting point
+- **Files**: `includes/checkout.php`, `includes/donation-only-level.php`
+- **Acceptance Criteria**:
+ - [ ] Confirmation page says "Thank you for your donation" instead of "your membership is now active" for donation-only levels
+ - [ ] Checkout email uses donation-appropriate language for donation-only levels
+ - [ ] Regular donation levels (donation + membership) still show standard membership messaging
+ - [ ] Site owner can customize the donation confirmation message per level (already exists)
+
+### TICKET-009: Donor note field at checkout
+- **Type**: Feature
+- **Issue**: #82
+- **Priority**: Medium
+- **Description**: Add an optional "Donation Note" text field at checkout that appears on the invoice and in emails. Common use: "In memory of..." or "For the purpose of..."
+- **Implementation**:
+ - Add new level setting: `donation_note_enabled` (checkbox, optional per level)
+ - Add new level setting: `donation_note_label` (custom label text, default "Donation Note")
+ - Render textarea below donation amount field at checkout
+ - Store note in order meta: key `donation_note`
+ - Add email variable: `!!donation_note!!`
+ - Display on invoice bullets and confirmation page
+- **Files**: `includes/level-settings.php`, `includes/checkout.php`, `includes/admin.php`
+- **Acceptance Criteria**:
+ - [ ] Optional per-level setting to enable donation note field
+ - [ ] Text field appears at checkout below donation amount when enabled
+ - [ ] Note stored in order meta
+ - [ ] Note appears on order invoice page
+ - [ ] `!!donation_note!!` email variable works in templates
+ - [ ] Note visible and editable in admin order edit page
+ - [ ] Note included in CSV export
+
+### TICKET-010: Admin donation field when adding orders
+- **Type**: Enhancement
+- **Issue**: #5
+- **Priority**: Low
+- **Description**: When admin creates an order via "Add New Order" or adds a member via "Add Member", there's no field to specify a donation amount. Currently only works through front-end checkout with payment gateways.
+- **Implementation**:
+ - Hook into `pmpro_after_order_settings` on the add-new-order admin page (may already exist for editing — check `admin.php`)
+ - Show donation amount field when the selected level has donations enabled
+ - Save donation to order meta on order creation
+- **Files**: `includes/admin.php`
+- **Acceptance Criteria**:
+ - [ ] Donation amount field appears when creating new orders in admin
+ - [ ] Field only shows for levels with donations enabled
+ - [ ] Donation amount saved to order meta
+ - [ ] Works with "Add Member" admin page if applicable
+
+---
+
+## Phase 3: Ideas Meeting Features
+
+### TICKET-011: Big and beautiful donation option buttons
+- **Type**: Feature (UI overhaul)
+- **Source**: Ideas meeting (Jason)
+- **Priority**: High
+- **Description**: Replace the plain dropdown with visually appealing donation amount buttons at checkout. Think charity website donation forms — large, clickable amount tiles.
+- **Implementation**:
+ - New display mode when `dropdown_prices` are set: render as button grid instead of `