---
phase: 12
slug: offer-editor-tier-tag-prezzo-pubblico
status: draft
shadcn_initialized: true
preset: default
created: 2026-06-14
---

# Phase 12 — UI Design Contract
## Offer Editor — Tier A/B/C, Tag & Prezzo Pubblico

> Visual and interaction contract for the Offer Editor: list page + detail editor with 3-tier checkbox matrix, multi-dimensional tags, and structured transformation promise. Generated by gsd-ui-researcher, verified by gsd-ui-checker.

---

## Design System

| Property | Value |
|----------|-------|
| Tool | shadcn/ui |
| Preset | default (existing, Phase 11 forward) |
| Component library | radix |
| Icon library | Lucide React |
| Font | Geist Sans |
| Design direction | ClickUp/Pipedrive minimal flat style (per DESIGN-SYSTEM.md) |

**Notes:** Phase 12 builds on Phase 11's established database-view UX (inline edit, tag multi-select, EditableCell). All colors, spacing, typography inherited from Phase 11 and project globals. No new design system tokens introduced.

---

## Spacing Scale

Multiples of 4px per project standard:

| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Icon gaps, inline padding |
| sm | 8px | Compact element spacing, tag chip gaps |
| md | 16px | Default cell/control spacing, table row padding |
| lg | 24px | Section padding, card margin, tag dimension spacing |
| xl | 32px | Layout gaps between sections |
| 2xl | 48px | Major section breaks |
| 3xl | 64px | Page-level spacing |

Exceptions: None

---

## Typography

Phase 12 reuses Phase 11 typography (no new sizes introduced). Two weights only (400 regular, 600 bold):

| Role | Size | Weight | Line Height |
|------|------|--------|-------------|
| Body | 14px | 400 | 1.5 |
| Label | 12px | 400 | 1.5 |
| Heading (h3) | 16px | 600 | 1.2 |
| Heading (h2) | 20px | 600 | 1.2 |

---

## Color

Phase 12 uses Phase 11 color contract (invariant brand tokens):

| Role | Value | Usage |
|------|-------|-------|
| Dominant (60%) | #ffffff | Page/card backgrounds, editor surfaces |
| Secondary (30%) | #f9f9f9 | Muted backgrounds, table headers, category filter chips (inactive) |
| Accent (10%) | #1A463C | Primary CTAs ("Salva Offerta"), focus ring, active/selected state, active filter chips |
| Accent secondary | #DEF168 | Status indicators (archived badge), active offer state |
| Destructive | #dc2626 | Archive/delete actions only |
| Muted text | #71717a | Placeholders, metadata, disabled states, inactive labels |
| Border | #e5e7eb | Cell/block dividers, table borders |

**Accent reserved for:**
- "Salva Offerta" primary CTA button
- Focus ring on EditableCell, TagMultiSelect, and input fields
- Active state indicator (category filter chip, active tags)
- Selection highlight in checkbox matrix

---

## Components & Interaction Patterns

All patterns inherited from Phase 11 (EditableCell, TagMultiSelect, database-view table) and extended for offer composition and tier management.

### 1. Offer List Page (`/admin/offers`)

**Purpose:** Display all offers (active + optionally archived) in a filterable card grid. Admin can create new offers, filter by category, and toggle archive view.

**Layout Structure:**
```
┌──────────────────────────────────────────────────────────────┐
│ Offerte                                  [+ Nuova Offerta]    │
├──────────────────────────────────────────────────────────────┤
│ Filter row:                                                   │
│ [Tutti] [Entry Offer] [Signature Offer] [Retainer Offer]    │
│ Mostra offerte archiviate: [toggle off]                      │
│                                                               │
│ ┌──────────────┐  ┌──────────────┐  ┌──────────────┐         │
│ │ Offerta 1    │  │ Offerta 2    │  │ Offerta 3    │         │
│ │ Breve desc   │  │ Breve desc   │  │ Breve desc   │         │
│ │ [Signature]  │  │ [Entry]      │  │ [Retainer]   │         │
│ └──────────────┘  └──────────────┘  └──────────────┘         │
│                                                               │
│ [card 4] [card 5] [card 6]                                   │
└──────────────────────────────────────────────────────────────┘
```

#### A. Page Header
- **Title:** "Offerte" (h2, 20px/600, black)
- **Primary CTA:** "+ Nuova Offerta" button (bg #1A463C, text white, right-aligned)

#### B. Filter Row
- **Category filter chips:** Buttons for "Tutti", "Entry Offer", "Signature Offer", "Retainer Offer"
  - Inactive: border #e5e7eb, text #71717a, bg transparent
  - Active: border #1A463C, text #1A463C, bg transparent (or solid #1A463C text white — planner decides)
  - Spacing between chips: 8px (sm)
- **Archive toggle:** "Mostra offerte archiviate" text label + checkbox or toggle switch
  - Position: right side of filter row OR below filter chips on tablet/mobile
  - Label color: #71717a
  - Toggle: standard checkbox or `<Switch>` component from shadcn

#### C. Offer Card Grid
- **Grid layout:** 3 columns on desktop (≥1024px), 2 on tablet (768–1023px), 1 on mobile (<768px)
- **Card dimensions:** ~280px width, responsive height (min 140px)
- **Card structure:**
  ```
  ┌───────────────────────────┐
  │ Offerta Name (editable?)  │  ← h3/16px/600
  │                           │
  │ Breve descrizione         │  ← body/14px/400, 2 lines max, ellipsis
  │                           │
  │ [Categoria Chip]          │  ← single category badge
  │                           │
  │ [archived badge] [edit]   │  ← conditional
  └───────────────────────────┘
  ```
- **Card styling:**
  - Background: white (#ffffff)
  - Border: #e5e7eb, 1px
  - Padding: 16px (md)
  - Border-radius: 8px
  - Hover: `cursor-pointer`, subtle shadow (box-shadow: 0 4px 12px rgba(0,0,0,0.08))
- **Offer name:** Clickable, links to detail editor
- **Short description:** Body text, truncated at 2 lines
- **Category badge:** Single chip (Entry/Signature/Retainer), color-coded (TBD by planner; suggest brand palette shades)
- **Archived badge:** Red text "Archiviata" (only if offer is archived)
- **Edit icon:** Optional Lucide pencil icon (top-right corner), links to detail editor

#### D. Empty State
- When no offers exist: centered section with
  - Icon: Lucide briefcase or package (48px, #71717a)
  - Heading: "Nessuna offerta" (h3)
  - Copy: "Inizia creando la tua prima offerta" (body, #71717a)
  - CTA: "+ Nuova Offerta" button

---

### 2. Offer Editor (Detail Page) (`/admin/offers/[id]/edit` or modal)

**Purpose:** Create/edit a complete offer with 3 tiers (A/B/C), service composition via checkbox matrix, tag system, and transformation promise.

**Page Structure:**
```
┌───────────────────────────────────────────────────────────────┐
│ ← Indietro  |  Modifica Offerta                              │
├───────────────────────────────────────────────────────────────┤
│                                                               │
│ [Offerta Name — editable inline]   [Category] [Ticket]       │
│                                                               │
│ ┌─────────────────────────────────────────────────────────┐  │
│ │ Tags (4 dimensions):                                    │  │
│ │ Categoria: [Signature Offer]                           │  │
│ │ Ticket: [Low Ticket / Mid Ticket / High Ticket]       │  │
│ │ Tipo: [Audit] [Done For You] [+ Crea Tipo]           │  │
│ │ Obiettivo: [Primo Acquisto] [Up Scala] [+ Crea Obj]  │  │
│ └─────────────────────────────────────────────────────────┘  │
│                                                               │
│ [Divider]                                                    │
│                                                               │
│ SERVIZI INCLUSI                                             │
│ ┌─────────────────────────────────────────────────────────┐  │
│ │ Servizio          Prezzo    │    A    │    B    │    C  │  │
│ ├─────────────────────────────────────────────────────────┤  │
│ │ Service 1         €100      │ ☑ │ ☐ │ ☐ │          │  │
│ │ Service 2         €250      │ ☐ │ ☑ │ ☑ │          │  │
│ │ ...               ...       │...│...│... │          │  │
│ ├─────────────────────────────────────────────────────────┤  │
│ │ Totale Servizi    —         │€350│€500│€250│        │  │
│ ├─────────────────────────────────────────────────────────┤  │
│ │ Prezzo Pubblico   —         │€499│€699│€349│        │  │
│ └─────────────────────────────────────────────────────────┘  │
│                                                               │
│ [Divider]                                                    │
│                                                               │
│ PROMESSA DI TRASFORMAZIONE                                 │
│ ┌─────────────────────────────────────────────────────────┐  │
│ │ Aiuto:       [Client Ideal — editable inline]          │  │
│ │ A ottenere:  [Result — editable inline]                │  │
│ │ In:          [Estimated Time — editable inline]        │  │
│ │ Senza:       [Pain Point — editable inline]            │  │
│ │ Grazie a:    [Proprietary Method — editable inline]    │  │
│ └─────────────────────────────────────────────────────────┘  │
│                                                               │
│ [Salva Offerta] [Annulla] [Archivia]                        │
└───────────────────────────────────────────────────────────────┘
```

#### A. Header & Navigation
- **Back button:** "← Indietro" (left-aligned, links to `/admin/offers`)
- **Page title:** "Modifica Offerta" (h2, 20px/600) OR "Crea Offerta" if new

#### B. Offer Name & Quick Category/Ticket Display
- **Name field:** EditableCell (text), required, displayed as h3
- **Category chip:** Single-select tag (Entry/Signature/Retainer), visually prominent
- **Ticket chip:** Single-select tag (Low/Mid/High Ticket), visually secondary

#### C. Tags Block (4 Dimensions)

**Structure:**
- Section label: "Tag" (h3, 16px/600)
- Four rows (one per dimension):
  1. **Categoria** (single): "Entry Offer" / "Signature Offer" / "Retainer Offer"
  2. **Ticket** (single): "Low Ticket" / "Mid Ticket" / "High Ticket"
  3. **Tipo** (multi): "Audit", "Done For You", "+ Crea Tipo" (add new)
  4. **Obiettivo** (multi): "Primo Acquisto", "Up Scala Valore", "Lead Generation", "+ Crea Obiettivo"

**Styling per dimension:**
- Dimension label: 12px/400, bold, #71717a
- Tags/chips: Use inherited TagMultiSelect component (from Phase 11)
  - Single-select (Categoria, Ticket): radio-like, one active chip at a time
  - Multi-select (Tipo, Obiettivo): checkbox-like, multiple active chips
  - Color: deterministic hash-based colors (6-8 color palette, per Phase 11 pattern)
  - Add action: "+ Crea [Dimension Name]" link/button inline
- Spacing between rows: 16px (md)

#### D. Services Matrix (Tier A/B/C)

**Section label:** "Servizi Inclusi" (h3)

**Table structure:**
- **Columns:** Service Name | Unit Price | Tier A (checkbox) | Tier B (checkbox) | Tier C (checkbox)
- **Header row:**
  - Left: "Servizio" (12px/400, bold)
  - Cols: "Prezzo" (right-aligned) + "A", "B", "C" (centered, column headers)
  - Header background: #f9f9f9
  - Header border-bottom: #e5e7eb
- **Service rows:**
  - Service name (left, flex-grow)
  - Unit price (right-aligned, tabular nums, "€X,XX")
  - Checkbox per tier (centered)
    - Style: standard `<input type="checkbox">`
    - Color: #1A463C when checked
    - Interaction: Click to toggle; live total updates immediately
  - Row height: ~40px
  - Border-bottom: #e5e7eb
  - Hover: subtle background #f9f9f9 on the entire row
- **Services filtered by category:** Only show services whose `services.category` matches the offer's selected category (Categoria tag)
- **Row order:** Configurable (drag-to-reorder via @dnd-kit optional; planner decides)

**Totals rows** (sticky to bottom of table):
1. **Totale Servizi** (auto-calculated):
   - Row label: "Totale Servizi" (bold, 14px/600)
   - Per tier: SUM of unit_price for all services with checkbox ☑ in that tier
   - Format: "€X,XXX.XX"
   - Updates **live** as checkboxes are toggled (no page reload)
   - Color: bold text, #1a1a1a

2. **Prezzo Pubblico** (manual input):
   - Row label: "Prezzo Pubblico" (bold, 14px/600)
   - Per tier: EditableCell (number type)
   - Placeholder: "€0,00" or empty
   - Format: "€X,XXX.XX"
   - Interaction: Click to edit, blur/Enter to save, Esc to cancel
   - Independent of "Totale Servizi" — user can set any value
   - Validation: numeric, ≥ 0, max 2 decimals

**Empty state (no services matching category):**
- Placeholder text: "Nessun servizio disponibile per questa categoria" (#71717a)
- Table still visible but with placeholder row

---

#### E. Transformation Promise Block

**Section label:** "Promessa di Trasformazione" (h3)

**Structure:** 5 editable fields, each a key-value pair:
```
Aiuto:       [Client Ideal — input text, 60 chars]
A ottenere:  [Result — input text, 60 chars]
In:          [Time — input text, 30 chars, e.g. "3 mesi"]
Senza:       [Pain — input text, 60 chars]
Grazie a:    [Method — input text, 60 chars]
```

**Implementation:**
- Each field is an EditableCell (text type, optional)
- Field label (dim): 12px/400, color #71717a
- Input placeholder: "Aggiungi [field name]" (muted)
- No asterisk (all optional)
- Layout: horizontal label-value pairs, or stacked label-over-input depending on space
  - On desktop: consider 2-column or full-width labels
  - Tablet/mobile: stack vertically
- Spacing between fields: 16px (md)
- Background: optional subtle bg #f9f9f9, or transparent (planner decides)
- Border: none, or bottom-border only on each field
- Focus ring: #1A463C when in edit mode (EditableCell pattern)

---

#### F. Action Buttons

**Bottom of editor:**
- **[Salva Offerta]** (primary CTA)
  - Button: bg #1A463C, text white, 14px/600, padding 16px 24px
  - Position: left-aligned
  - On click: Submit form, validate all required fields (name + category + at least 1 service selected in 1 tier), save to DB, redirect to `/admin/offers`, show toast "Offerta salvata"
  - Disabled state: if no tiers have services selected, button disabled with tooltip "Seleziona almeno un servizio"

- **[Annulla]** (secondary CTA)
  - Button: border #e5e7eb, text #1a1a1a, no fill
  - On click: Discard unsaved changes, redirect to `/admin/offers`
  - Optional confirmation: if form is dirty, "Scartare le modifiche?" dialog

- **[Archivia]** (destructive action, visible only on edit, not on create)
  - Button: text #dc2626, no fill
  - On click: Toggle offer `is_archived` flag
  - Confirmation required: "Sei sicuro? L'offerta non sarà visibile nella lista." [Annulla] [Archivia]
  - After confirm: Save, show toast "Offerta archiviata", redirect to `/admin/offers`

**Spacing between buttons:** 8px (sm)

---

### 3. Tag Creation Inline ("Crea Tipo", "Crea Obiettivo")

**Interaction:** When user clicks "+ Crea [Dimension]", an input field appears inline (or in a small popover).

**Structure:**
- Text input: placeholder "Nome [Dimension]..."
- On Enter or click checkmark: Create tag (server action `addTagToOffer`), add to active tags list
- On Esc or click X: Cancel, close input
- On success: Input clears, new tag appears in the chip list with deterministic color

**Validation:**
- Required: tag name 1-30 chars
- Trimmed whitespace
- Case-insensitive duplicate check per dimension (warn: "Questo tag esiste già")

---

## Copywriting Contract

| Element | Copy | Context |
|---------|------|---------|
| **Offer list heading** | "Offerte" | Page title |
| **Create offer CTA** | "+ Nuova Offerta" | Primary action on list |
| **Back link** | "← Indietro" | Breadcrumb/nav on detail |
| **Archive toggle label** | "Mostra offerte archiviate" | Filter checkbox |
| **Edit page heading** | "Modifica Offerta" | Detail page title (or "Crea Offerta" if new) |
| **Tags section label** | "Tag" | Section heading |
| **Category dimension** | "Categoria" | Single-select label |
| **Ticket dimension** | "Ticket" | Single-select label |
| **Type dimension** | "Tipo" | Multi-select label |
| **Objective dimension** | "Obiettivo" | Multi-select label |
| **Create type link** | "+ Crea Tipo" | Inline tag creation |
| **Create objective link** | "+ Crea Obiettivo" | Inline tag creation |
| **Services section label** | "Servizi Inclusi" | Table heading |
| **Service column header** | "Servizio" | Table column |
| **Price column header** | "Prezzo" | Table column |
| **Tier headers** | "A", "B", "C" | Table columns (single letters) |
| **Services total label** | "Totale Servizi" | Calculated row |
| **Public price label** | "Prezzo Pubblico" | Manual input row |
| **Transformation block label** | "Promessa di Trasformazione" | Section heading |
| **Ideal client label** | "Aiuto" | Field label |
| **Result label** | "A ottenere" | Field label |
| **Duration label** | "In" | Field label (e.g., "3 mesi") |
| **Pain label** | "Senza" | Field label |
| **Method label** | "Grazie a" | Field label |
| **Save CTA** | "Salva Offerta" | Primary action (verb + noun) |
| **Cancel CTA** | "Annulla" | Secondary action |
| **Archive CTA** | "Archivia" | Destructive action (visible on edit only) |
| **Archive confirmation** | "Sei sicuro? L'offerta non sarà visibile nella lista." | Destructive confirmation copy + context |
| **Empty services message** | "Nessun servizio disponibile per questa categoria" | Conditional placeholder |
| **Empty offers message** | "Nessuna offerta" | List page empty state heading |
| **Empty offers hint** | "Inizia creando la tua prima offerta" | List page empty state body |
| **Save success toast** | "Offerta salvata" | Success feedback |
| **Archive success toast** | "Offerta archiviata" | Success feedback |
| **Save error toast** | "Errore nel salvataggio. Verifica i campi." | Error feedback |
| **Tag name placeholder** | "Nome tag..." | Create tag input |
| **Duplicate tag warning** | "Questo tag esiste già" | Validation error |
| **Disabled save tooltip** | "Seleziona almeno un servizio" | Constraint message |

---

## Interaction & State Management

### Checkbox Matrix Behavior
- **On checkbox toggle:** Live recalculation of "Totale Servizi" for that tier (no network call)
- **Debounce:** None — updates instant as user clicks
- **Persistence:** On "Salva Offerta" click, POST all tier compositions to backend
- **Validation on save:** At least 1 tier must have ≥1 service selected (backend confirms)

### Tag Dimensions
- **Categoria & Ticket:** Single-select (user can only have one value per offer)
  - UI: Radio-button-like chips or dropdown — planner decides
  - Switching to a different service category auto-filters the services matrix
- **Tipo & Obiettivo:** Multi-select (user can have 0..N values per offer)
  - UI: Checkbox-like chips (inherited TagMultiSelect pattern)
  - Tag creation inline: "+ Crea Tipo" / "+ Crea Obiettivo"
  - Duplicate check: case-insensitive per dimension (warn and skip)
  - Color: deterministic based on tag name hash (6-8 color palette, per Phase 11)

### Archive Workflow
- Archive is a toggle, not permanent deletion
- Archived offers are hidden from list by default (togglable via "Mostra offerte archiviate")
- On edit page, "Archivia" button only visible if offer is not already archived (or toggle text changes to "Ripristina")
- Confirmation required for destructive state change

### Edit State Management
- Form is dirty if any field changed since load
- On cancel with unsaved changes: confirmation dialog "Scartare le modifiche?"
- On back button navigation: same check (browser may warn too)
- Auto-save optional (planner decides) — recommend explicit "Salva" for clarity

---

## Visual States

### Offer Card States
| State | Visual |
|-------|--------|
| Default | White card, border #e5e7eb, normal shadow |
| Hover | `cursor-pointer`, shadow deepens (0 4px 12px rgba(0,0,0,0.08)) |
| Archived | Gray text, optional red "Archiviata" badge top-right |

### Checkbox States
| State | Visual |
|-------|--------|
| Unchecked | Empty square, border #e5e7eb |
| Checked | Filled square, bg #1A463C, white checkmark |
| Hover | Cursor pointer, subtle border darken |
| Disabled | Gray box, opacity-50, no cursor |

### Editable Field States (EditableCell)
| State | Visual |
|-------|--------|
| Display mode | Normal text, `cursor-pointer`, hover bg #f0f0f0 |
| Edit mode | Input with ring-1 ring-#1A463C on focus |
| Error | Ring-1 ring-#dc2626, error message below |
| Disabled | Gray text, opacity-50, no cursor |

### Tag States (TagMultiSelect)
| State | Visual |
|-------|--------|
| Display | Colored chip (6-8 colors), text inside, X icon to remove |
| Hover | Slight opacity change on X icon |
| Add mode | Input field + "+ Aggiungi" button (or Enter to confirm) |
| Create new | Temporary placeholder, then colored chip with new tag name |

### Category Filter Chip States (List Page)
| State | Visual |
|-------|--------|
| Inactive | Border #e5e7eb, text #71717a, bg transparent, `cursor-pointer` |
| Active | Border #1A463C, text #1A463C (or bg #1A463C text white — planner decides) |
| Hover | Border darken slightly |

---

## Responsive Behavior

**Desktop (≥1024px):**
- Offer list: 3-column card grid
- Filter chips: horizontal row
- Services matrix: full-width table, horizontal scroll if needed
- Editor sections: full-width, stacked vertically

**Tablet (768px–1023px):**
- Offer list: 2-column card grid
- Filter chips: wrap horizontally or scroll if needed
- Services matrix: horizontal scroll (sticky left column optional)
- Tag fields: stack or 2-column layout (planner decides)

**Mobile (<768px):**
- Offer list: 1-column card stack
- Filter chips: vertical stack OR horizontal scroll in a container
- Services matrix: horizontal scroll with sticky "Servizio" + "Prezzo" columns
- Tag fields: stack vertically
- Buttons: full-width stack
- Archive toggle: move below filter chips

---

## Registry Safety

| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | button, input, dialog, badge, table, form, checkbox | not required (official shadcn) |
| @dnd-kit | core, sortable, utilities (optional for row reordering) | not required (npm package, not registry) |

---

## Checker Sign-Off

- [ ] Dimension 1 Copywriting: All labels/CTAs use verb+noun, destructive actions clear ("Archivia" + confirmation), tag creation inline
- [ ] Dimension 2 Visuals: Checkbox matrix clear, live totals update instant, archive badge visible, tag colors consistent
- [ ] Dimension 3 Color: Accent (#1A463C) reserved for primary CTA ("Salva Offerta"), focus ring, active filter chip; destructive (#dc2626) for archive only
- [ ] Dimension 4 Typography: Body 14px/400, labels 12px/400, headings 16–20px/600 — all per Phase 11 contract (2 weights only)
- [ ] Dimension 5 Spacing: All gaps/padding multiples of 4px, card padding 16px (md), section gaps 24px (lg), row height ~40px
- [ ] Dimension 6 Registry Safety: shadcn official + npm packages, no third-party registries

**Approval:** pending

---

## Notes for Planner

1. **Service filtering by category:** Implement dynamic filter on the services matrix when Categoria tag changes. Query: fetch all services WHERE `services.category = offer.categoria_tag_value`.

2. **Live total calculation:** Client-side state, no server call. Recalculate sum of unit prices for all checked services in each tier on every checkbox toggle.

3. **Three tiers (A/B/C):** Map to `offer_micros` table, one micro per tier. Store which services are assigned via new `offer_tier_services` junction table (additive, not modifying legacy `offer_micro_services`).

4. **Tag dimensions:** Use Phase 11's polymorphic `tags` table with `entity_type = "offer_macros"`. Add optional `dimension` column to distinguish Categoria/Ticket/Tipo/Obiettivo (or use tag name prefixes, planner decides). Single-select for Categoria/Ticket likely stored on `offer_macros` columns directly (simpler).

5. **Tag creation on the fly:** User can create new tag values during editing. Server action: `addTagToOffer(offerId, dimension, tagName)`. Validation + idempotency (onConflictDoNothing).

6. **Archive flag:** Add boolean `is_archived` column to `offer_macros` (additive migration). Filter in list query: WHERE is_archived = false (or include if toggle is on).

7. **Edit page routing:** Consider `/admin/offers/[id]/edit` (new page) or modal overlay. Both work; full-page simpler for first iteration.

8. **Services matrix columns:** Each tier (A/B/C) is visually a column with checkboxes. Sticky header recommended. Sticky left column (service name + price) optional on mobile/tablet.

9. **Transformation promise fields:** All optional. Can be empty string. EditableCell (text) pattern from Phase 11.

10. **Disable "Salva" button validation:**
    - Rule: at least 1 tier must have ≥1 service checked
    - If all tiers unchecked: button disabled with tooltip
    - On submit: backend also validates (server-side check)

11. **Category filter on list:** Clicking a chip sets active filter. API: `GET /admin/offers?category=entry` (or all if "Tutti"). Auto-refresh list.

12. **Migration strategy:** Additive only. Do NOT modify existing `offer_macros`, `offer_micros`, `offer_micro_services` — extend with:
    - `offer_tier_services` (new junction: tier_id → service_id)
    - `offer_macros.is_archived` (boolean, default false)
    - Optionally: structured promise fields on `offer_macros` (cliente_ideale, risultato, tempo, pain, metodo)
    - Optionally: `services.category` → verify not already used (check Phase 11 usage first)

13. **Dry run before prod:** Test with existing offer data. Verify no data loss. Run audit script to confirm migration integrity.

---

## Requirements Traceability

| Requirement | Phase 12 Coverage |
|-------------|------------------|
| OFFER-11 | Offer editor: checkbox matrix, 3 tiers (A/B/C), live totals ✓ |
| OFFER-15 | 4-dimensional tags (Categoria/Ticket/Tipo/Obiettivo), creatable on-the-fly ✓ |
| OFFER-16 | Manual "Prezzo Pubblico" per tier, independent of service total ✓ |
| OFFER-17 | Structured transformation promise (5 fields) ✓ |
| OFFER-18 | List page filterable by category, archive toggle ✓ |

---

## Deferred Items

- **CSV/Notion import** (OFFER-12): Deferred to future phase per user decision (2026-06-14)
- **Drag-to-reorder services** (optional @dnd-kit): Not required for MVP; can be added in post-phase enhancement

---

*UI-SPEC regenerated: 2026-06-14*
*Design system source: Phase 11 (inherited), no new tokens*
*Scope: Offer Editor (list + detail) with 3-tier checkbox matrix, multi-dimensional tags, manual pricing, transformation promise*
