diff --git a/fusion_plating/docs/superpowers/specs/2026-05-13-sticker-multi-part-and-variants-design.md b/fusion_plating/docs/superpowers/specs/2026-05-13-sticker-multi-part-and-variants-design.md
new file mode 100644
index 00000000..4c798b12
--- /dev/null
+++ b/fusion_plating/docs/superpowers/specs/2026-05-13-sticker-multi-part-and-variants-design.md
@@ -0,0 +1,439 @@
+# Sticker — Multi-part, Per-box, Internal/External Variants
+
+**Date:** 2026-05-13
+**Module(s):** `fusion_plating_jobs`, `fusion_plating_reports`
+**Author:** Gurpreet (Nexa Systems Inc.)
+**Status:** Approved — ready for implementation plan
+
+## Summary
+
+The box sticker (printed at SO level and at fp.job level) currently
+mishandles three real-world scenarios on multi-line orders:
+
+1. **Silent thickness/SN merge bug.** When two SO lines share
+ `(recipe, part, coating)` but differ in thickness or serial,
+ the current `_create_fp_jobs` grouping collapses them into one
+ `fp.job`. The job inherits the FIRST line's thickness/SN — the
+ other line's values are silently dropped from the sticker (and
+ eventually from the CoC).
+2. **No per-box stickers.** A line with `qty = 5` prints one
+ sticker showing `Qty: 5`. Operators want one physical label per
+ box, with a `1 / 5`, `2 / 5`, ... indicator.
+3. **No Internal variant.** The sticker always prints the
+ customer-facing description (`_line.name`) in the Notes column.
+ The shop floor wants a parallel variant that shows the
+ internal ops description (`_line.x_fc_internal_description`,
+ from Sub 2) instead.
+
+This spec covers all three as a single piece of work — they touch
+the same files and ship together.
+
+## Goals / non-goals
+
+**Goals**
+
+- Multi-thickness / multi-SN lines split into separate `fp.job`
+ records with correct WO-XXXXX-NN naming.
+- SO sticker and Job sticker render one page per physical box,
+ with a `Box X / N` indicator replacing the current `Qty: N`.
+- New "Internal" variant for each sticker that prints the internal
+ description in the Notes column. Existing variant becomes
+ "External".
+- Both variants share the same inner template — only the Notes
+ source differs.
+- Existing action XML IDs unchanged so bookmarks and binding
+ records keep working.
+
+**Non-goals**
+
+- Per-physical-box serial number tracking (today's `x_fc_serial_id`
+ is one per line, shared across all boxes in that line — that's
+ fine).
+- Box-count override (today: 1 sticker per qty unit; if the shop
+ packs 5 parts into 1 box, that's an operational choice the
+ sticker doesn't try to encode).
+- Migration of pre-existing single-line, single-thickness jobs —
+ they remain as-is.
+
+## Current state (post Sub 11)
+
+### Backend — `fusion_plating_jobs/models/sale_order.py`
+
+```python
+# Inside _create_fp_jobs(), the grouping key:
+key = (recipe.id, part_id, coating_id)
+groups[key] = groups.get(key, ...) | line
+```
+
+Lines that share ALL THREE collapse into one `fp.job`. Sub 11's
+comment explicitly calls out the part_id+coating_id check ("sharing
+only the recipe is not enough — would put Part A's number on a cert
+covering both") but doesn't extend the same reasoning to thickness
+or SN. The thickness Many2one (`x_fc_thickness_id`) and serial
+Many2one (`x_fc_serial_id`) were added in Sub 5, after the grouping
+logic was last touched.
+
+### Sticker — `fusion_plating_reports/report/report_fp_wo_sticker.xml`
+
+Two outer templates wrap a shared inner:
+
+- `report_fp_so_sticker` (bound to `sale.order` via
+ `action_report_fp_so_sticker`) — iterates
+ `so.order_line.filtered(lambda l: l.x_fc_part_catalog_id)`,
+ renders one inner per line.
+- `report_fp_job_sticker_template` (in
+ `fusion_plating_jobs/report/report_fp_job_sticker.xml`, bound to
+ `fp.job` via `action_report_fp_job_sticker`) — iterates `docs`,
+ renders one inner per job.
+
+Neither outer accounts for `qty > 1` — each line/job produces
+exactly one inner render.
+
+The inner template `report_fp_wo_sticker_inner` sets variables and
+renders one page. The Notes content is fixed:
+
+```xml
+
+```
+
+There is no way for an outer to override this — it's a hard read of
+`_line.name`.
+
+## Architecture — the three changes
+
+### Change 1 — Backend split: extend grouping key
+
+In `fusion_plating_jobs/models/sale_order.py`, in the method that
+builds the `groups` dict (currently `_create_fp_jobs` around line
+424–441), extend the key tuple:
+
+```python
+# Before
+key = (recipe.id, part_id, coating_id)
+
+# After
+thickness_id = (
+ 'x_fc_thickness_id' in line._fields
+ and line.x_fc_thickness_id.id
+) or False
+serial_id = (
+ 'x_fc_serial_id' in line._fields
+ and line.x_fc_serial_id.id
+) or False
+key = (recipe.id, part_id, coating_id, thickness_id, serial_id)
+```
+
+**Effect:** Lines that previously merged silently across different
+thicknesses or SNs now split into separate fp.jobs. WO-XXXXX-NN
+suffixes apply normally (driven by the existing
+`ordered_keys = sorted(...)` block — no change needed there).
+
+**Backwards compat:** Single-line SOs and same-(thickness, SN)
+multi-line SOs collapse identically to before. No data migration
+required.
+
+### Change 2 — Per-box render in the inner template
+
+`fusion_plating_reports/report/report_fp_wo_sticker.xml`, in the
+`report_fp_wo_sticker_inner` template:
+
+1. Move the variable-resolution + style block OUT of the per-page
+ render (these don't change per box, so they don't need to repeat).
+2. Wrap the `
` body in a box loop:
+
+```xml
+
+
+
+ ... existing structure ...
+
+
+```
+
+3. Change the Qty row's value column to show `X / N` when
+ `_qty_total > 1`:
+
+```xml
+
+ | Qty: |
+
+
+
+ /
+
+
+
+
+
+ |
+
+```
+
+**Outer templates supply `_qty_total`:**
+
+- SO outer: `_qty_total = line.product_uom_qty`
+- Job outer: `_qty_total = job.qty`
+
+If `_qty_total` is missing/zero, fall back to `1` so single-box
+behavior is unchanged.
+
+### Change 3 — Internal/External variants
+
+#### 3a. Inner template: override-or-fallback on `_notes_content`
+
+In `report_fp_wo_sticker_inner`, change the `_notes_content` set
+from a hard read to override-or-fallback (matches the existing
+pattern for `_so`, `_part`, etc.):
+
+```xml
+
+
+
+
+
+```
+
+External outer templates don't set `_notes_content` → falls through
+to `_line.name` (unchanged External behavior).
+
+Internal outer templates pre-set `_notes_content` before
+t-calling the inner:
+
+```xml
+
+```
+
+#### 3b. New outer templates + action records
+
+**SO Internal** — in `fusion_plating_reports/report/report_fp_wo_sticker.xml`:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**SO External** — existing `report_fp_so_sticker` template gets one
+addition: `
`.
+No other logic change (no `_notes_content` set = External default).
+
+**Job Internal** — in `fusion_plating_jobs/report/report_fp_job_sticker.xml`:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**Job External** — existing `report_fp_job_sticker_template`
+template gets one addition: `
`.
+
+**Action records — labels + new XML IDs**
+
+In `fusion_plating_reports/report/report_actions.xml`:
+
+```xml
+
+
+ External Sticker
+ ...
+
+
+
+
+ Internal Sticker
+ sale.order
+ qweb-pdf
+ fusion_plating_reports.report_fp_so_sticker_internal
+ fusion_plating_reports.report_fp_so_sticker_internal
+ 'Internal Sticker - %s' % (object.name or '').replace('/', '-')
+
+ report
+
+
+```
+
+In `fusion_plating_jobs/report/report_fp_job_sticker.xml`:
+
+```xml
+
+
+ External Job Sticker
+ ...
+
+
+
+
+ Internal Job Sticker
+ fp.job
+ qweb-pdf
+ fusion_plating_jobs.report_fp_job_sticker_internal_template
+ fusion_plating_jobs.report_fp_job_sticker_internal_template
+ 'Internal Job Sticker - %s' % (object.name or '').replace('/', '-')
+
+ report
+
+
+```
+
+## Files touched
+
+| # | File | Change |
+|---|------|--------|
+| 1 | `fusion_plating_jobs/models/sale_order.py` | Extend grouping key in `_create_fp_jobs` (+5 lines) |
+| 2 | `fusion_plating_reports/report/report_fp_wo_sticker.xml` | Inner template: box loop, Qty row logic, `_notes_content` fallback chain. SO outer: add `_qty_total`. NEW: SO Internal outer template. |
+| 3 | `fusion_plating_reports/report/report_actions.xml` | Rename existing SO action label. NEW: SO Internal action record. |
+| 4 | `fusion_plating_jobs/report/report_fp_job_sticker.xml` | Job outer: add `_qty_total`. Rename existing job action label. NEW: Job Internal outer template + action record. |
+| 5 | `fusion_plating_jobs/__manifest__.py` | Version bump |
+| 6 | `fusion_plating_reports/__manifest__.py` | Version bump |
+
+## Migration
+
+None required.
+
+- **New grouping key (`_create_fp_jobs`)** is purely additive —
+ existing jobs are protected by the existing
+ `if existing: return` idempotency guard. Single-line and
+ same-(thickness, SN) multi-line SOs collapse identically to
+ before.
+- **Existing XML IDs unchanged** — bookmarks / `binding_model_id`
+ records keep working. Only the visible label flips.
+- **New variants** appear in the Print menu on next module
+ upgrade with no data work.
+
+## Testing
+
+### Scenario 1 — Multi-thickness split (new fp.jobs)
+
+Create a new SO with two lines:
+- Line 10: Part A, Coating X, Thickness 0.3-0.5 mils, qty 2
+- Line 20: Part A, Coating X, Thickness 0.5-1.0 mils, qty 1
+
+Confirm SO → 2 fp.jobs are created:
+- `WO-XXXXX-01`: qty 2, thickness 0.3-0.5
+- `WO-XXXXX-02`: qty 1, thickness 0.5-1.0
+
+Print each job's External sticker → confirm correct thickness on each.
+
+### Scenario 2 — Per-box rendering
+
+Take Scenario 1's SO, click "Print → External Sticker" on the SO.
+
+Confirm: 3-page PDF.
+- Page 1: Line 10 box 1 → Qty row shows `1 / 2`
+- Page 2: Line 10 box 2 → Qty row shows `2 / 2`
+- Page 3: Line 20 box 1 → Qty row shows `1`
+
+### Scenario 3 — Internal variant
+
+On the same SO, click "Print → Internal Sticker".
+
+Confirm: same 3 pages, same WO#/PO#/Customer/Part#/SN/Thickness/Qty,
+but the Notes column shows `x_fc_internal_description` from each
+line instead of `name`.
+
+If `x_fc_internal_description` is blank on a line, Notes shows `-`.
+
+### Scenario 4 — Regression check (existing single-line)
+
+Re-print SO-30019 (1 line, qty 1) → External sticker prints
+single-page, no `X / N` indicator, Notes shows `_line.name` as
+before. Internal variant: single-page, Notes shows `x_fc_internal_description`
+or `-`.
+
+### Scenario 5 — Job-level multi-box
+
+Take any existing fp.job with `qty = 3`. Print External Job Sticker.
+
+Confirm: 3 pages, `1/3`, `2/3`, `3/3`. Internal Job Sticker also 3
+pages with the line's internal description in Notes.
+
+### Scenario 6 — Action menu visibility
+
+On a sale order Print menu: both "External Sticker" and
+"Internal Sticker" appear. On an fp.job Print menu: both
+"External Job Sticker" and "Internal Job Sticker" appear.
+
+## Out-of-scope items (deferred)
+
+- **Per-box SN registry.** Today `x_fc_serial_id` is one per line.
+ If the customer needs unique SNs per physical box (5 parts =
+ 5 SNs), build out an `fp.box.serial` registry that links to the
+ line. Out of scope for this spec — would need workflow design
+ (UI for assigning, where SNs print, etc.).
+- **Box count ≠ qty.** Some shops pack multiple parts per box.
+ Today this spec assumes 1 sticker per qty unit. If needed,
+ add an `x_fc_box_count` field on the line that defaults to qty
+ but can be overridden, and the sticker loops over box_count
+ instead. Defer until requested.
+- **Sticker preview UI in the form view.** No live preview today;
+ operators print + visually verify. Defer.
+
+## Open questions
+
+None — all decisions locked at spec time:
+
+| Q | Decision |
+|---|---|
+| Add SN to grouping key? | **Yes.** Same reasoning as thickness — silent merge of different SNs is a compliance hole. |
+| Per-box indicator location? | **Replace Qty row value.** Operator's confirmation: "we can use the quantity field portion for the box, there is room we can use rather than creating another line below and making everything smaller." |
+| Box indicator format? | **`1 / 5`** (slash, spaces around for legibility at 50pt). When qty=1, show plain `1` (no slash) — matches current behavior. |
+| Label naming convention? | **Prefix.** `External Sticker` / `Internal Sticker` (SO Print menu), `External Job Sticker` / `Internal Job Sticker` (fp.job Print menu). |
+| Migration for existing jobs? | **None.** Idempotency guard in `_create_fp_jobs` protects them. |
+| Existing action XML IDs? | **Unchanged.** Only labels rename — bookmarks/binding records survive. |
+| Fractional qty? | Cast to `int(qty)` — current behavior preserved. |
+| Qty=0 line? | Already filtered out by `lambda l: l.x_fc_part_catalog_id` (no part → no sticker). |