diff --git a/docs/plans/2026-04-23-adjustments-tab-design.md b/docs/plans/2026-04-23-adjustments-tab-design.md
new file mode 100644
index 0000000..d78e597
--- /dev/null
+++ b/docs/plans/2026-04-23-adjustments-tab-design.md
@@ -0,0 +1,557 @@
+# Payroll Adjustments Tab — Design (23 Apr 2026)
+
+## Goal
+
+Add a new **Adjustments** tab to the payroll dashboard (alongside Pending / History / Loans & Advances) that lets admins browse ALL payroll adjustments across all workers — filter by type, worker, team, paid/unpaid status, and date — with semantic colour-coded badges, bulk delete, group-by-type / group-by-worker render, and inline actions matching the rest of the dashboard.
+
+Today the only place to see a list of every adjustment is `/admin/core/payrolladjustment/` (Django admin). Adjustments surface per-worker on the Pending tab, per-worker in the Worker Payment Hub modal, and per-worker on the payslip detail page — but nowhere as a unified, filterable list.
+
+## Origin
+
+Konrad's request from a brainstorming session on 23 Apr 2026:
+
+> _"At the moment I can only lookup loans and advances easily but I would like to be able to see all payroll adjustments and filter between type (keep loans and advances separate as they are both loans and record keep is very important)."_
+
+Followed by a UX refinement:
+
+> _"Can we have the Adjustments tab next to Loans and advances on the payroll dashboard?"_
+
+And a premium-UX request:
+
+> _"So in Adjustments we need filters for type of adjustment (multiselect), Teams, Name (Multiselect), paid/unpaid, Date of Adjustment... let us keep loans dirty yellow, adjustments dark pastel blue, deductions deep purple, bonuses dirty pastel green, overtime dirty pink."_
+
+## Who it's for
+
+**Admins** (`is_staff` or `is_superuser`). Supervisors keep no payroll-dashboard access.
+
+## Architecture at a glance
+
+- **URL**: existing `/payroll/` view with new `?status=adjustments` query param (mirrors the pending/paid/loans pattern already in `payroll_dashboard_view`)
+- **Template**: extend `core/templates/core/payroll_dashboard.html` — add tab in the nav-tabs strip + new content block gated on `active_tab == 'adjustments'`
+- **Modal reuse**: the existing `#addAdjustmentModal`, `#editAdjustmentModal`, `#payslipPreviewModal` all live on the payroll dashboard already. The Adjustments tab's row actions just trigger them — zero duplication.
+- **New**: CSS semantic badge palette (7 types × dark/light themes = 14 colour tokens + 7 badge classes), one bulk-delete endpoint, and a group-by rendering layer
+- **No model changes. No migrations.**
+
+## 1. Colour palette — semantic type badges
+
+### Mapping: 7 adjustment types → 5 colour categories
+
+Loan/Advance **repayment** sub-types use the same category colour as their outgoing counterparts but with ~15% more saturation — "same family, hotter signal" so eyes instantly catch "this one is money coming back, not going out".
+
+| Type | Category | Saturation |
+|---|---|---|
+| Bonus | Bonus | base |
+| Overtime | Overtime | base |
+| Deduction | Deduction | base |
+| New Loan | Loans | base |
+| Loan Repayment | Loans | +15% |
+| Advance Payment | Advances | base |
+| Advance Repayment | Advances | +15% |
+
+### CSS tokens
+
+Added to `static/css/custom.css` `:root` (dark) and `:root.light` blocks:
+
+```css
+/* Dark theme — badge palette */
+--badge-bonus-bg: #5b8260; --badge-bonus-fg: #e8f3ea;
+--badge-overtime-bg: #a16881; --badge-overtime-fg: #fce4ec;
+--badge-deduction-bg: #5b4f8c; --badge-deduction-fg: #e0daf3;
+--badge-loan-bg: #9b7f39; --badge-loan-fg: #fef4d1;
+--badge-loan-rep-bg: #b48a1a; --badge-loan-rep-fg: #fef4d1; /* +15% saturation */
+--badge-advance-bg: #3e5c7b; --badge-advance-fg: #d7e5f2;
+--badge-advance-rep-bg: #2f679a; --badge-advance-rep-fg: #d7e5f2; /* +15% saturation */
+
+/* Light theme — overrides in :root.light */
+--badge-bonus-bg: #d7e8d9; --badge-bonus-fg: #385640;
+--badge-overtime-bg: #f3d1dd; --badge-overtime-fg: #703347;
+--badge-deduction-bg: #d8d0ef; --badge-deduction-fg: #3b2f6d;
+--badge-loan-bg: #f0dc9d; --badge-loan-fg: #6a5320;
+--badge-loan-rep-bg: #f7d873; --badge-loan-rep-fg: #5a4418;
+--badge-advance-bg: #bccee0; --badge-advance-fg: #243b56;
+--badge-advance-rep-bg: #9ec1dd; --badge-advance-rep-fg: #1d3550;
+```
+
+Each badge class maps type name → token:
+```css
+.badge-type-bonus { background: var(--badge-bonus-bg); color: var(--badge-bonus-fg); }
+.badge-type-overtime { background: var(--badge-overtime-bg); color: var(--badge-overtime-fg); }
+.badge-type-deduction { background: var(--badge-deduction-bg); color: var(--badge-deduction-fg); }
+.badge-type-new-loan { background: var(--badge-loan-bg); color: var(--badge-loan-fg); }
+.badge-type-loan-repayment { background: var(--badge-loan-rep-bg); color: var(--badge-loan-rep-fg); }
+.badge-type-advance-payment { background: var(--badge-advance-bg); color: var(--badge-advance-fg); }
+.badge-type-advance-repayment { background: var(--badge-advance-rep-bg); color: var(--badge-advance-rep-fg); }
+```
+
+Template slug helper: a small template filter `|type_slug` that turns `"Advance Payment"` → `"advance-payment"` for class naming.
+
+**All badges use Inter, 11pt, weight 500, `padding: 0.3rem 0.7rem`, `border-radius: 999px`.** Amount is shown NOT on the badge (badge shows type only); amount is a separate cell with `+` / `−` prefix.
+
+## 2. Tab markup (adds one `
` to existing nav-tabs)
+
+In `payroll_dashboard.html` at around line 252:
+
+```django
+
+```
+
+## 3. Filter bar — sticky, multi-select, with cross-filter
+
+Five filters in a single row under the tab:
+
+```
+┌──────────────────────────────────────────────────────────────────────────────┐
+│ [Type ▾ 2] [Workers ▾ 3] [Teams ▾ 1] [Status ▾] [Date 📅] [Apply] │
+└──────────────────────────────────────────────────────────────────────────────┘
+```
+
+| Filter | UI | Behaviour |
+|---|---|---|
+| **Type** | Choices.js multi-select, 7 options (the 7 adjustment types). Selected chips show the badge colour of that type. | empty = all types |
+| **Workers** | Choices.js multi-select, searchable. Cross-filtered by selected Teams (see below). | empty = all workers |
+| **Teams** | Choices.js multi-select, all active teams | empty = all teams |
+| **Status** | Native single-select: All / Unpaid / Paid | default All |
+| **Date** | Single date picker. A `…` button toggles range mode (From / To). | Single date = exact; range = inclusive bounds |
+| **Apply** | Button visible only when filters dirty | Submits via `?status=adjustments&type=Bonus&type=Overtime&worker=1&worker=2&team=3&adj_status=unpaid&adj_date_from=...&adj_date_to=...&group_by=type` |
+
+**Sticky:** the filter bar stays at the top as the table scrolls below it. `position: sticky; top: 0; z-index: 10; background: var(--bg-card);`.
+
+### Cross-filter — Team → Workers
+
+When Team(s) are selected, the Workers dropdown shows only workers in those teams. Implementation mirrors Feature 1's project↔team cross-filter:
+
+- Backend: compute a JSON map of `(team_id, worker_id)` pairs from `Team.workers.through`:
+ ```python
+ team_worker_pairs = list(
+ Team.workers.through.objects.values('team_id', 'worker_id').distinct()
+ )
+ context['team_worker_pairs_json'] = json.dumps(team_worker_pairs)
+ ```
+- Frontend: when the Workers popover opens, filter visible options based on current team selection. Auto-remove now-invalid worker selections with a toast ("Alice removed — not in selected teams").
+- Scope: entire active roster (not filtered by date range) — cross-filter is about data possibility, not data in this period.
+
+One-way: Teams filter Workers. (Worker→Team filter is less useful — you usually pick teams first, then drill down to specific workers within them.)
+
+## 4. Columns + row actions
+
+### Flat view (no grouping)
+
+| # | Column | Sort | Notes |
+|---|---|---|---|
+| 1 | `☐` (bulk checkbox) | No | Only on unpaid rows (paid rows show disabled checkbox) |
+| 2 | Date | Yes ▲▼ | `d M Y` format |
+| 3 | Worker | Yes | Link to `/workers//` |
+| 4 | Type | No (filter instead) | Badge with `.badge-type-` |
+| 5 | Amount | Yes | Right-aligned, tabular-nums. Sign: `+` for additive, `−` for deductive. Plain text colour (`--text-primary`) |
+| 6 | Project | No | Link to `/projects//` or `—` |
+| 7 | Team | No | `worker.teams.first().name` or `—` |
+| 8 | Description | No | Truncate 40 chars + `title` attr tooltip for full text |
+| 9 | Status | Yes | Badge: `Paid #123` (success, links `/payroll/payslip//`) or `Unpaid` (warning) |
+| 10 | Actions | No | See below |
+
+**Sortable:** 4 columns (Date, Worker, Amount, Status) with click-to-toggle. Bootstrap sort arrows in the headers. URL state: `?sort=
&order=`.
+
+### Row actions
+
+Inline buttons matching the rest of the dashboard — NO expandable rows:
+
+- **Unpaid row:**
+ - `[Preview]` — opens existing `#payslipPreviewModal` for that worker (same as Pending Payments tab's Preview)
+ - `[Edit]` — opens existing `#editAdjustmentModal` pre-filled (same as Pending tab's Edit)
+ - `[×]` (delete) — opens existing delete confirm flow
+- **Paid row:**
+ - `[View Payslip]` — links to `/payroll/payslip//` (same as History tab's View)
+
+Zero new modals. Zero new JS — reuse the handlers already on the payroll dashboard.
+
+## 5. Group-by toggle
+
+Three radio pills above the table (next to the filter bar):
+
+```
+Show as: [ Flat ● ] [ By Type ○ ] [ By Worker ○ ]
+```
+
+### When grouped:
+
+```
+▾ BONUS · 3 rows · +R 1 500 ───────────────────────────────────
+ 22 Apr 2026 Alice M. Bonus +R 500 Wilkot ... Unpaid [Preview][Edit][×]
+ 18 Apr 2026 Bob N. Bonus +R 500 Alpha ... Paid #8 [View]
+ ...
+▾ OVERTIME · 5 rows · +R 750 ──────────────────────────────────
+ ...
+▸ DEDUCTION · 2 rows · −R 400 ───────────────────────────────── (collapsed)
+ ...
+```
+
+- Click group header → collapse/expand (Bootstrap Collapse, smooth animation)
+- Group header shows: `` · row count · net sum (`+R` for net-additive groups, `−R` for net-deductive)
+- Default: all groups expanded
+- URL state: `?group_by=type` or `?group_by=worker`
+
+### By-worker grouping
+
+Same shape but grouped by `worker.id`:
+
+```
+▾ Alice Mokoena · 4 rows · +R 1 200 ─────────────────
+ ...
+▾ Bob Ndlovu · 2 rows · +R 300 ──────────────────────
+ ...
+```
+
+### Implementation
+
+Backend:
+```python
+if group_by == 'type':
+ groups = defaultdict(list)
+ for adj in paginated_adjustments:
+ groups[adj.type].append(adj)
+ rendered_groups = [
+ {'label': t, 'slug': slug(t), 'rows': rows,
+ 'count': len(rows), 'net_sum': sum_with_sign(rows)}
+ for t, rows in groups.items()
+ ]
+elif group_by == 'worker':
+ # same shape, groups keyed by worker_id; label = worker.name
+```
+
+Template iterates over `rendered_groups` if grouping is active, else over the flat page. Both paths render the same row-level HTML (shared partial `_adjustment_row.html`) so there's no markup duplication.
+
+## 6. Bulk actions
+
+### Checkbox column
+
+Leftmost column with `` on each unpaid row. Paid rows show a disabled greyed-out checkbox (visually consistent, not interactive). "Select all" checkbox in the thead toggles all visible unpaid rows on the current page.
+
+### Floating action bar
+
+When ≥1 row is selected, a small bar slides up from the bottom of the viewport (not the page — fixed position):
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ 3 selected · [🗑 Delete] [Clear selection] │
+└─────────────────────────────────────────────────────────┘
+```
+
+- `Delete` → confirms via native `confirm()` → POSTs to new endpoint `/payroll/adjustments/bulk-delete/`
+- `Clear selection` → unticks all, bar slides away
+
+### Bulk-delete endpoint
+
+```python
+@login_required
+def bulk_delete_adjustments(request):
+ """Delete multiple unpaid adjustments at once.
+
+ Only unpaid adjustments can be deleted (paid ones are locked into payroll
+ history). POST body: list of adjustment IDs. Returns JSON with success
+ count + error count. Admin-only.
+ """
+ if request.method != 'POST':
+ return JsonResponse({'error': 'POST required'}, status=405)
+ if not is_admin(request.user):
+ return JsonResponse({'error': 'Not authorized'}, status=403)
+
+ ids = request.POST.getlist('adjustment_ids')
+ # Only unpaid adjustments; silently skip any paid ones (defensive against UI bugs)
+ to_delete = PayrollAdjustment.objects.filter(
+ id__in=ids, payroll_record__isnull=True
+ )
+ deleted_count = to_delete.count()
+ to_delete.delete()
+
+ return JsonResponse({'deleted': deleted_count, 'requested': len(ids)})
+```
+
+URL: `path('payroll/adjustments/bulk-delete/', views.bulk_delete_adjustments, name='bulk_delete_adjustments')`.
+
+## 7. Header stats
+
+Directly under the filter bar, above the table:
+
+```
+247 adjustments · 38 unpaid (R 126 500) · +R 45 000 net additive · −R 2 100 net deductive
+```
+
+All four numbers scoped to the current filter set (not the whole DB). Updates on Apply.
+
+## 8. Date filter — single vs range
+
+The Date field is a single date picker by default. A small `…` (ellipsis) button next to it toggles to range mode:
+
+- **Single mode** (default): pick ONE date. Filter: `date = selected_date`.
+- **Range mode**: two date pickers (From / To). Filter: `from ≤ date ≤ to`.
+
+Single mode covers Konrad's "see adjustments for a specific date" use case. Range mode handles "this week" / "this month" audit queries.
+
+Preset quick-buttons above the picker: `[Today] [This week] [This month] [Clear]`. Clicking a preset fills the range and stays in range mode.
+
+## 9. URL state & bookmarkability
+
+Every filter + sort + group-by choice lives in the querystring. Shareable/bookmarkable:
+
+```
+/payroll/?status=adjustments
+ &type=Bonus&type=Overtime ← multi-value (same key repeated)
+ &worker=1&worker=3
+ &team=2
+ &adj_status=unpaid
+ &adj_date_from=2026-03-01
+ &adj_date_to=2026-04-30
+ &group_by=type
+ &sort=amount&order=desc
+ &page=2
+```
+
+## 10. Empty state
+
+When filters return 0 rows:
+
+```
+┌──────────────────────────────────────────┐
+│ 📭 │
+│ No adjustments match these filters. │
+│ │
+│ [Clear filters] [Add new adjustment] │
+└──────────────────────────────────────────┘
+```
+
+Action buttons offer immediate recovery paths.
+
+## 11. Backend changes
+
+### New branch in `payroll_dashboard` view
+
+Around line 2496 where `status_filter` is parsed:
+
+```python
+elif status_filter == 'adjustments':
+ active_tab = 'adjustments'
+ # Parse filters
+ type_filter = request.GET.getlist('type')
+ worker_filter = [int(v) for v in request.GET.getlist('worker') if v.strip().isdigit()]
+ team_filter = [int(v) for v in request.GET.getlist('team') if v.strip().isdigit()]
+ adj_status = request.GET.get('adj_status', '').strip()
+ adj_date_from = request.GET.get('adj_date_from', '').strip()
+ adj_date_to = request.GET.get('adj_date_to', '').strip()
+ group_by = request.GET.get('group_by', '').strip()
+ sort_col = request.GET.get('sort', 'date').strip()
+ sort_order = request.GET.get('order', 'desc').strip()
+
+ adjustments = PayrollAdjustment.objects.select_related(
+ 'worker', 'project', 'payroll_record'
+ ).prefetch_related('worker__teams')
+
+ if type_filter:
+ adjustments = adjustments.filter(type__in=type_filter)
+ if worker_filter:
+ adjustments = adjustments.filter(worker_id__in=worker_filter)
+ if team_filter:
+ # Subquery pattern per CLAUDE.md ORM gotcha — avoids M2M JOIN inflation
+ adjustments = adjustments.filter(
+ worker__in=Worker.objects.filter(teams__id__in=team_filter).values('id')
+ )
+ if adj_status == 'unpaid':
+ adjustments = adjustments.filter(payroll_record__isnull=True)
+ elif adj_status == 'paid':
+ adjustments = adjustments.filter(payroll_record__isnull=False)
+ if adj_date_from:
+ adjustments = adjustments.filter(date__gte=parse_date(adj_date_from))
+ if adj_date_to:
+ adjustments = adjustments.filter(date__lte=parse_date(adj_date_to))
+
+ # Sorting
+ sort_map = {'date': 'date', 'worker': 'worker__name',
+ 'amount': 'amount', 'status': 'payroll_record'}
+ sort_field = sort_map.get(sort_col, 'date')
+ if sort_order == 'desc':
+ sort_field = '-' + sort_field
+ adjustments = adjustments.order_by(sort_field, '-id')
+
+ # Stats (scoped to filtered set)
+ adj_total_count = adjustments.count()
+ unpaid_qs = adjustments.filter(payroll_record__isnull=True)
+ adj_unpaid_count = unpaid_qs.count()
+ adj_unpaid_sum = unpaid_qs.aggregate(total=Sum('amount'))['total'] or Decimal('0.00')
+ additive_sum = adjustments.filter(type__in=ADDITIVE_TYPES).aggregate(
+ total=Sum('amount'))['total'] or Decimal('0.00')
+ deductive_sum = adjustments.filter(type__in=DEDUCTIVE_TYPES).aggregate(
+ total=Sum('amount'))['total'] or Decimal('0.00')
+
+ # Pagination
+ paginator = Paginator(adjustments, 50)
+ adj_page = paginator.get_page(request.GET.get('page', 1))
+
+ # Group-by rendering
+ adj_groups = None
+ if group_by in ('type', 'worker'):
+ adj_groups = _group_adjustments(adj_page.object_list, group_by)
+
+ # Cross-filter data for JS
+ team_worker_pairs_json = json.dumps(list(
+ Team.workers.through.objects.values('team_id', 'worker_id').distinct()
+ ))
+
+ context.update({
+ 'adj_page': adj_page,
+ 'adj_groups': adj_groups,
+ 'adj_total_count': adj_total_count,
+ 'adj_unpaid_count': adj_unpaid_count,
+ 'adj_unpaid_sum': adj_unpaid_sum,
+ 'adj_additive_sum': additive_sum,
+ 'adj_deductive_sum': deductive_sum,
+ 'adj_filter_values': {
+ 'type': type_filter, 'worker': worker_filter, 'team': team_filter,
+ 'adj_status': adj_status, 'adj_date_from': adj_date_from,
+ 'adj_date_to': adj_date_to, 'group_by': group_by,
+ 'sort': sort_col, 'order': sort_order,
+ },
+ 'adjustment_types': ADDITIVE_TYPES + DEDUCTIVE_TYPES,
+ 'all_workers_for_filter': Worker.objects.filter(active=True).order_by('name'),
+ 'all_teams_for_filter': Team.objects.filter(active=True).order_by('name'),
+ 'team_worker_pairs_json': team_worker_pairs_json,
+ })
+```
+
+### New helper `_group_adjustments`
+
+```python
+def _group_adjustments(adjustments, group_by):
+ """Regroup a flat list of adjustments by type or by worker.
+
+ Returns list of dicts: [{'label', 'slug', 'rows', 'count', 'net_sum'}, ...]
+ Groups ordered by descending net_sum magnitude (biggest impact first).
+ """
+ from collections import defaultdict
+ buckets = defaultdict(list)
+ for adj in adjustments:
+ key = adj.type if group_by == 'type' else adj.worker_id
+ buckets[key].append(adj)
+
+ groups = []
+ for key, rows in buckets.items():
+ if group_by == 'type':
+ label = key
+ slug = key.lower().replace(' ', '-')
+ else: # worker
+ label = rows[0].worker.name
+ slug = f'worker-{key}'
+ net_sum = sum(
+ (r.amount if r.type in ADDITIVE_TYPES else -r.amount)
+ for r in rows
+ )
+ groups.append({
+ 'label': label, 'slug': slug, 'rows': rows,
+ 'count': len(rows), 'net_sum': net_sum,
+ })
+ groups.sort(key=lambda g: -abs(g['net_sum']))
+ return groups
+```
+
+### New endpoint `bulk_delete_adjustments`
+
+(Shown in section 6.)
+
+### New template filter `|type_slug`
+
+In `core/templatetags/format_tags.py`:
+
+```python
+@register.filter
+def type_slug(value):
+ """Convert 'Advance Payment' -> 'advance-payment' for CSS class naming."""
+ if not value:
+ return ''
+ return value.lower().replace(' ', '-')
+```
+
+## 12. Testing
+
+New `AdjustmentsTabTests` class in `core/tests.py` (~8 tests):
+
+- `test_admin_sees_adjustments_tab` — 200 for admin at `?status=adjustments`
+- `test_supervisor_forbidden` — 403 for supervisor at `/payroll/?status=adjustments`
+- `test_type_multi_filter` — `?type=Bonus&type=Overtime` returns union (not intersection)
+- `test_worker_multi_filter` — `?worker=1&worker=2` returns union
+- `test_team_cross_filter` — team filter uses subquery pattern (no inflation on linked counts)
+- `test_status_filter_unpaid` — only unpaid rows returned
+- `test_date_filter_single_vs_range` — single date = exact match; range = inclusive
+- `test_group_by_type` — response context has `adj_groups` with correct keys
+- `test_group_by_worker` — same with worker grouping
+- `test_bulk_delete_only_affects_unpaid` — POST with mixed paid+unpaid IDs deletes only unpaid
+- `test_bulk_delete_requires_admin` — 403 for supervisor
+
+(~11 tests; aim for ~130 lines of test code.)
+
+## 13. Scope estimate
+
+| Change | Lines |
+|---|---|
+| `core/views.py` — new `payroll_dashboard` branch + `_group_adjustments` helper + `bulk_delete_adjustments` endpoint | ~180 |
+| `core/templates/core/payroll_dashboard.html` — tab `
` + filter bar + sticky stats row + group-by toggle + table with grouping + row actions + bulk action bar | ~260 |
+| `core/templates/core/_adjustment_row.html` — shared row partial (used by both flat + grouped views) | ~40 |
+| `core/templatetags/format_tags.py` — `type_slug` filter | ~10 |
+| `static/css/custom.css` — 14 badge colour tokens × 2 themes + 7 badge classes + sticky filter bar + group header + bulk action bar | ~170 |
+| JS (inline in template) — Choices.js init × 3 multi-selects + Team→Worker cross-filter + bulk checkbox logic + group-header collapse + sort header clicks + date single/range toggle | ~170 |
+| `core/urls.py` — 1 new path (bulk-delete) | ~3 |
+| Tests in `core/tests.py` | ~130 |
+| **Total** | **~960 lines** |
+
+~12 tasks, 2 checkpoints.
+
+**Suggested checkpoints:**
+1. After core filter logic + Choices.js multi-selects + stats + sort + flat table render
+2. After group-by + bulk delete + date single/range + cross-filter + row actions + full QA
+
+## 14. Edge cases
+
+| Case | Behaviour |
+|---|---|
+| Filter returns 0 rows | Empty-state card with "Clear filters" + "Add new adjustment" CTAs |
+| Paid row selected via bulk checkbox (shouldn't happen via UI; defensive) | Backend silently skips paid rows in bulk-delete |
+| Worker with no teams (orphan) | Hidden from Workers dropdown when any Team filter is active |
+| Sort by column with missing values (e.g. sorting by worker name when worker deleted) | NULLs sort last; fallback to date-desc within same key |
+| Group-by on an empty filter result | Empty state message (no group headers shown) |
+| 500+ adjustments in DB | Pagination handles it — 50/page, URL `?page=2` |
+| User clicks Edit on a paid row (shouldn't have button; defensive) | Edit modal wouldn't allow edit; existing `edit_adjustment` view blocks paid |
+| Bulk-delete hits a race condition (another admin pays one of the selected) | Endpoint filters `payroll_record__isnull=True` at DELETE time; stale ones silently excluded |
+
+## 15. Dependencies on Feature 1 (inline filters)
+
+Feature 1's Choices.js infrastructure (CDN loading, SRI hashes, dark/light theme overrides) is already shipped. This feature reuses it directly — no duplicate CDN loads.
+
+Feature 1 retires the report-config modal; this feature is on a different page and doesn't interact with that change.
+
+**Order of implementation: either can ship first.** Feature 1 is smaller (~5-6 tasks) and may inform JS patterns we lift into Feature 2. Feature 2 is larger but self-contained. Suggested order: Feature 1 first, then Feature 2, separate plans.
+
+## 16. Out of scope (YAGNI)
+
+- **Bulk Mark Paid** — entangled with Pay Now flow; use existing per-worker `process_payment` via Preview modal
+- **CSV export** — easy to add later if requested; not in original ask
+- **Keyboard shortcuts beyond Esc** — browser Tab is fine
+- **Persistent sort / filter session state** — URL bookmark covers it
+- **Inline editing of description or amount** — Edit modal is sufficient
+- **Adjustment history / audit log** — would require a new model; not asked for
+- **Group by Project** — Type and Worker cover the two most useful axes
+
+## 17. Rollback
+
+Template + view additions only; one new endpoint; no schema changes. Rollback = revert the commits (or disable the tab via template edit if a panic fix is needed).
+
+## 18. Next step
+
+Hand off to `superpowers:writing-plans`. Two design docs exist today:
+- `docs/plans/2026-04-23-inline-filters-design.md` (Feature 1 — report page pills)
+- `docs/plans/2026-04-23-adjustments-tab-design.md` (this doc — Feature 2)
+
+Recommended sequence: **Feature 1 first** (smaller, ~5-6 tasks; Choices.js patterns learned here can lift into Feature 2). Ship Feature 1, validate on production, then Feature 2's plan + implementation. Both design docs stay local until their respective implementations ship; then push everything together.