diff --git a/documentation/MODULE__AE_Events_Badge_Templates.md b/documentation/MODULE__AE_Events_Badge_Templates.md
new file mode 100644
index 00000000..6d7bea23
--- /dev/null
+++ b/documentation/MODULE__AE_Events_Badge_Templates.md
@@ -0,0 +1,276 @@
+# MODULE: Aether Events — Badge Templates
+
+**Module Path:** `src/routes/events/[event_id]/(badges)/templates/`
+**API Module:** `src/lib/ae_events/ae_events__event_badge_template.ts`
+**Database Table:** `event_badge_template`
+**Last Updated:** 2026-03-02
+
+---
+
+## Overview
+
+Badge templates define the visual and structural configuration for printing event badges.
+Each template applies to one category of badge (e.g., general attendees, workshops,
+exhibitors). An event typically has 1–3 templates.
+
+**Key principle:** One template per badge stock type/audience. Do not use flags on a
+single template to drive multiple layouts — create a separate template instead.
+
+**Common template sets:**
+- **General Attendees** — main conference badge (most attendees)
+- **Workshops / Pre-conference** — alternate header, possibly different badge type list
+- **Exhibitors** — distinct footer stripe colors, exhibitor-specific badge types
+
+Each template uses the same physical badge stock and printer configuration.
+
+---
+
+## DB Field Reference
+
+### Core Identity
+| Field | Type | Notes |
+|---|---|---|
+| `id` | int | Internal PK |
+| `id_random` | str | External-facing ID (AE Triple ID pattern) |
+| `event_id` | str | Parent event |
+| `name` | str | Template display name |
+| `description` | str | Optional description |
+
+### Image Assets
+| Field | Type | Notes |
+|---|---|---|
+| `logo_path` | str (URL) | Org logo — fallback when no header image |
+| `logo_filename` | str | **Deprecated** — redundant with logo_path; do not use |
+| `header_path` | str (URL) | Front-of-badge header image — primary branding |
+| `secondary_header_path` | str (URL) | Back-of-badge header image (falls back to header_path) |
+| `footer_path` | str (URL) | Optional footer image — rarely used |
+| `header_row_1` | str/HTML | Text fallback line 1 when no header image |
+| `header_row_2` | str/HTML | Text fallback line 2 |
+| `footer_title`, `footer_left`, `footer_right` | str | Legacy Flask-era fields — not used |
+| `header_background`, `footer_background` | str | Legacy — not used; do not add to new templates |
+
+### Network / WiFi
+| Field | Type | Notes |
+|---|---|---|
+| `wireless_ssid` | str | WiFi network name — displayed on badge back |
+| `wireless_password` | str | WiFi password — displayed on badge back |
+
+### QR Code Behavior
+| Field | Type | Notes |
+|---|---|---|
+| `show_qr_front` | bool (0/1) | Show attendee QR code on front of badge |
+| `show_qr_back` | bool (0/1) | Show attendee QR code (+ ID text) on back of badge |
+
+### Badge Type List
+| Field | Type | Notes |
+|---|---|---|
+| `badge_type_list` | JSON string | List of `{code, name}` objects for this template |
+
+**Format:**
+```json
+[
+ {"code": "current_member", "name": "Member"},
+ {"code": "guest", "name": "Guest"},
+ {"code": "staff", "name": "Staff"},
+ {"code": "test", "name": "Test"}
+]
+```
+
+The badge type footer stripe color is driven by CSS rules targeting the `code` value
+as a class on the footer element. Each event/template defines its own list — there is
+no global default. The component derives this list from the template at render time.
+
+### Ticket Definitions
+| Field | Type | Notes |
+|---|---|---|
+| `ticket_list` | JSON string | List of `{num, code, name}` for this template's tickets |
+| `ticket_1_text` – `ticket_8_text` | HTML | Ticket block HTML printed on badge back |
+
+**ticket_list format:**
+```json
+[
+ {"num": 1, "code": "foundation_reception", "name": "Foundation Reception"},
+ {"num": 2, "code": "volunteer_reception", "name": "Volunteer Reception"}
+]
+```
+
+The `ticket_N_code` field on the badge object references a ticket by its `code`. The
+corresponding `ticket_N_text` on the template provides the HTML rendered on the badge.
+
+### Print Layout / Styling
+| Field | Type | Notes |
+|---|---|---|
+| `layout` | str | Layout code — see Layout Codes below |
+| `style_filename` | str | CSS filename for locally-served stylesheets |
+| `style_href` | str (URL) | **Preferred** — external URL for custom CSS |
+| `script_src` | str (URL) | **Do not use** — Flask-era arbitrary script injection |
+
+### Access Control
+| Field | Type | Notes |
+|---|---|---|
+| `passcode` | str | Shared passcode for template management access |
+| `enable` | bool | Standard AE enable flag |
+| `hide` | bool | Standard AE hide flag |
+| `priority`, `sort`, `group` | int/str | Standard AE sort fields |
+| `notes` | str | Internal notes |
+
+### New Field (pending backend addition)
+| Field | Type | Notes |
+|---|---|---|
+| `duplex` | bool | **Planned** — when `false`, back section is hidden from print (`@media print`) |
+
+The `duplex` field controls whether the back-of-badge section renders during printing.
+When `false` (single-sided), `badge_back` gets `print:hidden` applied so only the front
+prints. The back section still displays on screen for configuration reference.
+
+The first event using this system (Axonius, NYC, mid-April 2026) uses single-sided PVC
+cards on a Zebra ZC10L — `duplex` will be `false` for that event's templates.
+
+---
+
+## External CSS Approach
+
+### Why External
+
+Badge templates may need visual adjustments mid-event (e.g., a color correction, a
+footer fix) without deploying a new SvelteKit build. Hosting the CSS at an external URL
+allows changes to take effect on next page load without any deployment.
+
+### How It Works
+
+The `style_href` field contains a full URL to a CSS file hosted on the static server
+(e.g., `https://static.oneskyit.com/c/ISHLT/css/badges_custom_ishlt.css`).
+
+The print page (`print/+page.svelte`) or the badge view should conditionally add a
+`` element via `` when `style_href` is populated:
+
+```svelte
+
+ {#if $lq__event_badge_template_obj?.style_href}
+
+ {/if}
+
+```
+
+This is not yet implemented — tracked as a pending Phase 1 item.
+
+### CSS Scope
+
+External badge CSS should scope all rules under `.badge_front`, `.badge_back`, etc.
+to avoid bleeding into the rest of the app. The classes used in
+`ae_comp__badge_obj_view.svelte` are the canonical hook points:
+
+- `.badge_front` — entire front card
+- `.badge_back` — entire back card
+- `.badge_header` — front header area
+- `.badge_body` — front content area (name, title, affiliations, location)
+- `.badge_footer` — front footer stripe
+- `.badge_back_header` — back header area
+- `.badge_back_content` — back content area
+- `.badge_footer_center.` — footer text per badge type code (for color stripes)
+
+### layout field
+
+The `layout` field encodes physical badge stock dimensions. Standard codes to use:
+
+| Code | Dimensions | Description |
+|---|---|---|
+| `badge_3.5x5.5_pvc` | 3.5" × 5.5" (88.9 × 139.7mm) | PVC card, Zebra ZC10L |
+| `badge_4x6_fanfold` | 4" × 6" (101.6 × 152.4mm) | Fanfold paper badge |
+| `badge_4x6_fanfold_tickets` | 4" × 6" + tear-offs | Fanfold with ticket stubs |
+
+The current badge component is hard-coded to `w-[4in]` / `min-h-[6.0in]`. These
+dimensions will need to be made template-driven once the `layout` field is wired up.
+For now, override via print CSS in the `style_href` stylesheet.
+
+---
+
+## Template-Derived Features (component behavior)
+
+### badge_type_list → badge type select
+The badge type dropdown shown when editing a badge is derived from the template's
+`badge_type_list` JSON, not a hardcoded list. This was a bug (fixed 2026-03-02).
+See `ae_comp__badge_obj_view.svelte` — `badge_type_code_li` is now `$derived.by()`.
+
+### "Info section" flags (exhibitor_info, presenter_info, etc.)
+These flags (`exhibitor_info`, `presenter_info`, `staff_info`, `vip_info`, `vote_info`)
+**do not exist as DB columns**. They appeared as placeholder `{#if}` blocks in the
+badge view component from Flask-era development and were never implemented.
+
+The correct approach is **one template per badge audience** — an Exhibitor template will
+have exhibitor-specific `badge_type_list`, header images, and CSS. No flags needed.
+
+The dead `{#if $lq__event_badge_template_obj.exhibitor_info}` blocks in
+`ae_comp__badge_obj_view.svelte` should be removed in a future cleanup pass.
+
+---
+
+## Properties Saved to IDB (Dexie)
+
+The `properties_to_save` array in `ae_events__event_badge_template.ts` controls what
+gets cached locally. Current state — fields **NOT** in properties_to_save that exist
+in DB and may be needed:
+
+- `style_href` — needed once external CSS is wired via ``
+- `passcode` — not needed client-side
+- `footer_title`, `footer_left`, `footer_right` — not needed (legacy)
+- `header_background`, `footer_background` — not needed (legacy)
+- `script_src` — do not add; this field should not be used
+- `duplex` — **add when backend adds the field**
+
+---
+
+## Standard Template Setup (per event)
+
+### 1. General Attendees template
+- `header_path`: event-specific conference header image
+- `secondary_header_path`: back-of-badge header (often same or related image)
+- `wireless_ssid` + `wireless_password`: venue WiFi
+- `show_qr_back`: `1` (back QR is standard for most events)
+- `show_qr_front`: `0` (usually off for front)
+- `badge_type_list`: full list of member/guest/staff/test types
+- `ticket_list` + `ticket_N_text`: event-specific tickets if applicable
+- `style_href`: client-specific CSS URL
+- `layout`: appropriate layout code
+- `duplex`: `1` (or `0` for single-sided events like Axonius 2026)
+
+### 2. Workshop / Pre-conference template
+- Same as above but with workshop-specific header images
+- `badge_type_list`: reduced list (workshop-relevant types only)
+- `ticket_list`: may be empty `[]`
+- `duplex`: match main template
+
+### 3. Exhibitor template
+- Exhibitor-specific header images
+- `badge_type_list`: exhibitor-only types (`ex_all`, `ex_booth`, `guest`, `staff`, `test`)
+- `ticket_list`: `[]` (exhibitors typically don't have event tickets)
+- `show_qr_back`: may be `0` (exhibitors scan others, they don't need their own QR prominent)
+- `wireless_ssid` + `wireless_password`: same venue WiFi
+
+---
+
+## Related Files
+
+| File | Role |
+|---|---|
+| `ae_events__event_badge_template.ts` | API + IDB functions; `properties_to_save` |
+| `db_events.ts` | Dexie schema for `badge_template` table |
+| `templates/+page.svelte` | Template list + create/edit/delete UI |
+| `templates/ae_comp__badge_template_form.svelte` | Template create/edit form |
+| `[badge_id]/ae_comp__badge_obj_view.svelte` | Badge render — consumes template data |
+| `[badge_id]/print/+page.svelte` | Print page — loads template, hosts `` CSS |
+| `documentation/MODULE__AE_Events_Badges.md` | Badge object reference |
+
+---
+
+## Pending / TODO
+
+- [ ] Wire `style_href` via `` in print page
+- [ ] Add `duplex` to `properties_to_save` once backend field exists
+- [ ] Add `duplex`-driven `print:hidden` to `badge_back` section in `ae_comp__badge_obj_view.svelte`
+- [ ] Make `layout` field drive actual card dimensions in the badge component (currently hard-coded)
+- [ ] Remove dead `exhibitor_info` / `presenter_info` / `staff_info` / `vip_info` / `vote_info` `{#if}` blocks from `ae_comp__badge_obj_view.svelte`
+- [ ] Improve `ae_comp__badge_template_form.svelte` to edit all relevant fields (currently minimal)
diff --git a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte
index d990b518..c0101dc6 100644
--- a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte
+++ b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte
@@ -65,22 +65,18 @@
// let ae_tmp: key_val = $state({});
let ae_triggers: key_val = $state({});
- // ISHLT 2024 badge type codes
- let badge_type_code_li = [
- { code: 'current_member', name: 'Member' },
- { code: 'inactive_member', name: 'Non-Member' },
- { code: 'current_member_trainee', name: 'Trainee Member' },
- { code: 'inactive_member_trainee', name: 'Trainee Non-Member' },
- { code: 'ex_all', name: 'Exhibitor All Access' },
- { code: 'ex_booth', name: 'Exhibitor Booth Staff' },
- { code: 'hftx', name: 'HFTX Master Academy' },
- { code: 'mcs', name: 'MCS Master Academy' },
- { code: 'pediatric', name: 'Pediatric' },
- { code: 'guest', name: 'Guest' },
- { code: 'staff', name: 'Staff' },
- { code: 'volunteer', name: 'Volunteer' },
- { code: 'test', name: 'Test' }
- ];
+ // Badge type list is derived from the template — each event/template defines its own set.
+ // Falls back to empty array if template not loaded or badge_type_list is invalid JSON.
+ let badge_type_code_li = $derived.by(() => {
+ const raw = $lq__event_badge_template_obj?.badge_type_list;
+ if (!raw) return [];
+ try {
+ return JSON.parse(raw);
+ } catch {
+ console.warn('badge_type_list is not valid JSON:', raw);
+ return [];
+ }
+ });
// *** Set initial variables
$effect(() => {
@@ -999,7 +995,7 @@ onkeypress={() => {
"
title={editable_badge_type_code}
>
- {#if edit_mode_active && badge_type_code_li}
+ {#if edit_mode_active && badge_type_code_li.length > 0}