# 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-06-12

---

## 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 |

### Duplex / Single-Sided
| Field | Type | Notes |
|---|---|---|
| `duplex` | bool | 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.

`duplex` is in `properties_to_save` and `show_badge_back` is derived from it in
`ae_comp__badge_obj_view.svelte`. (Verified 2026-03-18)

Axonius events use `duplex = false` — single-sided printing only.

---

## 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
`<link>` element via `<svelte:head>` when `style_href` is populated:

```svelte
<svelte:head>
    {#if $lq__event_badge_template_obj?.style_href}
        <link
            rel="stylesheet"
            href={$lq__event_badge_template_obj.style_href}
        />
    {/if}
</svelte:head>
```

This is implemented — `style_href` loads via `<svelte:head>` in `print/+page.svelte` and is included in `properties_to_save`. (Verified 2026-03-18)

### 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.<code>` — 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 | CSS file | Description |
| --- | --- | --- | --- |
| `badge_4x5_fanfold` | 4" × 5" (101.6 × 127mm) | `badge_layout_epson_4x5_fanfold.css` | Epson ColorWorks C3500 / ExpoBadge fanfold — preferred for general conference use (ISHLT, demos) |
| `badge_3.5x5.5_pvc` | 3.5" × 5.5" (88.9 × 139.7mm) | `badge_layout_zebra_zc10l_pvc.css` | PVC card, Zebra ZC10L — single-sided, set `duplex=0` |
| `badge_4x6_fanfold` | 4" × 6" (101.6 × 152.4mm) | `badge_layout_epson_4x6_fanfold.css` | Single-sided fanfold; Axonius Adapt 2026 (June 2026). Lanyard hole: 5/8in × 1/8in, centered, 1/4in from top. |
| `badge_4x6_fanfold_tickets` | 4" × 6" + tear-offs | *(pending)* | Fanfold with ticket stubs |

Layout CSS files live in `src/lib/ae_events/badges/css/` and are imported by
`ae_comp__badge_obj_view.svelte`. Rules are scoped under `[data-layout="..."]` on the
wrapper so multiple layouts can coexist in the bundle without conflict.

`@page` paper size rules are injected per-layout from `print/+page.svelte <svelte:head>`
(attribute selectors cannot scope `@page` rules, so they're handled dynamically).

---

## cfg_json Reference

All keys are optional. Unknown keys are preserved on save (forward-compatible). Managed via the template form's **Advanced** and **Header & Branding** sections, or directly in phpMyAdmin.

### Visibility

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `hide_badge_header` | bool | `false` | Hides the entire header section (image + logo/text fallback). Auto-true when `background_image_path` is set, unless explicitly overridden. |
| `hide_badge_footer` | bool | `false` | Hides the badge type footer stripe. |
| `hide_title` | bool | `false` | Suppresses the professional title field on the badge front. |
| `hide_affiliations` | bool | `false` | Suppresses the affiliations field. |
| `hide_location` | bool | `false` | Suppresses the location field. |

### QR Codes

These keys override the top-level DB fields (`show_qr_front`, `show_qr_back`) when present. Prefer setting them here rather than the top-level fields.

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `show_qr_front` | bool | `false` | Show attendee QR on badge front. |
| `show_qr_back` | bool | `true` | Show attendee QR (+ ID text) on badge back. |

### Text Alignment

Stored under a nested `align` object.

```json
"align": { "name": "left", "title": "left", "affiliations": "left", "location": "center" }
```

| Key | Values | Default |
| --- | --- | --- |
| `align.name` | `left` \| `center` \| `right` \| `justify` | `center` |
| `align.title` | `left` \| `center` \| `right` \| `justify` | `center` |
| `align.affiliations` | `left` \| `center` \| `right` \| `justify` | `center` |
| `align.location` | `left` \| `center` \| `right` \| `justify` | `center` |

QR alignment stored under `qr_alignment`:

| Key | Values | Default |
| --- | --- | --- |
| `qr_alignment.front` | `left` \| `center` \| `right` | `center` |
| `qr_alignment.back` | `left` \| `center` \| `right` \| `justify` | `center` |

### Header Image

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `header_margin_top` | CSS length | `2rem` | Vertical offset of the header image. Negative = shift up. e.g. `"-0.25in"`, `"1rem"`. |
| `header_border_color` | hex color | none | Bottom border drawn below the header div. **Empty = no border.** e.g. `"#FE6111"`. |
| `header_border_width` | CSS length | `2px` | Thickness of the header bottom border. Only applied when `header_border_color` is set. |
| `header_padding_bottom` | CSS length | none | Space between the header image and the bottom border line. e.g. `"1.45in"`. |

### Appearance

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `body_text_color` | hex color | `#000000` | Inline color applied to all badge body text. |
| `bleed` | CSS length | none | Extends background image past card edges on all sides. Prevents white borders on printers that clip slightly inside the card. e.g. `"0.125in"`, `"3mm"`. |

### Text Zone Heights (`fit_heights`)

Per-layout height overrides for the auto-scaling text zones. Set any subset — unset keys fall back to the layout default. Useful when `background_image_path` is set and the designed zones don't align with code defaults.

```json
"fit_heights": { "grp_name_title": "1.8in", "name": "1.4in" }
```

| Key | Notes |
|---|---|
| `grp_name_title` | Height of the name+title container |
| `grp_name_title_flex` | Flex distribution: `around` \| `between` \| `even` \| `center` \| `start` \| `end` |
| `name` | Height of the name text zone |
| `title` | Height of the title text zone |
| `grp_aff_loc` | Height of the affiliations+location container |
| `grp_aff_loc_flex` | Flex distribution (same values as above) |
| `affiliations` | Height of the affiliations text zone |
| `location` | Height of the location text zone |

### Punch-Out Hole Markers (`punch_holes`)

Enables X overlays at the physical badge clip slot positions. Slots are pre-perforated on the badge stock — the markers print on the badge so attendees know where to push them out.

**Slot dimensions:** 5/8″ wide × 1/8″ tall, 1/4″ from top edge, 3/8″ from left/right edges. Center slot is horizontally centered.

```json
"punch_holes": { "left": true, "right": true, "center": false }
```

| Key | Default | Notes |
| --- | --- | --- |
| `punch_holes.left` | `false` | Left clip slot marker |
| `punch_holes.right` | `false` | Right clip slot marker |
| `punch_holes.center` | `false` | Center clip slot marker (less common) |

---

### Controls Panel (`controls_cfg`)

Controls which fields appear in the print controls panel for non-trusted users, and which fields authenticated users may edit. Trusted + Edit Mode always sees and can edit all fields regardless of this config.

```json
"controls_cfg": {
  "shown": ["name", "title", "affiliations"],
  "auth_editable": ["title", "affiliations", "location"]
}
```

| Key | Type | Default |
| --- | --- | --- |
| `controls_cfg.shown` | `string[]` | `["name", "title", "affiliations", "location"]` |
| `controls_cfg.auth_editable` | `string[]` | `["title", "affiliations", "location", "allow_tracking", "pronouns"]` |

Valid field keys: `name`, `title`, `affiliations`, `location`, `pronouns`, `allow_tracking`.

This config applies to the onsite print controls. Remote review currently uses
`event.mod_badges_json.edit_permissions` instead. Consolidating or defining precedence between
these two permission sources is tracked in `documentation/TODO__Agents.md`.

---

## 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:

- `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` is already saved to IDB and drives single-sided rendering.

---

## 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

---

## Print Layout Architecture

### How the print CSS works

The print page (`print/+page.svelte`) injects `<style>` blocks into `<svelte:head>` that
take effect only in `@media print`. Multiple layers of the SvelteKit layout chain must
be neutered to get a clean print surface.

**`#ae_main_content` — cannot dissolve, must passthrough:**
`#ae_main_content` has `overflow: auto` (it is the events layout scroll container). CSS
spec prohibits `display: contents` from overriding elements with overflow clipping —
Firefox enforces this strictly, Chrome is lenient. Workaround: strip all its visual/layout
effects with explicit `display: block; overflow: visible; position: static; width: 100%`.

**Wrappers dissolved via `display: contents` (safe — no overflow constraints):**
| Selector | Source | Why dissolved |
|---|---|---|
| `.main_content` | `events/+layout.svelte` | `pb-48`, `pt-20+`, `grow` |
| `#badge_render_area` | `print/+page.svelte` | Screen-only right-padding offset for controls panel |

**App chrome hidden via `print:hidden`:**
- `nav.submenu` (events layout nav bar)
- `footer.footer` (events layout footer)
- Scroll-to-top / scroll-to-bottom button div
- Kiosk header (`<header>` in print page)
- Controls panel (`<div>` fixed right in print page)
- Debug info section (edit mode only)
- Root layout: offline banner, session expired banner, hydration overlay, sys/debug menus

**Badge centering — `position: fixed`:**
`.event_badge_wrapper` uses `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%)`.
In print, `position: fixed` anchors relative to the `@page` content area, bypassing the entire
ancestor hierarchy (no containing-block height dependency, no overflow-clip interference).

**Future per-template margins:** `print_margin_cfg` is already parsed from `cfg_json`
in `print/+page.svelte`. A dynamic `@page { margin: ... }` injection can be built from
that value when a UI for it exists.

---

### Cross-browser print behavior — IMPORTANT

Verified 2026-03-12 by comparing print-to-PDF output from both browsers across multiple
print dialog settings.

#### `@page { size }` — paper size

| Browser | Save to PDF | Physical printer |
|---|---|---|
| **Firefox** | Paper size locked — cannot change in dialog; CSS `@page { size }` used ✅ | Can select paper size in dialog |
| **Chrome/Chromium** | Paper size locked — cannot change in dialog; uses system default (letter, A4, etc.) ❌ | Can select paper size under "More settings" |

Chrome intentionally does not honor `@page { size }` for Save as PDF. It uses the system
default paper size. This is a Chrome design decision, not a bug in our code.

For actual printing to Epson/Zebra hardware: the printer driver controls paper size from
the loaded badge stock. CSS `@page { size }` is advisory only. Real badge printing is
unaffected by Chrome's behavior.

Use Firefox for accurate print-to-PDF proofing — it produces a correctly-sized PDF that
matches the badge stock dimensions exactly.

#### Margins — Chrome "Default" causes layout problems

| Chrome margin setting | Result |
|---|---|
| **Default** | ❌ Adds URL, date, and page-number headers/footers into the printable area. These eat into the space that `position: fixed; top: 50%` references, making the badge appear off-center or clipped against the footer. |
| **None** | ✅ Correct — badge centered cleanly |
| **Minimum** | ✅ Correct — small margins, badge still centered |
| **Custom (reasonable values)** | ✅ Correct |

The badge content itself is **not** distorted. Verified: Chrome "None" margins on an A4
page produces the badge perfectly horizontally centered (page center 297.5 pts, badge
content center 297.5 pts). The CSS centering logic is correct.

**Staff guidance for Chrome:**
- Set **Margins → None** (or Minimum) in Chrome's print dialog.
- Optionally set paper size to match badge stock under "More settings" when printing to PDF.
- For physical printer: select correct paper size under "More settings".

Firefox users can use "Save to PDF" directly — it just works.

---

## 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 `<svelte:head>` CSS |
| `documentation/MODULE__AE_Events_Badges.md` | Badge object reference |

---

## Pending / TODO

- [x] Wire `style_href` via `<svelte:head>` in print page — done in `print/+page.svelte`; also in `properties_to_save`. (2026-03-18 verified)
- [x] Add `duplex` to `properties_to_save` — done. (2026-03-18 verified)
- [x] Add `duplex`-driven suppression to `badge_back` section — done in `ae_comp__badge_obj_view.svelte`; `show_badge_back` derived from `duplex` field.
- [x] `badge_4x6_fanfold` layout CSS created (`badge_layout_epson_4x6_fanfold.css`), imported in badge component, `@page 4in 6in` wired in print page. (2026-05-15)
- [x] Template form expanded — `layout`, `style_href`, `badge_type_list`, `duplex`, and all `cfg_json` keys now editable via the form. (2026-06-04)
- [x] `cfg_json.header_margin_top`, `header_border_color`, `header_border_width`, `header_padding_bottom` added — header image position and bottom border are fully configurable without a code deploy. (2026-06-04)
- [ ] Wire `badge_type_list` from the template into the badge search filter — currently the search form uses a hardcoded list. See `ae_comp__badge_search.svelte` TODO comment.
- [ ] `badge_4x5_fanfold` layout CSS exists but is stale (not used in 2+ years) — review against actual hardware before next use.
- [ ] Remove dead `exhibitor_info` / `presenter_info` / `staff_info` / `vip_info` / `vote_info` `{#if}` blocks from `ae_comp__badge_obj_view.svelte` (if they were carried over from v1)
