diff --git a/doc/promo-code-apply-to-all-audience-restriction.md b/doc/promo-code-apply-to-all-audience-restriction.md
new file mode 100644
index 000000000..fc0a0e1fc
--- /dev/null
+++ b/doc/promo-code-apply-to-all-audience-restriction.md
@@ -0,0 +1,140 @@
+# Promo Code "Apply to All" Audience Restriction — summit-admin Spec
+
+Created: 2026-05-11
+Author: casey@caseylocker.com
+Status: PENDING
+Type: Change Request (UI copy + admin clarity)
+ClickUp: [86b9vrpxp](https://app.clickup.com/t/86b9vrpxp)
+Parent feature: [86b952pgc](https://app.clickup.com/t/86b952pgc) — Promo Codes for Early Registration
+Parent feature plan (local working doc): `docs/superpowers/plans/2026-04-21-promo-codes-for-early-registration-access.md`
+API counterpart SDS: `summit-api/doc/promo-code-apply-to-all-audience-restriction.md`
+
+## Summary
+
+**Goal:** Update the "Apply to all Ticket Types" checkbox copy in the discount-code editor so it accurately reflects the (now stricter) backend behavior: the implicit sweep only covers ticket types with `Audience = All`. Add helper text that tells the admin how to associate non-All-audience ticket types (WithInvitation, WithoutInvitation, WithPromoCode) — they must be added explicitly via the picker after saving.
+
+**Why:** The backend has been tightened (see API SDS) so an empty `allowed_ticket_types` collection no longer means "apply to every ticket type, regardless of audience." Without a label/helper update, admins will keep ticking the box expecting the old behavior and be surprised that `WithoutInvitation`/`WithInvitation`/`WithPromoCode` ticket types are not covered. Parent feature SDS Resolved Decision #8 — _"audience controls visibility, type controls access"_ — depends on the admin understanding this distinction.
+
+**Approach:** Pure i18n + minor JSX. Two label changes and one helper-text row in `discount-base-pc-form.js`. No JS logic change: the FE `apply_to_all_tix` checkbox is already a UI-only flag that hides the per-ticket-type picker; the BE carries the load-bearing enforcement.
+
+## Scope
+
+### In Scope
+
+- `src/i18n/en.json` — update `edit_promocode.apply_to_all_tix` copy; add `edit_promocode.apply_to_all_tix_helper` for the inline explainer.
+- `src/components/forms/promocode-form/forms/discount-base-pc-form.js` — render the helper text below the checkbox.
+- `src/components/forms/speakers-promo-code-spec-form.js` — same checkbox label is reused here under the `AUTO_GENERATED_SPEAKERS_DISCOUNT_CODE` branch; render the same helper text so admins on the bulk-speaker-discount path get the same clarity.
+- Extend `src/components/forms/promocode-form/__tests__/promocode-form.integration.test.js` (existing — extended for this change) with assertions for the new label and helper text.
+
+### Out of Scope
+
+- Client-side ticket-type filtering. The FE never had "select all" behavior on this checkbox; it only hides the per-type picker. No JS logic change is required.
+- Changes to the picker/`DiscountTicketTable` component. Audience visibility filtering in the picker — if needed at all — is a separate concern; the picker already surfaces every audience because the parent feature explicitly relies on this for opting `WithPromoCode` types in.
+- Backend enforcement. Covered entirely by the API SDS.
+
+## Truths (Authoritative Decisions)
+
+1. **The FE `apply_to_all_tix` flag is UI-only and stays UI-only.** It is deleted before send (`src/actions/promocode-actions.js:160`) and rederived from `ticket_types_rules.length === 0` (`src/reducers/promocodes/promocode-reducer.js:179`). The backend has no corresponding stored flag — it infers "apply to all" from an empty `allowed_ticket_types` collection. Do not change this contract.
+
+2. **No "select all on click" behavior is added.** The existing UX is: checking the box hides the per-type picker and saves the code with no `ticket_types_rules`. Adding any kind of FE pre-population would conflict with the BE's empty-collection semantics. The helper text instead tells the admin to use the picker for non-All audiences after saving.
+
+3. **Helper text wording is required, not optional.** The change request explicitly calls out admin confusion as the failure mode. Without the inline explainer below the checkbox, admins will keep encountering surprise.
+
+4. **Copy keys live under `edit_promocode.*`** to match the existing namespace at `src/i18n/en.json:992`-`1008`.
+
+## Approach
+
+**Chosen:** Two-key i18n update + one helper-text JSX row beneath the existing checkbox.
+
+**Why:** Smallest possible change that addresses the admin clarity gap. Pure copy. Mirrors the surrounding bootstrap form styling. No JSX restructuring or component refactor.
+
+**Alternatives considered:**
+
+- _Filter the per-type picker to hide `WithPromoCode` rows by default._ Rejected — the parent feature explicitly requires admins to be able to opt `WithPromoCode` types into a discount code via the picker. Hiding them would block the only legitimate path.
+- _Pre-populate `ticket_types_rules` with all `Audience = All` types when the box is checked._ Rejected — adds FE logic that diverges from the BE's empty-collection semantics; would also reverse-break existing discount codes on first edit.
+- _Disable the checkbox entirely on edit forms with mixed-audience summits._ Rejected — overreaches; the box still has a useful "apply to all Audience = All types" function.
+
+## Context for Implementer
+
+- **Entry point:** `src/components/forms/promocode-form/forms/discount-base-pc-form.js` is the only JSX file that changes. The checkbox sits at lines 19-30; the helper row goes immediately after the closing `` of the `form-check` block (line 30) but still inside the `col-md-4` (line 15).
+- **i18n keys:** existing `edit_promocode.apply_to_all_tix` (line 1003 in `en.json`) gets a copy update. New key `edit_promocode.apply_to_all_tix_helper` is added beside it. The same key is also rendered by `src/components/forms/speakers-promo-code-spec-form.js:227` under the `AUTO_GENERATED_SPEAKERS_DISCOUNT_CODE` branch — that form gets a matching helper-text row.
+- **Existing tests touched:** `src/components/forms/promocode-form/__tests__/promocode-form.integration.test.js` already asserts label rendering for the discount-code form; extend rather than duplicate.
+- **Style:** keep the helper-text styling consistent with surrounding form rows (bootstrap legacy classes — `form-text`, `text-muted`, or equivalent — no MUI). The skill file `skills/react-frontend.md` in the fn-skills vault calls out the legacy-Bootstrap convention for this area.
+
+## Tasks
+
+### Task 1: i18n copy
+
+**File:** `src/i18n/en.json`
+
+**Changes:**
+
+- Update `edit_promocode.apply_to_all_tix` (around line 1003 in `en.json`) from `"Apply to all Ticket Types"` to `"Apply to all ticket types (Audience: All)"`.
+- Add new key beneath it: `edit_promocode.apply_to_all_tix_helper` with value:
+ `"Only ticket types with Audience = All are covered. WithInvitation, WithoutInvitation, and WithPromoCode ticket types must be added explicitly via the ticket-type picker after saving."`
+
+**Definition of Done:**
+
+- [ ] Both keys present in `src/i18n/en.json` under the `edit_promocode` namespace.
+- [ ] No duplicate keys, no JSON syntax errors (`yarn build` / lint runs cleanly).
+
+### Task 2: Helper-text row in the discount form
+
+**File:** `src/components/forms/promocode-form/forms/discount-base-pc-form.js`
+
+**Change:** Below the existing checkbox `
...
` (line 30 closing), add a small bootstrap-styled helper paragraph:
+
+```jsx
+
+ {T.translate("edit_promocode.apply_to_all_tix_helper")}
+
+```
+
+This sits inside the same `col-md-4` container as the checkbox.
+
+**Definition of Done:**
+
+- [ ] Helper text renders only on this discount form (no leakage into other promo-code variants).
+- [ ] Visible whether the box is checked or unchecked (it's always relevant context).
+- [ ] Styling matches surrounding form rows (no MUI components introduced).
+
+### Task 3: Test coverage
+
+**File:** `src/components/forms/promocode-form/__tests__/promocode-form.integration.test.js`
+
+**Add:** new `it()` cases under the existing discount-code suite asserting:
+
+- The "Apply to all ticket types (Audience: All)" label string is rendered.
+- The helper text including "WithPromoCode" (case-insensitive) is rendered immediately below.
+
+**Definition of Done:**
+
+- [ ] Both assertions pass.
+- [ ] Existing tests in the file still pass.
+- [ ] `yarn test --watchAll=false src/components/forms/promocode-form/__tests__/promocode-form.integration.test.js` exits 0.
+
+## Test Plan (Manual)
+
+1. Run `yarn start` against a summit that has all four audience values represented across its ticket types.
+2. Open or create a discount-code promo code (Domain-Authorized or plain Summit Discount).
+3. Verify the checkbox label reads "Apply to all ticket types (Audience: All)".
+4. Verify the helper text "Only ticket types with Audience = All are covered..." appears beneath the checkbox.
+5. Check the box and save — code should save with no `ticket_types_rules`. Reload — checkbox should still be checked (derived state).
+6. Uncheck and use the picker to opt in a `WithPromoCode` ticket type — picker continues to expose it.
+
+## Acceptance Criteria
+
+- [ ] New label string conveys "Audience: All" scope clearly.
+- [ ] Helper text appears beneath the checkbox and names the three excluded audiences.
+- [ ] No FE logic change; no regression in existing form behavior.
+- [ ] Integration test asserts both label and helper text.
+- [ ] API counterpart (`summit-api/doc/promo-code-apply-to-all-audience-restriction.md`) merged or scheduled in the same release.
+
+## References
+
+- API SDS: `summit-api/doc/promo-code-apply-to-all-audience-restriction.md`
+- Parent feature plan (local): `docs/superpowers/plans/2026-04-21-promo-codes-for-early-registration-access.md`
+- Form file: `src/components/forms/promocode-form/forms/discount-base-pc-form.js`
+- i18n file: `src/i18n/en.json`
+- Reducer note explaining the UI-only flag: `src/reducers/promocodes/promocode-reducer.js:166-180`
+- ClickUp: 86b9vrpxp
diff --git a/src/components/forms/promocode-form/__tests__/promocode-form.integration.test.js b/src/components/forms/promocode-form/__tests__/promocode-form.integration.test.js
index 98c331f70..05cf847f7 100644
--- a/src/components/forms/promocode-form/__tests__/promocode-form.integration.test.js
+++ b/src/components/forms/promocode-form/__tests__/promocode-form.integration.test.js
@@ -634,3 +634,66 @@ describe("regression — handleSubmit must not dehydrate allowed_ticket_types",
});
});
});
+
+describe("DiscountBasePCForm — Apply to all Ticket Types audience restriction copy", () => {
+ // i18n-react renders raw keys in the jest env (no translator configured),
+ // so we match against the key strings, not the translated copy. See the
+ // existing comment around line 368 for the same convention.
+ const APPLY_ALL_KEY = "edit_promocode.apply_to_all_tix";
+ const HELPER_KEY = "edit_promocode.apply_to_all_tix_helper";
+
+ // Classes that route through DiscountBasePCForm and therefore should
+ // render both the (Audience: All) label and the helper-text row.
+ // The helper text belongs to the same col-md-4 column that hosts the
+ // apply_to_all_tix checkbox. Scope the selector to that column to avoid
+ // colliding with other `small.form-text.text-muted` rows (e.g. the
+ // allowed_email_domains caption on the DOMAIN_AUTHORIZED form).
+ const getApplyToAllHelper = (container) =>
+ container
+ .querySelector("#apply_to_all_tix")
+ .closest(".col-md-4")
+ .querySelector("small.form-text.text-muted");
+
+ it.each([
+ ["SUMMIT_DISCOUNT_CODE"],
+ ["MEMBER_DISCOUNT_CODE"],
+ ["SPEAKER_DISCOUNT_CODE"],
+ ["SPONSOR_DISCOUNT_CODE"],
+ ["SPEAKERS_DISCOUNT_CODE"],
+ ["DOMAIN_AUTHORIZED_DISCOUNT_CODE"]
+ ])(
+ "for %s: renders the Audience=All label and helper-text below the checkbox",
+ (class_name) => {
+ const { container } = renderForm(baseEntity({ class_name }));
+
+ const checkboxLabel = container.querySelector(
+ "label[for=\"apply_to_all_tix\"]"
+ );
+ expect(checkboxLabel).toBeInTheDocument();
+ expect(checkboxLabel.textContent).toBe(APPLY_ALL_KEY);
+
+ const helper = getApplyToAllHelper(container);
+ expect(helper).toBeInTheDocument();
+ expect(helper.textContent).toBe(HELPER_KEY);
+ }
+ );
+
+ it("renders the helper text even when apply_to_all_tix is unchecked", () => {
+ const { container } = renderForm(
+ baseEntity({
+ class_name: "SUMMIT_DISCOUNT_CODE",
+ apply_to_all_tix: false
+ })
+ );
+
+ // Checkbox state reflects the override.
+ const checkbox = container.querySelector("#apply_to_all_tix");
+ expect(checkbox).toBeInTheDocument();
+ expect(checkbox.checked).toBe(false);
+
+ // Helper text is always rendered — it's reference copy, not state-gated.
+ const helper = getApplyToAllHelper(container);
+ expect(helper).toBeInTheDocument();
+ expect(helper.textContent).toBe(HELPER_KEY);
+ });
+});
diff --git a/src/components/forms/promocode-form/forms/discount-base-pc-form.js b/src/components/forms/promocode-form/forms/discount-base-pc-form.js
index aa368b930..b0081856d 100644
--- a/src/components/forms/promocode-form/forms/discount-base-pc-form.js
+++ b/src/components/forms/promocode-form/forms/discount-base-pc-form.js
@@ -1,7 +1,7 @@
import React from "react";
import T from "i18n-react";
-import BasePCForm from "./base-pc-form";
import Input from "openstack-uicore-foundation/lib/components/inputs/text-input";
+import BasePCForm from "./base-pc-form";
import { DiscountTicketTable } from "../../../tables/dicount-ticket-table";
const DiscountBasePCForm = (props) => {
@@ -28,6 +28,9 @@ const DiscountBasePCForm = (props) => {
{T.translate("edit_promocode.apply_to_all_tix")}
+
+ {T.translate("edit_promocode.apply_to_all_tix_helper")}
+
{props.entity.apply_to_all_tix && (
diff --git a/src/components/forms/speakers-promo-code-spec-form.js b/src/components/forms/speakers-promo-code-spec-form.js
index 73085c346..a5516cace 100644
--- a/src/components/forms/speakers-promo-code-spec-form.js
+++ b/src/components/forms/speakers-promo-code-spec-form.js
@@ -14,10 +14,10 @@
import React from "react";
import T from "i18n-react/dist/i18n-react";
import { connect } from "react-redux";
-import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown"
-import Input from "openstack-uicore-foundation/lib/components/inputs/text-input"
-import PromocodeInput from "openstack-uicore-foundation/lib/components/inputs/promocode-input"
-import TagInput from "openstack-uicore-foundation/lib/components/inputs/tag-input"
+import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown";
+import Input from "openstack-uicore-foundation/lib/components/inputs/text-input";
+import PromocodeInput from "openstack-uicore-foundation/lib/components/inputs/promocode-input";
+import TagInput from "openstack-uicore-foundation/lib/components/inputs/tag-input";
import TicketTypesInput from "openstack-uicore-foundation/lib/components/inputs/ticket-types-input";
import BadgeFeatureInput from "../inputs/badge-feature-input";
import {
@@ -227,6 +227,9 @@ class SpeakerPromoCodeSpecForm extends React.Component {
{T.translate("edit_promocode.apply_to_all_tix")}
+
+ {T.translate("edit_promocode.apply_to_all_tix_helper")}
+
{!entity.applyToAllTix && (
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 5b87a0d74..42b6c822a 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -1000,7 +1000,8 @@
"allowed_ticket_types": "Ticket Types",
"amount": "Amount",
"rate": "Rate",
- "apply_to_all_tix": "Apply to all Ticket Types",
+ "apply_to_all_tix": "Apply to all ticket types (Audience: All)",
+ "apply_to_all_tix_helper": "Only ticket types with Audience = All are covered. WithInvitation, WithoutInvitation, and WithPromoCode ticket types must be added explicitly via the ticket-type picker after saving.",
"badge_features_apply_to_all_tix_retroactively": "Apply to Tickets already using this Promo Code ?",
"promocode_saved": "Promo Code saved successfully.",
"promocode_created": "Promo Code created successfully.",