diff --git a/astro.config.mjs b/astro.config.mjs
index b01dce2..9ace6f6 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -2,6 +2,18 @@
import { defineConfig } from 'astro/config';
import { localCfpProxy } from './scripts/local-cfp-proxy.mjs';
+const previewBackend = 'https://devcon26-full-test-events-management.admins-a7d.workers.dev';
+// Workers Builds replaces the branch alias on every push. Set both values before
+// Astro reads public env variables so the page and CSP use the same test backend.
+if (process.env.WORKERS_CI === '1'
+ && process.env.WORKERS_CI_BRANCH === 'feature/devcon26-flexible-ticket-checkout') {
+ process.env.PUBLIC_DEVCON26_COMPLETE_TEST = 'true';
+ process.env.PUBLIC_DEVCON26_API_ORIGIN = previewBackend;
+}
+
+const previewConnectSource = process.env.PUBLIC_DEVCON26_COMPLETE_TEST === 'true'
+ && process.env.PUBLIC_DEVCON26_API_ORIGIN === previewBackend ? ` ${previewBackend}` : '';
+
export default defineConfig({
site: 'https://devcongress.org',
base: '/',
@@ -21,7 +33,7 @@ export default defineConfig({
"form-action 'self' https://em.devcongress.org",
"img-src 'self' data: blob: https:",
"font-src 'self' https://fonts.gstatic.com",
- "connect-src 'self' https://em.devcongress.org https://dvevcongress.goatcounter.com https://gc.zgo.at https://challenges.cloudflare.com",
+ `connect-src 'self' https://em.devcongress.org https://dvevcongress.goatcounter.com https://gc.zgo.at https://challenges.cloudflare.com${previewConnectSource}`,
"frame-src https://www.youtube-nocookie.com https://www.youtube.com https://challenges.cloudflare.com https://pages.inttegro.com",
"worker-src 'self' blob:",
'upgrade-insecure-requests',
diff --git a/content/site.yaml b/content/site.yaml
index 237a90e..37dc718 100644
--- a/content/site.yaml
+++ b/content/site.yaml
@@ -47,7 +47,7 @@
eyebrow: "Africa's tech community"
headline: "Where Africa's builders connect, collaborate and grow."
cta_primary:
- text: "Explore DevCon26"
+ text: "Buy tickets"
color: "yellow"
cta_secondary:
text: "Join the community"
diff --git a/docs/changelog.md b/docs/changelog.md
index f33afc8..8c9195e 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,5 +1,73 @@
# Changelog
+## 2026-10-25 — Grouped community tickets
+
+- Render organizer-assigned community tickets as one quantity group without recipient email inputs, including a grouped order-summary line.
+- Grow community-only orders as community tickets; retain explicit additional tickets and per-ticket choices for mixed personal, gift and shareable orders. Changing one grouped ticket’s use preserves the rest.
+- Keep gift emails attached to their allocations when community counts change, enforce total ticket limits, and retain keyboard focus at the bounds.
+- Keep the community heading inside a continuous card border, use concise explanatory copy and a clearly labeled quantity row, and reduce the change-use action to a quiet secondary control. Wrap controls on narrow phones, with minimum 44px targets and reduced-motion-aware press feedback. Preserve the existing per-seat API contract; no migration or backend setting changes.
+
+## 2026-10-25 — Provider-aware receipt guidance
+
+- Gate receipt actions on code-reviewed provider capabilities: a validated provider URL takes precedence, otherwise confirmed customer receipt email guidance, otherwise payment details only.
+- Keep Paystack receipt links and email promises disabled until supported URL origins or mode-specific merchant email settings are verified. Clear stale receipt links whenever verification restarts or leaves the verified state.
+- Preserve removal of the unreliable browser-print button. No provider settings or production deployments changed.
+- Refresh legacy checkout assertions for the current homepage-to-registration journey, flexible ticket form, dotted badge roles, shared badge renderer, and compact footer; retain payment safety and non-admission checks.
+
+## 2026-10-25 — Remove unreliable payment-record action
+
+- Remove the payment-confirmation Print or save action, which invoked browser printing rather than a provider receipt. Keep verified payment details and the non-admission caption; native browser printing remains available. No provider receipt link is currently supplied by this flow.
+
+## 2026-10-24 — Leave-dialog focus polish
+
+- Focus the payment-confirmation leave-dialog heading on opening so touch users do not see a second outline around Keep editing. Preserve a restrained pink keyboard-focus indicator on both dialog actions.
+
+## 2026-10-24 — Consistent desktop checkout progress
+
+- Match desktop checkout progress to the compact mobile step title/count and three slim segments. Retain accessible, keyboard-operable completed-step navigation without the oversized tinted tabs.
+
+## 2026-10-24 — Compact mobile badge actions
+
+- Keep Edit, Download and Share on one mobile row with shorter labels, and reduce footer separation and bottom padding while preserving touch targets and safe-area spacing.
+
+## 2026-10-24 — Restore badge photo adjustment
+
+- Restore automatic photo adjustment after upload and an Adjust photo action in both editor steps. Save zoom and position with the correct seat, wait for photo uploads, restore saved crops, and keep failed saves retryable.
+- Use side-by-side controls on desktop and a scrollable, touch-friendly mobile dialog. Keep mobile footer links together and explicitly color the download action to avoid Safari's default blue.
+- Verification: production build, badge renderer and payment-confirmation checks passed; local browser fixtures exercised crop save, retry, Escape and a 390px mobile layout. The legacy checkout check still stops at its unrelated landing-page assertion.
+
+## 2026-10-24 — Clear ticket purchase entry points
+
+- Send homepage Buy tickets actions to the DevCon26 ticket section before checkout. Show the GH₵150 per-person price, QR PDF email and badge link, and multi-ticket guidance. Align navigation and FAQs; remove stale registration-opening copy.
+
+## 2026-10-24 — Payment badge renderer and minimal footer
+
+- Select the blended-photo badge renderer for the payment-confirmation editor as well as registration, including matching name validation, photo defaults and artwork loading. Preview and download now share the approved composition.
+- Simplify the page footer to muted links separated by a dot, with pink hover and no arrows or underlines.
+
+## 2026-10-24 — Conference badge and action polish
+
+- Preserve the blended-photo physical badge and original name typography; use dotted uppercase attendee/speaker labels in canvas and SVG.
+- Remove testing banners from badge artwork and simplify confirmation copy while retaining test-payment isolation and disabled public publication.
+- Place compact badge actions beneath the editor, group download/share on narrow screens, and move page links into a quiet footer. Preserve multi-seat navigation and deferred-edit eligibility.
+
+## 2026-10-21 — Clear public checkout availability
+
+- Remove the one-ticket payment rehearsal and its test-email/Turnstile code from public checkout.
+- Replace the disabled form with a loading or unavailable panel and retry action until EMS returns a valid ticket catalog. Clear stale availability and provider details on retry.
+
+## 2026-10-21 — Separate sponsor handover from public checkout
+
+- Remove sponsor handover from the public DevCon26 per-ticket choices. Additional tickets require an explicit gift recipient or community allocation before payment.
+- Keep existing order-management actions for historical buyer-managed purchases; sponsor allocations are arranged in EMS.
+
+## 2026-10-20 — Flexible DevCon26 ticket checkout
+
+- Replace the closed checkout redirect with a responsive quantity and per-seat allocation form. The server catalog owns the unit price, payment provider, maximum quantity, and payment availability.
+- Unify checkout into one continuous form surface and replace browser-native allocation selects with an accessible in-page choice list that explains each handoff option.
+- Add a separate one-ticket Paystack test rehearsal that requires the exact-origin preview-email catalog flag and a Turnstile challenge. Test checkout cannot issue real admission.
+- Verify flexible payments using their own session-scoped grant and show a payment record without admission credentials. Add private buyer-management and recipient-claim destinations for the lifecycle emails.
+
## 2026-10-19 — Consolidate dependency updates from #66
- Include Astro 7.4.0, Zod 4.6.7, Cloudflare Workers types 5.20261016.1 and Wrangler 4.151.0 in the donation follow-up, preserving Inttegro 0.5.8 and the donation recovery build checks. Raise the Node engine minimum to 22.22.0 to match Astro 7.4.0.
@@ -192,3 +260,85 @@
- Unified the DevCon26 mobile section navigation with the homepage header frame:
a sticky 68px cream header, homepage-sized controls, a full-width white menu,
and safe anchor offsets for the dedicated page’s section links.
+
+### 2026-10-21 — Full-flow test preview
+
+- Select complete-test checkout and confirmation using an explicit preview build flag, with test notices and preview-only human verification.
+- Preserve private ticket/claim/badge flows while visibly marking test badge downloads and disabling public badge publication.
+- Reset the human check after checkout errors and back-navigation; preserve ordinary live checkout copy.
+
+### 2026-10-21 — Preview backend CSP
+
+- Allow the exact EMS preview backend only for the explicit full-test build; preserve ordinary production CSP.
+- Verified the deployed form loads GH₵150 pricing and opens a GH₵300 Paystack TEST checkout for self and gift tickets.
+
+### 2026-10-21 — Preview checkout presentation
+
+- Removed the prominent test-checkout banner at the user’s request to bring the preview layout closer to production. Test payment routing and backend safeguards remain active.
+
+## 2026-10-21 — Clean conference checkout
+
+- Redesigned checkout with compact numbered sections, an integrated quantity stepper, app-style inputs and restrained focus rings.
+- Replaced the image summary with a branded conference order summary; it appears before the form on mobile so totals are visible before payment.
+- Kept checkout routing, allocations, test payments, ticket/badge delivery, and shared ticket-access styles unchanged.
+- Verified the Astro build and preview quantity/allocation controls.
+
+## 2026-10-21 — Local checkout stepper
+
+- Added Tickets, Your details, and Payment steps with contextual order information on the right, per-step validation, preserved back-navigation state, and reduced-motion-aware transitions.
+- Render the payment challenge only when Payment is visible; earlier Continue buttons do not initialize payment.
+- Tightened the ticket layout to fit a 1280×720 viewport for one ticket, and position assignment menus upward when needed. Small viewports and larger orders retain natural scrolling.
+- Verified local catalog-fixture navigation, missing gift/contact validation, retained inputs, summary totals, menu bounds, build, and diff whitespace checks. No payment or email side effects; not pushed or deployed.
+
+## 2026-10-21 — Local ticket choice and boarding-pass refinement
+
+- Replaced the allocation dropdown with native radio tiles, selected-choice explanations, one-self availability, and conditional gift email validation.
+- Added a masked decorative Accra-inspired vector illustration, perforated divider, and ticket notches to the order preview; no issued ticket or QR is implied.
+- Changed checkout actions to brand yellow and progress to connected segments with current/completed states.
+- Checked keyboard radio selection, gift validation/removal, step advancement, local viewport fit, and the production build. This refinement remains local, unpushed, and undeployed.
+
+## 2026-10-21 — Pink checkout interaction accents
+
+- Switched primary checkout actions, current-step indicators and selected ticket choices to pink; retained yellow on the illustrated order preview. Used a deeper pink for readable white button text. Local only; no push or deployment.
+
+### Restrained pink refinement
+
+- Reserved pink for primary actions and focus feedback; restored neutral active-step and selected-ticket styling. Yellow remains on the ticket artwork. Local only.
+- Softened the ticket illustration to 42% opacity with fading edge masks, preserving the existing arch clip.
+
+- Restored the previous pink active-step and selected-ticket accents at user request; retained the illustration’s 42% opacity and fading mask. Local only.
+
+### 2026-10-21 — Quantity input focus
+
+- Replace the checkout quantity field’s heavy outer focus outline with a thin inset pink border and a soft tint. Preserve a visible system-color outline in forced-colors mode.
+
+### 2026-10-22 — Persist preview checkout build settings
+
+- Bind the DevCon26 feature branch’s Cloudflare builds to the test checkout backend before Astro generates HTML and CSP, preventing later pushes from reverting the preview to production checkout. Main and other branch builds retain existing settings.
+
+### 2026-10-22 — Payment provider and navigation consistency
+
+- Use the same Back/Continue action row on contact and payment steps.
+- Show Paystack as the sole selected provider in a reusable, vertically expanding provider list with a locally served logo; payment routing remains unchanged. Logo source: https://commons.wikimedia.org/wiki/File:Paystack_Logo.svg.
+
+### 2026-10-22 — Compact step actions
+
+- Size contact and payment Continue buttons to their labels, with Back aligned left and the primary action right. Allow wrapping on narrow screens without stretching the primary button.
+
+## 2026-10-22 — Complete-test badge editor after payment
+
+- Open the saved conference badge editor after verified preview payment for a self ticket.
+- Address the actual issued seat for name/photo edits and mark preview badge downloads as test-only.
+- Preserve gift-only confirmation and add a runtime confirmation regression check.
+
+## 2026-10-24 — Mobile checkout polish
+
+- Added mobile-specific step progress, sticky ticket controls, per-ticket removal, and a collapsed summary below the form.
+- Preserved recipient details when removing a middle ticket; refined radio focus, quantity colors and gift labels.
+- Verified production build and browser removal/navigation behavior; physical iPhone checks remain pending. Local changes only.
+
+### Mobile ticket quantity alignment
+
+- Use an explicit two-column mobile quantity row so the label stays centered and the stepper sits at the right edge. Keep 44px button targets and allow the label to wrap on narrow screens.
+
+- Added a softly masked community photo behind the expanded mobile order summary at 10% opacity, preserving text contrast and the compact collapsed state.
diff --git a/docs/devcon26-badges.md b/docs/devcon26-badges.md
index 38dc1f3..71f2d87 100644
--- a/docs/devcon26-badges.md
+++ b/docs/devcon26-badges.md
@@ -1,8 +1,7 @@
# DevCon26 badges
The physical conference badge is shared by immediate local registration previews and
-verified attendee/speaker private-card previews and downloads. The role label is
-44px on the 1080×1350 export, right-aligned opposite `/images/logo.png` in the
+verified attendee/speaker private-card previews and downloads. The role label uses shared dotted uppercase glyphs on the 1080×1350 export, right-aligned opposite `/images/logo.png` in the
footer. The backing reuses the homepage V11 artwork, including its metal clip and
paper outline; a clean sample of its paper texture replaces baked-in lettering
inside the personalized area. The homepage artwork itself is unchanged.
@@ -36,3 +35,7 @@ Run `pnpm build`, `node scripts/check-devcon26-checkout.mjs`, and
`node scripts/check-devcon26-badge.mjs`. Mocked browser checks cover both roles,
private photos, invalid links, name changes, and preview/download parity without
creating registrations, sending email, or publishing real cards.
+
+Payment confirmation places compact edit, download and share actions beneath the badge.
+Names retain their original display font. Badge artwork omits testing banners;
+test payment and public-publication restrictions remain enforced by the existing flow.
diff --git a/docs/devcon26-flexible-checkout.md b/docs/devcon26-flexible-checkout.md
new file mode 100644
index 0000000..dcb0f2f
--- /dev/null
+++ b/docs/devcon26-flexible-checkout.md
@@ -0,0 +1,100 @@
+# DevCon26 flexible checkout website
+
+The /devcon26/checkout/ page accepts any whole ticket quantity from one to the
+live EMS catalog's flexible_checkout.max_per_payment. The page displays the
+server's GHS unit amount and selects Paystack automatically only when EMS
+returns the single Paystack provider. It keeps the payment action disabled
+until the live catalog reports flexible_checkout.mode: live. The homepage
+links to the page; unavailable checkout is explained before showing buyer fields.
+
+Each seat has one allocation: self (at most one), private gift (recipient
+email), organizer-managed community pool, or buyer-shareable claim. New tickets
+default to a private gift, which requires an address or an explicit change of
+allocation before payment. Sponsor ticket handover is arranged in EMS, not
+through this public checkout.
+
+Initialization posts the full cart and a request UUID to
+/api/public/annual-conference/2026/checkout/flexible/initialize.
+Purchaser details and allocation emails are JSON body fields, never URL
+parameters. The server validates the cart and amount; the website checks the
+response against its catalog snapshot before leaving for Paystack. The
+checkout grant and verifier remain in this browser's session storage, scoped
+to the returned reference. A Paystack callback carries only the reference.
+
+The quantity, allocation, purchaser details, and payment action share one
+continuous form surface. Allocation choices use an in-page listbox rather
+than the operating system's select popup, with visible explanations for each
+choice and keyboard support for arrow keys, Home, End, Enter, Space, Escape,
+and Tab. The listbox keeps the at-most-one self allocation rule visible and
+unavailable on the other tickets. Mobile layouts stack the counter, fields,
+and order summary without introducing a nested scrolling region.
+
+The payment confirmation page recognizes a matching flexible grant, verifies
+with /checkout/flexible/verify, and shows a payment record for verified
+payment only. The verify response has no admission QR or claim capability, so
+the receipt never pretends to be a ticket. Existing standard live and sandbox
+confirmation paths remain separate.
+
+After fulfillment, EMS emails the buyer a private
+/devcon26/tickets/manage/#capability=... link. That page calls the order
+endpoint and can create or replace a shareable admission claim or reissue a gift
+invitation. Historical buyer-managed orders retain their existing handover
+actions. The page constructs links only from a capability returned by the matching action.
+Gift and shareable recipients use /devcon26/tickets/claim/#capability=...;
+a badge link additionally has purpose=badge. A successful claim reports
+the server's delivery state and never displays a QR on the website.
+
+Public checkout contains no payment rehearsal or test-email form. While the catalog
+loads, a status panel replaces the form. If availability cannot be confirmed,
+the panel explains that checkout is unavailable and offers a retry. The form
+and order summary appear only after a valid catalog arrives; retry clears stale
+provider, price, and availability text. The separate EMS sandbox remains unchanged.
+
+The checkout, management, and claim routes are noindex static pages. They use
+the existing site CSP, processed Astro scripts, and the EMS exact-origin CORS
+gate. Website source and a successful static build do not activate sales,
+apply EMS migrations, or prove a Paystack round trip.
+
+Desktop and mobile captures of the static gated state are available for
+[checkout](./screenshots/devcon26/checkout-desktop.png),
+[ticket management](./screenshots/devcon26/manage-desktop.png), and
+[ticket claims](./screenshots/devcon26/claim-desktop.png). The corresponding
+mobile captures are [checkout](./screenshots/devcon26/checkout-mobile.png),
+[management](./screenshots/devcon26/manage-mobile.png), and
+[claims](./screenshots/devcon26/claim-mobile.png). These show responsive layout
+with sales closed; they do not demonstrate payment or admission.
+
+## Full-flow test preview
+
+Build the feature preview with `PUBLIC_DEVCON26_COMPLETE_TEST=true`. This selects the separate complete-test catalog, initialization, and verification endpoints and displays a test-payment notice. Omit the flag for the public website build. The main checkout includes a Turnstile check bound to `devcon26_complete_test_checkout`; its widget must allow the exact preview hostname.
+
+Paystack test payments drive real private management/claim links, QR email attachments, and badge creation. EMS restricts recipients to the enabled tester list and enforces quotas. Test QR PNGs and downloaded badges visibly state that they are not valid for admission. Test badges cannot publish publicly.
+
+EMS migration `20261021150000_complete_test_ticket_checkout.sql` must be reviewed and applied before activating this path. It retains test provenance and blocks live launch while test orders remain. No migration or preview activation was performed as part of the review-fix pass; shared capacity cleanup requires separate approval.
+
+The test build's CSP adds only
+`https://devcon26-full-test-events-management.admins-a7d.workers.dev` when both
+`PUBLIC_DEVCON26_COMPLETE_TEST=true` and `PUBLIC_DEVCON26_API_ORIGIN` exactly
+match that backend. No wildcard or production CSP change is required.
+
+### Checkout presentation
+
+The purchase page imports `devcon26-checkout-design.css` after the shared ticket-access stylesheet. Its `.checkout-design` scope keeps claim and management screens unchanged. Controls follow the EMS form palette and 8px radii, with 44px minimum button targets and a subtle pink input focus ring. The order summary precedes the form in source/mobile reading order and occupies the right column on desktop. Gift email disclosure and the single continuous form retain the existing checkout controller.
+
+### Local stepper refinement
+
+Checkout now presents three steps within one preserved form. Continue validates only the current step; final submission reveals the earliest invalid prior step before reporting browser validity. Earlier-step edits require advancing through validation again. The right context panel uses text-only rendering for allocation and purchaser details, with totals always visible. Turnstile uses explicit rendering when Payment is shown. The local UI was checked with a read-only catalog fixture; payment and email testing still belongs to the authorized preview journey after deployment approval.
+
+The local allocation refinement uses native radio groups within a fieldset per ticket. Changing a selection restores focus to the corresponding replacement radio, and the other tickets disable the personal allocation with a visible explanation. The order preview has decorative masked SVG artwork and ticket notches; it remains explicitly an order preview. Primary actions use accessible deep pink with white text; current-step and selected-choice accents also use pink, while the illustrated ticket retains yellow and softened, fading artwork. The earlier custom dropdown and its keyboard/event state have been removed from the purchase controller.
+
+### Persistent Cloudflare preview builds
+
+Cloudflare Workers Builds supplies `WORKERS_CI=1` and `WORKERS_CI_BRANCH`. For the exact `feature/devcon26-flexible-ticket-checkout` branch, Astro config sets `PUBLIC_DEVCON26_COMPLETE_TEST=true` and the fixed EMS preview API origin before generating HTML and CSP. Every automatic push build therefore retains test routing. No GitHub variables or Worker runtime secrets are needed for these public build settings. Main and other branches retain their existing configuration. Manual builds outside Workers Builds still require the two explicit preview variables.
+
+### Mobile checkout controls (2026-10-24)
+
+Below 821px, progress uses the current step name, step count and three segments. Progress and ticket quantity stay sticky below the site header; Back/Continue navigate steps. Additional tickets can be removed in place without losing the other recipients. The summary follows the form and starts collapsed on mobile, expanded on desktop.
+
+### Post-payment photo adjustment
+
+The payment badge editor opens the photo adjuster after a new image is selected and exposes Adjust photo in both editing and preview steps. Zoom and horizontal/vertical position use the existing seat card crop API (`scale`, `x`, `y`). Saving waits for the selected seat's upload, persists its crop, and preserves retry controls on failure. Previously saved crops are restored when reopening the editor. Desktop uses side-by-side controls; mobile stacks the preview and 44px controls within a scrollable dialog.
diff --git a/docs/devcon26-test-checkout.md b/docs/devcon26-test-checkout.md
index c105587..fcca43e 100644
--- a/docs/devcon26-test-checkout.md
+++ b/docs/devcon26-test-checkout.md
@@ -107,7 +107,7 @@ the ticket tier, quantity, GHS amount, and payment status; local tier metadata
supplies only the matching label and photo.
Only a valid `mode: "test"`, `status: "verified"` response reveals the payment
-summary, full neutral reference, optional print/save action, and next steps.
+summary, full neutral reference, capability-gated provider receipt guidance, and next steps.
Pending, failed, missing/unknown reference, invalid response, timeout, and
connection-error states never display successful payment or a usable ticket.
Retries only check the existing payment. No JavaScript means no verified
@@ -129,8 +129,7 @@ and subtle side notches make it easier to retain without implying admission.
The record keeps the existing server-supplied amount, tier, quantity, coupon,
and neutral payment-reference hooks; no payment, EMS, or Paystack behavior changed.
-The action now accurately says **Print or save payment record**. Its print
-stylesheet renders only the record, with a white fallback-safe surface, 150mm
+The former **Print or save payment record** button has since been removed because embedded mobile browsers may not support printing. Native browser printing still uses the stylesheet to render only the record, with a white fallback-safe surface, 150mm
maximum width, exact-color adjustment, and legacy/modern break-avoid rules to
keep the receipt together.
@@ -407,8 +406,8 @@ The completed-payment summary keeps a single outer border without the stacked
bottom shadow. Internal row rules, the dashed footer divider, and its full-bleed
tinted band are removed. Amount-first typography and additional spacing around
ticket selection and the payment reference provide the hierarchy instead.
-Desktop and mobile retain comfortable padding, and the save action and
-"Payment record only. Not an admission ticket." caption remain intact.
+Desktop and mobile retain comfortable padding and the
+"Payment record only. Not an admission ticket." caption. The former save action was subsequently removed.
The confirmation venue block no longer includes the building photo or its mask.
Venue information and directions remain; the selected ticket photo inside the
@@ -439,3 +438,41 @@ the script; no Downloads folder or external source file is required.
These checks neither call the payment API nor replace browser layout, native
form validation, or Paystack round-trip verification. No dependency or separate
test runner is added.
+
+
+## Provider receipt capabilities
+
+The confirmation page uses the code-owned registry in `src/lib/payment-receipts.ts`.
+After payment verification succeeds, it displays a returned `receipt_url` only
+when it is an HTTPS URL on an explicitly approved provider origin, with no
+credentials or control characters. It never constructs a receipt link from a
+reference, receipt number, or checkout authorization URL. Otherwise it shows
+email guidance only for a payment mode whose merchant customer-receipt setting
+has been operationally confirmed. Without either capability, payment details
+and the reference remain visible without a receipt promise.
+
+Paystack currently has no allowed receipt origins and no confirmed email modes.
+Its transaction API documents `receipt_number`, not a customer receipt URL:
+https://paystack.com/docs/api/transaction/
+Customer emails are a merchant preference and must be checked in the actual
+account and applicable mode before enabling guidance:
+https://support.paystack.com/en/articles/2131010
+
+For a future provider, review its documented receipt URL format/origins, add a
+server adapter that returns the transaction's real receipt URL, and update the
+trusted provider context and provider label in the confirmation flow. Add
+origins to the registry only after that review. Record current merchant-setting
+evidence before adding a mode to `customerEmailConfirmedModes`; supported email
+functionality alone does not prove emails are enabled or delivered. Recheck
+this configuration when the provider/account settings change. Existing
+DevCongress ticket PDF and badge emails are independent and unchanged.
+
+Run `node scripts/check-payment-receipts.mjs` for the focused provider-policy and rendering checks.
+
+### Grouped community allocations
+
+Organizer-assigned (`community_managed`) places appear as one quantity group and one summary line. They never require recipient emails. Increasing the total of a community-only order adds more community places; mixed orders retain the existing gift default for additional places. “Add another ticket” creates an individually editable ticket, and “Change one ticket’s use” converts one community place to an individually editable gift without changing the total.
+
+The API still receives one allocation per seat. Only gifts carry `recipient_email`; personal and shareable tickets retain their existing behavior. Community quantity changes preserve other allocations and gift details, with the catalog limit applied to the whole order.
+
+Verification: `node scripts/check-community-tickets.mjs`, the full checkout checker and the preview build; local in-app browser checks with a synthetic catalog for community-only and mixed orders. No payment or email is initiated by these checks. Delivery targets the PR branch preview only; production remains unchanged.
diff --git a/docs/screenshots/devcon26/checkout-desktop.png b/docs/screenshots/devcon26/checkout-desktop.png
new file mode 100644
index 0000000..392c47f
Binary files /dev/null and b/docs/screenshots/devcon26/checkout-desktop.png differ
diff --git a/docs/screenshots/devcon26/checkout-mobile.png b/docs/screenshots/devcon26/checkout-mobile.png
new file mode 100644
index 0000000..9151ec1
Binary files /dev/null and b/docs/screenshots/devcon26/checkout-mobile.png differ
diff --git a/docs/screenshots/devcon26/claim-desktop.png b/docs/screenshots/devcon26/claim-desktop.png
new file mode 100644
index 0000000..48345a6
Binary files /dev/null and b/docs/screenshots/devcon26/claim-desktop.png differ
diff --git a/docs/screenshots/devcon26/claim-mobile.png b/docs/screenshots/devcon26/claim-mobile.png
new file mode 100644
index 0000000..2b01cbc
Binary files /dev/null and b/docs/screenshots/devcon26/claim-mobile.png differ
diff --git a/docs/screenshots/devcon26/manage-desktop.png b/docs/screenshots/devcon26/manage-desktop.png
new file mode 100644
index 0000000..9a366bc
Binary files /dev/null and b/docs/screenshots/devcon26/manage-desktop.png differ
diff --git a/docs/screenshots/devcon26/manage-mobile.png b/docs/screenshots/devcon26/manage-mobile.png
new file mode 100644
index 0000000..3261953
Binary files /dev/null and b/docs/screenshots/devcon26/manage-mobile.png differ
diff --git a/public/images/payment-providers/paystack.svg b/public/images/payment-providers/paystack.svg
new file mode 100644
index 0000000..3fe7778
--- /dev/null
+++ b/public/images/payment-providers/paystack.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scripts/check-community-tickets.mjs b/scripts/check-community-tickets.mjs
new file mode 100644
index 0000000..52ec499
--- /dev/null
+++ b/scripts/check-community-tickets.mjs
@@ -0,0 +1,72 @@
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import { createRequire } from 'node:module';
+import vm from 'node:vm';
+
+const require = createRequire(import.meta.url);
+const astroRequire = createRequire(require.resolve('astro/package.json'));
+const { transform } = astroRequire('esbuild');
+const source = readFileSync('src/lib/devcon26-flexible-checkout.ts', 'utf8');
+const { code } = await transform(source, { loader: 'ts', format: 'cjs' });
+
+function extractFunction(name) {
+ const start = code.indexOf(`function ${name}(`);
+ const open = code.indexOf('{', start);
+ let depth = 1;
+ let end = open + 1;
+
+ while (depth && end < code.length) {
+ if (code[end] === '{') depth++;
+ if (code[end] === '}') depth--;
+ end++;
+ }
+
+ assert.ok(start >= 0 && depth === 0);
+ return code.slice(start, end);
+}
+
+const state = {
+ allocations: [{ kind: 'community_managed' }],
+ catalog: { maxPerPayment: 20 },
+ liveBusy: false,
+ quantity: { value: '1', setCustomValidity() {}, reportValidity() {} },
+ invalidateLiveRequest() {},
+ renderRows() {},
+ rows: { querySelector: () => ({ focus() {}, disabled: false }) },
+ page: { querySelector: () => ({ textContent: '' }) },
+};
+vm.createContext(state);
+vm.runInContext(extractFunction('setQuantity') + '\n' + extractFunction('changeCommunityQuantity'), state);
+
+state.setQuantity(5);
+assert.equal(state.allocations.length, 5);
+assert.ok(state.allocations.every((ticket) => ticket.kind === 'community_managed' && !('recipient_email' in ticket)));
+
+state.allocations.push({ kind: 'self' }, { kind: 'gift', recipient_email: 'gift@example.test' }, { kind: 'community_shareable' });
+state.changeCommunityQuantity(1);
+state.changeCommunityQuantity(-1);
+assert.deepEqual(Array.from(state.allocations.slice(5), (ticket) => ({ ...ticket })), [
+ { kind: 'self' },
+ { kind: 'gift', recipient_email: 'gift@example.test' },
+ { kind: 'community_shareable' },
+]);
+state.setQuantity(9);
+assert.equal(state.allocations[8].kind, 'gift');
+assert.equal(state.allocations[6].recipient_email, 'gift@example.test');
+
+for (let i = 0; i < 5; i++) state.changeCommunityQuantity(-1);
+assert.equal(state.allocations.filter((ticket) => ticket.kind === 'community_managed').length, 0);
+assert.equal(state.allocations[1].recipient_email, 'gift@example.test');
+state.setQuantity(20);
+state.changeCommunityQuantity(1);
+assert.equal(state.allocations.length, 20);
+state.setQuantity(21);
+assert.equal(state.allocations.length, 20);
+state.allocations = [{ kind: 'community_managed' }];
+state.changeCommunityQuantity(-1);
+state.setQuantity(0);
+assert.equal(state.allocations.length, 1);
+state.liveBusy = true;
+state.changeCommunityQuantity(1);
+assert.equal(state.allocations.length, 1);
+console.log('PASS: community-only growth, mixed allocations and gift preservation, group removal, quantity bounds and busy guard.');
diff --git a/scripts/check-devcon26-badge.mjs b/scripts/check-devcon26-badge.mjs
index 99bc547..9bcd1a7 100644
--- a/scripts/check-devcon26-badge.mjs
+++ b/scripts/check-devcon26-badge.mjs
@@ -54,7 +54,7 @@ for (role of ['attendee', 'speaker']) {
assert.equal(preview.status, 200);
assert.ok(svg.includes('viewBox="0 0 1080 1350"'));
- assert.ok(svg.includes('font-size="44"'));
+ assert.ok(svg.includes(` ({ ...quote, status: 'refund_required', amount_minor: 18999 }) });
await tick();
assert.equal(attentionRoot.dataset.paymentState, 'refund_required');
assert.equal(attentionElements['payment-receipt'].hidden, true);
assert.equal(attentionElements['payment-attention'].hidden, false);
assert.equal(attentionElements['payment-title'].textContent, 'Your payment needs attention.');
-await attentionElements['payment-print'].fire('click');
assert.equal(printed, false);
async function readyFixture(options) {
@@ -684,34 +687,27 @@ const checkoutHtml = readFileSync(root + '/dist/devcon26/checkout/index.html', '
assert.ok(!html.includes('ticket-checkout-dialog'), 'The landing page no longer contains a checkout modal');
-assert.ok(html.includes('href="/devcon26/register/"'), 'The landing page leads to current registration availability');
+assert.ok(homeHtml.includes('href="/devcon26/#tickets"'), 'Homepage ticket actions lead to the conference registration section');
+assert.ok(html.includes('id="tickets"') && html.includes('GH₵150'), 'Registration section explains the individual ticket price');
+assert.ok(html.includes('href="/devcon26/checkout/"'), 'Registration section continues to ticket checkout');
assert.ok(html.includes('href="/devcon26/speak/"'), 'The landing page links the annual conference CFP');
-assert.ok(!html.includes('data-ticket-checkout='), 'Group ticket sales are no longer advertised');
-assert.ok(readFileSync(root + '/dist/devcon26/register/index.html', 'utf8').includes('name="email"'));
-
-assert.ok(checkoutHtml.includes('data-checkout-page'));
-assert.ok(checkoutHtml.includes('Complete your booking.'));
+assert.ok(!html.includes('data-ticket-checkout='), 'Legacy group ticket sales are not advertised');
+assert.ok(checkoutHtml.includes('data-flexible-checkout'), 'Checkout uses the flexible individual-ticket flow');
assert.ok(checkoutHtml.includes('noindex, nofollow'));
-assert.ok(checkoutHtml.includes('data-checkout-fields disabled'), 'Buyer fields are disabled without JavaScript');
+assert.match(checkoutHtml, /data-checkout-layout hidden/, 'Checkout form stays unavailable until the catalog is loaded');
+assert.match(checkoutHtml, /data-pay[^>]*disabled/, 'Payment cannot begin without JavaScript and verified availability');
assert.ok(!/name="(?:purchaser_name|purchaser_email|coupon_code)"/.test(checkoutHtml), 'Native form fallback cannot send buyer details in a URL');
-assert.ok(!checkoutHtml.includes('
- Registration opens soon ↗
+ Buy tickets ↗
diff --git a/src/components/Devcon26HomeLink.astro b/src/components/Devcon26HomeLink.astro
new file mode 100644
index 0000000..727b645
--- /dev/null
+++ b/src/components/Devcon26HomeLink.astro
@@ -0,0 +1,56 @@
+---
+---
+← Back to DevCongress
+
+
diff --git a/src/components/Devcon26PhotoAdjuster.astro b/src/components/Devcon26PhotoAdjuster.astro
new file mode 100644
index 0000000..183985a
--- /dev/null
+++ b/src/components/Devcon26PhotoAdjuster.astro
@@ -0,0 +1,155 @@
+
+
+
diff --git a/src/components/HeroSection.astro b/src/components/HeroSection.astro
index f55661d..0695586 100644
--- a/src/components/HeroSection.astro
+++ b/src/components/HeroSection.astro
@@ -42,7 +42,7 @@ const colorClass: Record = {
Your ticket. Your community. A day of building what’s next.
+
+
+
+
Checking ticket availability…
+
We’re checking prices and whether checkout is ready.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {completeTest && }
+
+
+
diff --git a/src/pages/devcon26/index.astro b/src/pages/devcon26/index.astro
index 38fda1e..f868ea5 100644
--- a/src/pages/devcon26/index.astro
+++ b/src/pages/devcon26/index.astro
@@ -1,6 +1,7 @@
---
import { getCollection } from 'astro:content';
import Base from '../../layouts/Base.astro';
+import Devcon26HomeLink from '../../components/Devcon26HomeLink.astro';
const speakerSlots = ['01', '02', '03'];
const heroScenes = [
@@ -121,8 +122,8 @@ heroSlots[0].photos.push({
const faqs = [
['Who is DevCon26 for?', 'For the people building technology across Africa: developers, designers, founders, product people, researchers, and students.'],
['When and where is it?', 'Meet us at Ghana Digital Center (formerly Accra Digital Center), Accra, Ghana on 19 December 2026. The programme will be announced here.'],
- ['Can I come with my team?', 'Yes. When registration opens, each person will register separately with their own name and email.'],
- ['How do I register?', 'Registration opens soon. Check back here for updates.'],
+ ['Can I come with my team?', 'Yes. Buy individual tickets together in one checkout, then choose who each ticket is for. Each ticket admits one person.'],
+ ['How do I get a ticket?', 'Go to ticket checkout, choose your tickets, and complete payment. Your ticket email includes a QR ticket PDF and a link to personalize your conference badge.'],
];
const sponsorIds = new Set(['kweku_tech']);
@@ -213,7 +214,7 @@ function packageEnquiryLink(sponsorship: (typeof sponsorshipPackages)[number]) {
- ← Back to DevCongress
+ DevCongress annual conference2026
@@ -222,7 +223,7 @@ function packageEnquiryLink(sponsorship: (typeof sponsorshipPackages)[number]) {
A year together. One big gathering.
Meetups, ideas, and new connections all year. Join the community at DevCon26.