diff --git a/docs/plans/2026-04-24-ux-polish-plan.md b/docs/plans/2026-04-24-ux-polish-plan.md
new file mode 100644
index 0000000..eb916fd
--- /dev/null
+++ b/docs/plans/2026-04-24-ux-polish-plan.md
@@ -0,0 +1,742 @@
+# UX Polish Pass — Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Auto mode is active — execute continuously, no mid-execution checkpoints.
+
+**Goal:** Shorter adjustment-type labels in tables (display-only, DB untouched), uniform semantic badge colours across all payroll tabs, a CSS bug fix on the group-summary row, and two new documentation artifacts to prevent future confusion.
+
+**Architecture:** Path A — display-only rename. Every DB value stays exactly as it is today; only the human-readable labels change via the second tuple element of `TYPE_CHOICES`. Unifies badge colours by replacing ~3 branches of Bootstrap-state-class conditionals with the existing `.badge-type-*` semantic palette. Moves one CSS property (`display: flex`) from a `
` (where it silently breaks table rendering) to a `` (where it does what the original author intended).
+
+**Tech Stack:** Django 5.2.7 (TYPE_CHOICES + get_type_display + auto-generated AlterField migration); custom.css semantic palette (already in place); Django template filter `type_slug` (already in place). No new libraries.
+
+**Design doc:** `docs/plans/2026-04-24-ux-polish-design.md` (committed as `9aba9b8`).
+
+**Starting HEAD:** `9aba9b8` on branch `ai-dev`.
+
+**Expected net change:** ~120-180 lines across 6 files + 1 new doc + 1 auto-generated migration.
+
+---
+
+## Critical context for every task
+
+**The naming drift:** After this pass, what the user SEES and what the DATABASE stores for `PayrollAdjustment.type` will diverge permanently. Learn this before touching anything:
+
+| DB value (CANONICAL) | Display label (new) |
+|---|---|
+| `'New Loan'` | `Loan` |
+| `'Advance Payment'` | `Advance` |
+| `'Advance Repayment'` | `Advance Repaid` |
+
+**Never change the left column.** All logic, constants (`ADDITIVE_TYPES`), tests, CSS class slugs (`.badge-type-new-loan`), and `data-type="..."` attributes use the left column.
+
+**Two kinds of template usage of `adj.type`:**
+1. **Visible text** (renders letters on the page): `{{ adj.type }}` → change to `{{ adj.get_type_display }}`
+2. **Identifier** (feeds a CSS class via `|type_slug`, or a `data-type=` attribute, or a control-flow `{% if %}`): keep as `{{ adj.type }}` — the raw DB value is the right thing to emit
+
+The implementer for Tasks 3 and 4 MUST grep both patterns before editing and make the call case-by-case.
+
+---
+
+## Task 1: Create `docs/design-tokens.md` (canonical colour reference)
+
+**Goal:** A single doc that lists every semantic colour token in the app, where it's used, and how to add a new one. Do this BEFORE any colour code-changes so the reference exists first.
+
+**Files:**
+- Create: `docs/design-tokens.md`
+
+**Step 1: Create the file with the full content**
+
+Write the following to `docs/design-tokens.md` (verbatim):
+
+````markdown
+# Design Tokens — Semantic Colour Palette
+
+_Last reviewed: 24 Apr 2026._
+
+## How colours are structured
+
+The app has TWO colour categories — they MUST NOT share colours:
+
+1. **Type-of-adjustment** — 7 types × 2 themes. Used wherever a
+ `PayrollAdjustment` is shown as a badge or a group-header accent.
+ Token naming: `--badge--bg` / `--badge--fg`.
+2. **Transactional state** — Bootstrap's `bg-success` /
+ `bg-warning` / `bg-danger`. Used for Paid, Unpaid, Overdue —
+ the payment lifecycle, not the kind of adjustment.
+
+Mixing the two would make a green badge mean both "this is a Bonus"
+AND "this is Paid" — the user would lose the ability to read the
+colour as a signal. Keep the categories separate.
+
+## Type-of-adjustment tokens
+
+| DB type (canonical) | Displayed as | Dark BG | Dark FG | Light BG | Light FG | CSS class |
+|---|---|---|---|---|---|---|
+| Bonus | Bonus | `#5b8260` | `#e8f3ea` | `#d7e8d9` | `#385640` | `.badge-type-bonus` |
+| Overtime | Overtime | `#a16881` | `#fce4ec` | `#f3d1dd` | `#703347` | `.badge-type-overtime` |
+| Deduction | Deduction | `#5b4f8c` | `#e0daf3` | `#d8d0ef` | `#3b2f6d` | `.badge-type-deduction` |
+| New Loan | Loan | `#9b7f39` | `#fef4d1` | `#f0dc9d` | `#6a5320` | `.badge-type-new-loan` |
+| Loan Repayment | Loan Repayment | `#b48a1a` | `#fef4d1` | `#f7d873` | `#5a4418` | `.badge-type-loan-repayment` |
+| Advance Payment | Advance | `#3e5c7b` | `#d7e5f2` | `#bccee0` | `#243b56` | `.badge-type-advance-payment` |
+| Advance Repayment | Advance Repaid | `#2f679a` | `#d7e5f2` | `#9ec1dd` | `#1d3550` | `.badge-type-advance-repayment` |
+
+Token definitions live in `static/css/custom.css`:
+- Dark theme: `:root { ... }` block around lines 85-91
+- Light theme: `[data-theme="light"] { ... }` block around lines 149-155
+
+## Where each colour appears
+
+| Semantic | Used by |
+|---|---|
+| `--badge-bonus-*` (green) | Adjustments tab type badge; By-Type group-header left-border accent |
+| `--badge-overtime-*` (mauve) | Adjustments tab type badge; By-Type group-header accent |
+| `--badge-deduction-*` (purple) | Adjustments tab type badge; By-Type group-header accent |
+| `--badge-loan-*` (amber/yellow) | Adjustments tab type badge; By-Type group-header accent; Pending tab "Loan" worker flag (`.loan-flag-badge`) |
+| `--badge-loan-rep-*` (deeper amber, +15% saturation) | Adjustments tab type badge for Loan Repayment; By-Type group-header accent |
+| `--badge-advance-*` (blue) | Adjustments tab type badge; By-Type group-header accent |
+| `--badge-advance-rep-*` (deeper blue, +15% saturation) | Adjustments tab type badge for Advance Repayment; By-Type group-header accent |
+
+## Transactional-state colours (Bootstrap — unchanged)
+
+| Use | Class |
+|---|---|
+| Paid payslip badge | `bg-success` |
+| Unpaid status badge | `bg-warning` |
+| Overdue worker flag (Pending tab) | `bg-danger` |
+
+## How to add a new colour token
+
+1. Define in BOTH the `:root` and `[data-theme="light"]` blocks in
+ `static/css/custom.css`. Choose colours that retain enough contrast
+ against the card background in both themes.
+2. Add a row to the mapping table in this doc.
+3. Reference via `var(--badge-*-bg)` in CSS — never hard-code hex
+ anywhere else.
+4. If it's a new adjustment type, add:
+ - A `.badge-type-` class in the `.badge-type-*` block
+ (around line 1935 of `custom.css`)
+ - An entry in the `.adj-group-header[data-type="..."]` block
+ (around line 1994)
+ - The new TYPE_CHOICES entry in `core/models.py::PayrollAdjustment`
+ (and run `makemigrations`)
+
+## Maintenance
+
+This doc is the single source of truth for app-wide colour semantics.
+When CSS tokens are added / removed / renamed in `custom.css`, update
+this doc in the SAME commit.
+````
+
+**Step 2: Commit**
+
+```bash
+git add docs/design-tokens.md
+git commit -m "$(cat <<'EOF'
+docs(tokens): add canonical design-tokens reference
+
+New doc covering the semantic colour palette: every badge token, its
+hex values in both themes, its CSS class, and where it's used across
+the app. Categorises tokens into "type-of-adjustment" (custom semantic
+palette) vs "transactional state" (Bootstrap defaults) and explains
+why the two must not share colours.
+
+Intended to be the single source of truth for UI colour decisions.
+
+Co-Authored-By: Claude Opus 4.7 (1M context)
+EOF
+)"
+```
+
+No tests, no code change. File-only addition.
+
+---
+
+## Task 2: CLAUDE.md — the "UI-vs-DB naming drift" section
+
+**Goal:** Document the permanent gap between display labels and DB values so future Claude (and Konrad, on a tired day) don't chase ghosts. This goes in BEFORE any rename work — so it's searchable the moment the rename ships.
+
+**Files:**
+- Modify: `CLAUDE.md` (insert a new section near the existing "Schema name-drifts to remember" section)
+
+**Step 1: Find the insertion point**
+
+Use Grep to find the exact line:
+
+```
+grep -n "Schema name-drifts to remember" CLAUDE.md
+```
+
+Expected: one match, around line 52 (give or take).
+
+**Step 2: Read the next ~20 lines to see the end of that block**
+
+Use the Read tool at that line, limit=30, to see where the "Schema name-drifts" section ends. Note the line of the last bullet + the blank line after it.
+
+**Step 3: Insert the new section**
+
+Insert the following block AFTER the last `- ` bullet of the "Schema name-drifts to remember" section (before the next `##` header):
+
+```markdown
+
+## UI-vs-DB naming drift (Apr 2026) — READ BEFORE WRITING FORMULAS
+
+`PayrollAdjustment.type` is DISPLAYED to users with short labels,
+but the raw string stored in the database is always the long
+legacy value:
+
+| What the user SEES | What the DATABASE stores |
+|---|---|
+| Bonus | `'Bonus'` |
+| Overtime | `'Overtime'` |
+| Deduction | `'Deduction'` |
+| Loan Repayment | `'Loan Repayment'` |
+| Loan | `'New Loan'` ← mismatch |
+| Advance | `'Advance Payment'` ← mismatch |
+| Advance Repaid | `'Advance Repayment'` ← mismatch |
+
+When writing ANY formula, filter, comparison, ORM query, test
+fixture, CSS class name, or `data-type=` attribute: use the
+DATABASE value (left column of the model).
+
+- `ADDITIVE_TYPES = ['Bonus', 'Overtime', 'New Loan', 'Advance Payment']`
+ in `views.py` uses DB values.
+- `if adj.type == 'New Loan':` checks the DB value.
+- `` produces
+ `.badge-type-new-loan` from the DB value.
+- `` emits the DB value.
+- Tests use `PayrollAdjustment.objects.create(type='New Loan', ...)`.
+
+Only user-facing template TEXT uses the short label — via
+`{{ adj.get_type_display }}`, Django's built-in choices lookup.
+The label mapping lives in `PayrollAdjustment.TYPE_CHOICES`
+(`core/models.py`).
+
+**How this happened:** originally the adjustment-creation dropdown
+said "New Loan" because that's what the action meant (_"log a new
+loan"_). That label then propagated into every other view — tables,
+badges, reports. On 24 Apr 2026 we renamed the user-visible labels
+to be shorter and cleaner BUT deliberately kept the database values
+untouched — to avoid breaking historic rows, tests, and hardcoded
+string comparisons across ~30 source locations.
+
+**Symptom of getting this wrong:** code that filters for
+`type='Loan'` returns zero rows. Fix: use `type='New Loan'`.
+
+```
+
+**Step 4: Verify the insert didn't break anything**
+
+```
+grep -c "^## " CLAUDE.md # section-count should have increased by exactly 1
+```
+
+Run the test suite as a sanity check (CLAUDE.md isn't code, but any accidental wholesale rewrite of the file would show up elsewhere):
+
+```
+set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py test core.tests -v 0
+```
+
+Expected: 69/69 pass (no code changed).
+
+**Step 5: Commit**
+
+```bash
+git add CLAUDE.md
+git commit -m "$(cat <<'EOF'
+docs(claude): UI-vs-DB naming drift note (pre-rename)
+
+Adds a new CLAUDE.md section documenting the display/DB gap that
+Path A of the UX Polish Pass creates: user sees 'Loan' / 'Advance'
+/ 'Advance Repaid' while DB stores 'New Loan' / 'Advance Payment'
+/ 'Advance Repayment'. Includes a lookup table, the rule for when
+to use which (DB for logic, display for templates), and the failure
+symptom so future Claude sessions don't chase ghost filters.
+
+Ships BEFORE the rename so the doc is searchable from minute one.
+
+Co-Authored-By: Claude Opus 4.7 (1M context)
+EOF
+)"
+```
+
+---
+
+## Task 3: Display-only rename (TYPE_CHOICES + visible templates + migration)
+
+**Goal:** The three rename labels take effect everywhere a user sees a type. DB stays untouched. `makemigrations` generates the required no-op `AlterField`.
+
+**Files:**
+- Modify: `core/models.py` — the `TYPE_CHOICES` list in `PayrollAdjustment`
+- Create: `core/migrations/0012_alter_payrolladjustment_type.py` (auto-generated — do NOT hand-write)
+- Modify: `core/templates/core/payroll_dashboard.html` — lines 370, 454 (visible `{{ adj.type }}` emissions)
+- Modify: `core/templates/core/_adjustment_row.html` — the type-cell render (uses `badge-type-{{ adj.type|type_slug }}` and also emits `{{ adj.type }}` as visible text — only the visible-text copy changes)
+- Grep-audit: other templates (`payslip_detail.html`, PDF templates, any other `{{ adj.type }}` emission) — audit & fix where visible
+
+**Step 1: Edit `core/models.py`**
+
+Read the current `TYPE_CHOICES` block first:
+
+```
+grep -n "TYPE_CHOICES = \[" core/models.py
+```
+
+Expected: one match around line 190. Read lines 190-210 to confirm current state matches:
+
+```python
+TYPE_CHOICES = [
+ ('Bonus', 'Bonus'),
+ ('Overtime', 'Overtime'),
+ ('Deduction', 'Deduction'),
+ ('Loan Repayment', 'Loan Repayment'),
+ ('New Loan', 'New Loan'),
+ ('Advance Payment', 'Advance Payment'),
+ ('Advance Repayment', 'Advance Repayment'),
+]
+```
+
+Replace with:
+
+```python
+# === PayrollAdjustment TYPE_CHOICES — canonical DB value | display label ===
+# Path A rename (24 Apr 2026): DB values are PRESERVED as-is. Only the
+# second tuple element (the human label) changes for three types, so
+# users see shorter labels in tables while every historic row, formula,
+# constant, test fixture, CSS class, and data-attribute KEEP WORKING
+# UNCHANGED because they all key off the DB value on the left.
+# See CLAUDE.md "UI-vs-DB naming drift" section for the full rule.
+TYPE_CHOICES = [
+ ('Bonus', 'Bonus'),
+ ('Overtime', 'Overtime'),
+ ('Deduction', 'Deduction'),
+ ('Loan Repayment', 'Loan Repayment'),
+ ('New Loan', 'Loan'),
+ ('Advance Payment', 'Advance'),
+ ('Advance Repayment', 'Advance Repaid'),
+]
+```
+
+**Step 2: Generate the migration**
+
+```
+set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py makemigrations core --name alter_payrolladjustment_type_display_labels
+```
+
+Expected output: creates `core/migrations/0012_alter_payrolladjustment_type_display_labels.py` with a single `AlterField` operation changing `choices`. The migration is a no-op at the database level — Django tracks choices in its model metadata, not in the DB schema.
+
+Open the generated file and confirm the operation is EXACTLY `AlterField` with `choices=[...]` — no `RunPython`, no `RunSQL`, no schema-altering operation. If you see anything more, stop and ask — something is wrong.
+
+**Step 3: Run the migration locally**
+
+```
+set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py migrate
+```
+
+Expected: `Applying core.0012_alter_payrolladjustment_type_display_labels... OK` — 1 operation, < 1 second.
+
+**Step 4: Grep-audit visible `{{ adj.type }}` template usages**
+
+```
+grep -rn "{{ adj\.type }}" core/templates/
+grep -rn "{{ adjustment\.type }}" core/templates/
+grep -rn "{{ a\.type }}" core/templates/
+```
+
+For EACH match, decide:
+- Is the surrounding context VISIBLE text (e.g., `>{{ adj.type }}<` inside a badge span, or in a table cell)? → change to `{{ adj.get_type_display }}` (or `{{ adjustment.get_type_display }}` / `{{ a.get_type_display }}`)
+- Is it a DATA ATTRIBUTE (`data-adj-type="{{ adj.type }}"`) or a CSS CLASS slug (`badge-type-{{ adj.type|type_slug }}`)? → LEAVE AS-IS. This is an identifier feed, not a display text.
+
+Known sites (from the brainstorm grep — verify each):
+1. `core/templates/core/payroll_dashboard.html:370` — inside `{{ adj.type }}` → SWAP to `{{ adj.get_type_display }}`
+2. `core/templates/core/payroll_dashboard.html:454` — `{{ adj.type }}: R {{ adj.amount }}` → SWAP
+3. `core/templates/core/payroll_dashboard.html:363` — `data-adj-type="{{ adj.type }}"` → LEAVE (identifier, consumed by JS)
+4. `core/templates/core/_adjustment_row.html` — grep in that file; type appears both as a CSS class slug AND potentially as visible text. Only the visible-text copy swaps.
+
+Also audit PDF + other detail pages:
+
+```
+grep -rn "{{ adj\.type }}\|{{ adjustment\.type }}\|{{ a\.type }}" core/templates/core/pdf/ core/templates/core/payslip_detail.html core/templates/core/report.html
+```
+
+Apply the same visible-vs-identifier call.
+
+**Step 5: Run the full test suite**
+
+```
+set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py test core.tests -v 2
+```
+
+Expected: 69/69 pass. Tests use DB values in fixtures (`type='New Loan'`) so NONE should break. If any test fails, something in Step 4 went wrong — revert and re-audit.
+
+**Step 6: Visual smoke test via `manage.py shell`**
+
+Quick sanity check that `get_type_display` returns the new labels:
+
+```
+set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py shell -c "from core.models import PayrollAdjustment; choices = dict(PayrollAdjustment.TYPE_CHOICES); print('New Loan displays as:', choices.get('New Loan')); print('Advance Payment displays as:', choices.get('Advance Payment')); print('Advance Repayment displays as:', choices.get('Advance Repayment'))"
+```
+
+Expected output:
+```
+New Loan displays as: Loan
+Advance Payment displays as: Advance
+Advance Repayment displays as: Advance Repaid
+```
+
+**Step 7: Commit**
+
+```bash
+git add core/models.py core/migrations/0012_*.py core/templates/
+git commit -m "$(cat <<'EOF'
+ux(labels): shorter adjustment type labels (display-only rename)
+
+Path A rename — DB values untouched, only TYPE_CHOICES display
+labels change:
+ 'New Loan' → shown as 'Loan'
+ 'Advance Payment' → shown as 'Advance'
+ 'Advance Repayment' → shown as 'Advance Repaid'
+
+Templates that render the type as visible text switched from
+{{ adj.type }} to {{ adj.get_type_display }}. Data attributes and
+CSS class slugs keep the raw DB value (identifiers, not labels).
+
+Zero data migration. Zero changes to ADDITIVE_TYPES / DEDUCTIVE_TYPES
+constants, hardcoded string comparisons, CSS class names, test
+fixtures, or any other code that references the canonical DB value.
+Every historic PayrollAdjustment row keeps type='New Loan' /
+'Advance Payment' / 'Advance Repayment' as stored.
+
+Django's makemigrations generated a no-op AlterField migration to
+record the choices-metadata change.
+
+Tests: 69/69.
+
+Co-Authored-By: Claude Opus 4.7 (1M context)
+EOF
+)"
+```
+
+---
+
+## Task 4: Badge colour unification + loan-flag recolor
+
+**Goal:** Replace the three occurrences of the 4-branch Bootstrap-state conditional with a one-liner `badge-type-{{ adj.type|type_slug }}` that reuses the existing semantic palette. Recolour the Pending-tab "Loan" worker flag to the loan type colour.
+
+**Files:**
+- Modify: `core/templates/core/payroll_dashboard.html` — lines 360, 453 (type-badge blocks) + line 345 (loan flag)
+- Modify: `static/css/custom.css` — add `.loan-flag-badge` class near the `.badge-type-*` block
+
+**Step 1: Preconditions**
+
+Confirm the existing context-variable `additive_types` is threaded to the Pending + History tab contexts. The Adjustments tab already uses it (see `_adjustment_row.html` line 42). For Pending + History, grep:
+
+```
+grep -n "additive_types" core/views.py
+```
+
+If the Pending/History branch of `payroll_dashboard()` does NOT set `additive_types` in its context, we need to add it. The constant is already defined at `views.py:45` — it's just a matter of adding one key to the context dict. Look for the `context = { ... }` block in the `payroll_dashboard` view that handles the default/pending branch (grep for `'workers_data'` as a nearby key — the same context dict).
+
+If `additive_types` is already set for those branches, no view change is needed. Only the templates change.
+
+**Step 2: Replace the type-badge block at line 360 (Pending tab)**
+
+Current (lines 356-372):
+```django
+ {% for adj in wd.adjustments %}
+ {# Badge colour logic: #}
+ {# GREEN = earned money (Bonus, Overtime) or debt recovery (Loan/Advance Repayment) #}
+ {# YELLOW = loan-related outflow (New Loan, Advance Payment) — matches the Loan tag #}
+ {# RED = deductions (Deduction) #}
+
+ {% if adj.type == 'Bonus' or adj.type == 'Overtime' or adj.type == 'New Loan' or adj.type == 'Advance Payment' %}+{% else %}-{% endif %}R{{ adj.amount|floatformat:2 }}
+ {{ adj.type }}
+ {% if adj.project %}({{ adj.project.name }}){% endif %}
+
+ {% endfor %}
+```
+
+Note: `{{ adj.type }}` at the old line 370 will already have been changed to `{{ adj.get_type_display }}` by Task 3. If Task 3 shipped correctly, the current text at line 370 is `{{ adj.get_type_display }}`. Don't revert it.
+
+Replace the badge block with:
+```django
+ {% for adj in wd.adjustments %}
+ {# Type badge uses the semantic palette: colour = type (Bonus, Loan, etc.). #}
+ {# Sign + / − reflects additive-vs-deductive (orthogonal to the colour). #}
+
+ {% if adj.type in additive_types %}+{% else %}-{% endif %}R{{ adj.amount|floatformat:2 }}
+ {{ adj.get_type_display }}
+ {% if adj.project %}({{ adj.project.name }}){% endif %}
+
+ {% endfor %}
+```
+
+Three things happened in this change:
+- `class="badge ...multi-line conditional..."` → `class="badge badge-type-{{ adj.type|type_slug }}"`
+- Sign logic refactored from a long `{% if %}` chain to `{% if adj.type in additive_types %}` (cleaner, single source of truth for additive set)
+- Outdated comment block removed; new comment describes the current semantic scheme
+
+**Step 3: Replace the type-badge block at line 453 (History tab)**
+
+Current:
+```django
+ {% for adj in record.adjustments.all %}
+
+ {{ adj.type }}: R {{ adj.amount|floatformat:2 }}
+
+ {% empty %}
+```
+
+(The `{{ adj.type }}` on the inner line was already swapped to `{{ adj.get_type_display }}` by Task 3.)
+
+Replace with:
+```django
+ {% for adj in record.adjustments.all %}
+
+ {{ adj.get_type_display }}: R {{ adj.amount|floatformat:2 }}
+
+ {% empty %}
+```
+
+**Step 4: Recolour the loan flag at line 345 (Pending tab)**
+
+Current line 345:
+```django
+ Loan
+```
+
+Replace with:
+```django
+ Loan
+```
+
+Line 342 (Overdue flag) stays `bg-danger` — it's transactional / urgency, not type. Don't touch it.
+
+**Step 5: Add the `.loan-flag-badge` CSS class**
+
+In `static/css/custom.css`, find the `.badge-type-advance-repayment` line (around line 1941) and append a new block right after the type-badge definitions:
+
+```css
+/* --- Status flags that borrow a type's colour for semantic consistency.
+ "Has an active loan or advance" → Loan-type amber/yellow, so the
+ worker flag on the Pending tab visually matches the Adjustments
+ type badge for Loan. Keeps the Loan colour family unified across
+ the app regardless of which tab you're looking at. --- */
+.loan-flag-badge {
+ background: var(--badge-loan-bg);
+ color: var(--badge-loan-fg);
+}
+```
+
+**Step 6: Run the full test suite**
+
+```
+set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py test core.tests -v 2
+```
+
+Expected: 69/69. Template changes don't hit any test assertion.
+
+**Step 7: Visual smoke-test checklist**
+
+The implementer must mentally walk through these with the template open in an editor (no browser needed if Django check passes):
+
+- [ ] Pending tab: `{% for adj in wd.adjustments %}` block — the badge has exactly one class besides `badge`: `badge-type-{{ adj.type|type_slug }}`. No `bg-success`/`bg-warning`/`bg-danger` left.
+- [ ] Pending tab: the "Loan" worker flag uses `loan-flag-badge` class, NOT `bg-warning`.
+- [ ] Pending tab: the "Overdue" worker flag still uses `bg-danger`. (Don't "helpfully" change this.)
+- [ ] History tab: `{% for adj in record.adjustments.all %}` block — same check as Pending.
+- [ ] Paid #N / Unpaid badges elsewhere — still use Bootstrap state classes (`bg-success`/`bg-warning`). NOT touched.
+
+Run `python manage.py check` as a final sanity check:
+```
+set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py check
+```
+Expected: no errors (the pre-existing `staticfiles.W004` warning is fine).
+
+**Step 8: Commit**
+
+```bash
+git add core/templates/core/payroll_dashboard.html static/css/custom.css
+git commit -m "$(cat <<'EOF'
+ux(colors): unify badge colours across all payroll tabs
+
+Replaces the 4-branch Bootstrap-state conditional on the Pending
+and History tabs with the semantic .badge-type-{{ adj.type|type_slug }}
+palette that the Adjustments tab has been using. Now "Loan" badges
+are the same colour in every tab instead of Pending=yellow /
+Adjustments=amber.
+
+Also recolours the Pending-tab "Loan" worker flag to the same amber
+(.loan-flag-badge class). "Overdue" flag stays red — it's an urgency
+signal, not a type signal, and we deliberately keep transactional
+state colours (Bootstrap bg-success/bg-warning/bg-danger) separate
+from the type palette so a green badge can only mean "Bonus" and
+never ambiguously "Paid".
+
+Tests: 69/69.
+
+Co-Authored-By: Claude Opus 4.7 (1M context)
+EOF
+)"
+```
+
+---
+
+## Task 5: Fix `.adj-group-header` CSS (narrow-wrap bug)
+
+**Goal:** Make the group-summary row span the full table width and push the "N row · +R X net" meta to the right, as originally intended. Root cause: `display: flex` was applied to the ``, which breaks table rendering (a flexed ` ` ignores `colspan` and shrinks to intrinsic content).
+
+**Files:**
+- Modify: `static/css/custom.css` — the `.adj-group-header` block around lines 1972-1988
+
+**Step 1: Read the current block**
+
+```
+grep -n "\.adj-group-header {" static/css/custom.css
+```
+
+Expected: one match at line 1972. Read lines 1972-2000 to see the full current state.
+
+**Step 2: Rewrite the block**
+
+Replace the CURRENT content of lines 1972-1988 (note: preserve line 1984's `:hover` rule and everything below it):
+
+```css
+/* --- Group header (collapsible section divider for group-by mode) ---
+ NOTE: display: flex MUST be on the | , NOT on the | . Setting
+ display on a removes it from table row/column participation
+ (colspan is ignored, the row shrinks to intrinsic content width),
+ which caused the "narrow wrap" screenshot bug in Apr 2026. The td
+ is an ordinary block box and flexes fine. --- */
+.adj-group-header {
+ cursor: pointer;
+ background: var(--bg-inset);
+ border-top: 1px solid var(--border-default);
+ border-bottom: 1px solid var(--border-default);
+ user-select: none;
+ transition: background-color 120ms;
+}
+.adj-group-header > td {
+ padding: 0.75rem 1rem;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+.adj-group-header:hover { background: var(--bg-card-hover); }
+.adj-group-header .fa-chevron-down,
+.adj-group-header .fa-chevron-right { opacity: 0.7; width: 0.8rem; }
+.adj-group-header .adj-group-label { font-weight: 600; }
+.adj-group-header .adj-group-meta { color: var(--text-secondary); font-size: 0.875rem; margin-left: auto; white-space: nowrap; }
+```
+
+Diff from before:
+- REMOVED `display: flex; align-items: center; gap: 0.75rem;` from `.adj-group-header` (it was on the ` `, which was the bug)
+- REMOVED `padding: 0.75rem 1rem;` from `.adj-group-header` (moved to the `| ` where it belongs)
+- ADDED `.adj-group-header > td { ... }` with the flex stuff now applied to the ` | ` — where it actually works
+- ADDED `white-space: nowrap;` on `.adj-group-meta` so the meta text can overflow or fit on one line, never wrap into an ugly stub even in narrow viewports
+- ADDED the explanatory comment documenting why this looks slightly unusual
+
+Leave lines 1994-2007 (the `[data-type="X"]` border-left rules + chevron-rotation rules) alone — they were already correctly scoped and don't need to change.
+
+**Step 3: Run the test suite**
+
+```
+set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py test core.tests -v 2
+```
+
+Expected: 69/69. CSS-only change; no test should react.
+
+**Step 4: Visual confirmation** (walk the template + CSS mentally)
+
+- [ ] ` | ` participates in table layout again. `colspan="10"` on the `| ` is now honoured — the row spans all 10 columns.
+- [ ] ` | ` has `display: flex`, so the icon / label / meta are flex children. `align-items: center` vertically centres them. `gap: 0.75rem` puts space between them.
+- [ ] `.adj-group-meta { margin-left: auto; }` now works (it's a flex child of a flex container).
+- [ ] `.adj-group-meta { white-space: nowrap; }` prevents the "Bonus 1 / row · +R / 444" stutter wrap even if something downstream tries to squeeze the cell.
+- [ ] The `[data-type="X"]` border-left accent still paints the left edge of the row (it targets `.adj-group-header`, which is the ` | `; the ` ` is once again a normal table row).
+
+**Step 5: Commit**
+
+```bash
+git add static/css/custom.css
+git commit -m "$(cat <<'EOF'
+fix(css): move display:flex from to | on adj-group-header
+
+Root cause of Konrad's narrow-wrap screenshot: display:flex was set
+on .adj-group-header (a | ), which causes the browser to remove
+the row from table layout. A flex-mode ignores colspan and
+shrinks to intrinsic content width — which is why a row with
+colspan=10 ended up rendering at ~80-100px and wrapping the meta
+text into a 5-char column.
+
+Moved display:flex, align-items, gap, and padding onto the single
+| child. The td is a normal block box and flexes correctly,
+putting icon + label + meta in a horizontal row with the meta
+pushed to the right via margin-left:auto (now working since its
+parent is a real flex container).
+
+Also added white-space:nowrap on .adj-group-meta so the meta never
+wraps mid-phrase even if a narrow viewport squeezes the cell.
+
+Inline comment documents the | vs | distinction so future
+sessions don't re-introduce the bug.
+
+Tests: 69/69.
+
+Co-Authored-By: Claude Opus 4.7 (1M context)
+EOF
+)"
+```
+
+---
+
+## Final acceptance checklist
+
+Before declaring the pass complete, the controller verifies:
+
+- [ ] Every commit's prefix is appropriate (`docs(...)`, `ux(...)`, `fix(css)`)
+- [ ] `set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py test core.tests -v 2` → 69/69 passing
+- [ ] `python manage.py check` → only the pre-existing `staticfiles.W004` warning
+- [ ] `python manage.py migrate` ran cleanly in local dev (one new migration 0012)
+- [ ] `grep -n "New Loan" core/models.py` still finds the DB value in TYPE_CHOICES (left tuple element) → confirms Path A discipline held
+- [ ] `grep -rn "{{ adj.type }}" core/templates/` → only identifier contexts remain (data-attrs, CSS class slugs), no visible-text usages
+- [ ] `grep -n "bg-success\|bg-warning\|bg-danger" core/templates/core/payroll_dashboard.html` → still finds the "Paid"/"Unpaid"/"Overdue" occurrences (transactional state, correct), but NONE on adjustment-type badges
+- [ ] Working tree clean, branch ready to push
+
+---
+
+## What's NOT in this plan (explicit non-goals)
+
+- Any change to DB values of `PayrollAdjustment.type`
+- Any edit to `ADDITIVE_TYPES` / `DEDUCTIVE_TYPES` constants
+- Any hardcoded-string comparison in `core/views.py`
+- Any test fixture or assertion
+- Any badge class rename (`.badge-type-new-loan` stays)
+- Any `data-type="..."` attribute value change
+- Bootstrap state colours being replaced anywhere (we explicitly keep `bg-success`/`bg-warning`/`bg-danger` for transactional badges)
+- Adjustments tab layout or filter changes (Pass A for that shipped earlier)
+
+Rollback: `git revert ` on any individual commit. No data, schema, or URL-contract impact in any task.
+
+---
+
+## Execution
+
+Plan complete and saved to `docs/plans/2026-04-24-ux-polish-plan.md`.
+
+Per the arguments this plan was generated with, auto mode is active and the execution path is:
+
+**Sub-skill: `superpowers:subagent-driven-development`**
+
+Controller stays in-session and dispatches fresh subagents per task with spec-compliance + code-quality review after each. Expected ~5 task commits + ~2 fix commits if reviewers find anything = ~7 commits on `ai-dev`.
| |