diff --git a/AGENTS.md b/AGENTS.md
index 76f6cb9d8..a3ea19ff7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -210,6 +210,7 @@ end
- `BuiltinCalloutCards` — Renders the live, per-registration ticket callout cards (payment, certificate, scholarship, CE hours, videoconference), overlaying dynamic status (badge, colour, visibility guard, destination) on each materialized built-in row via `#card_for`. Rendered through the same `_callout_card` partial as `RegistrationTicketCallout`s. Skips any card an event has materialized (see `BuiltinCallouts`) so the two paths never double-render, and `#cards` serves as the fallback for events not yet seeded; `.editor_cards` builds the editor's preview cards. Art supplies, Handouts, and FAQ are pure content cards with no builder here — they render from their row. Public show pages live under `app/views/events/callouts/` (`Events::CalloutsController`, slug-authorized)
- `BuiltinCallouts` — Owns the built-in callout definitions and materializes them into `RegistrationTicketCallout` rows in canonical ticket order: `seed` persists (on create, and lazily on edit so older events heal with no backfill), `build` makes the same rows in memory for the new-event form (with `builtin_key` round-tripped through nested attributes), `reset`/`customized?` back the "Restore default" control. All eight seed **hidden** by default — admins publish the ones they want; there's no config-based auto-publish. Built-ins are edited in the **same** callout-fields row as custom callouts (pre-filled title/subtitle/colour/icon/callout-page-text/resources; hidden instead of deleted; "Restore default" shown only when `.customized?`). "Content" cards (Art supplies, Handouts, FAQ) render their own copy/resources on the generic callout page; "behavioral" cards render live status through `BuiltinCalloutCards#card_for`, which overlays the app's badge/visibility/destination on the row's editable presentation. Behavioral pages show the row's callout-page-text as an intro (`@builtin_intro`) and any linked resources below it. Videoconference drips a week before start via `display_from`. CE hours and Art supplies are edited like every other built-in — their title/text live entirely on the row (the legacy `event_details*`/`ce_hours_details*` event columns were dropped); the CE hours-offered/cost config still edits the event inline via `event_f` (`ce_config?`). The registrant CE page reads the row's title/description. Built-ins always seed and also materialize lazily on `edit`, so the editor shows the full set; the editor shows "Restore default" (or a static "Matches default") per row via `.customized?`. The visibility control is a `published` toggle (inverse of `hidden`)
- `CalloutContent` — Parses admin-authored callout HTML into ordered segments so **every** callout content page renders the same way: plain rich text, with each standard `……` disclosure (the markup any HTML generator/LLM produces; `` and a `title` attribute are accepted aliases; `` starts expanded) rebuilt into a styled collapsible card. ``/`` are also on the `form_label_html` allowlist (`FORM_LABEL_TAGS`, plus the `open` attribute), so a disclosure is never stripped on save — the parser only upgrades its styling. Rendered through the shared `app/views/events/callouts/_rich_content.html.erb` partial (which wraps each disclosure in `_toggle.html.erb`), used by the art-supplies ("Art supplies & what to bring", a content callout on the generic page), CE hours, custom-callout, behavioural-card-intro, and FAQ pages. The FAQ page renders the editable `faq` callout `description` (each question a ``), falling back to `BuiltinCallouts.faq_html` when the card isn't materialized. Content with no disclosure renders unchanged
+- `SampleTicketRegistration` — Builds the **unsaved, data-free** `EventRegistration` ("Sample Person") that the sample ticket and its admin-only callout-page previews render from; nothing is ever persisted, so the preview can't read from or write to a real registrant or leak into counts/revenue/rosters/reminders. `all_options:` mirrors the ticket's "Show all options" toggle (turns on scholarship/CE/W-9 so those cards and preview pages render). Shared by `EventsController#sample_ticket` and `Events::CalloutsController`'s sample mode (the `sample` param → admin-authed in-memory previews of the behavioral built-in pages, linked from the sample ticket via `EventHelper#sample_callout_path`)
### Affiliations
diff --git a/app/controllers/events/callouts_controller.rb b/app/controllers/events/callouts_controller.rb
index e421c06d9..63ab39883 100644
--- a/app/controllers/events/callouts_controller.rb
+++ b/app/controllers/events/callouts_controller.rb
@@ -4,7 +4,9 @@ module Events
# Each is reachable by the registration slug — the slug is the authorization,
# so no login is required (mirrors the public ticket/invoice pages).
class CalloutsController < ApplicationController
- skip_before_action :authenticate_user!
+ # Real registrant pages are public (the slug is the authorization); the
+ # sample-ticket previews instead require an admin (authorized in #authorize_callout).
+ skip_before_action :authenticate_user!, unless: :sample_preview?
before_action :set_event_registration
before_action :authorize_callout
before_action :set_event
@@ -12,6 +14,8 @@ class CalloutsController < ApplicationController
# above the app-controlled content, plus any resources linked to the row.
before_action :set_builtin_content, only: %i[ payment scholarship certificate videoconference ]
+ helper_method :sample_preview?
+
# The single-resource page previews a PDF in an