diff --git a/fusion_clock/docs/superpowers/specs/2026-05-31-biweekly-attendance-filter-design.md b/fusion_clock/docs/superpowers/specs/2026-05-31-biweekly-attendance-filter-design.md
new file mode 100644
index 00000000..344be129
--- /dev/null
+++ b/fusion_clock/docs/superpowers/specs/2026-05-31-biweekly-attendance-filter-design.md
@@ -0,0 +1,105 @@
+# Fusion Clock — Bi-Weekly Attendance Filter Design
+
+**Date:** 2026-05-31
+**Module:** `fusion_clock`
+**Status:** Approved (brainstorming) — ready for implementation plan
+
+---
+
+## 1. Problem
+
+Operators reviewing **All Attendances** have no quick way to scope the list to a pay period. In Canada most payroll runs **bi-weekly**, so the common need is "show me this two-week pay period's attendances" (and step to the previous/next, or jump to an arbitrary period). The module already computes bi-weekly windows for its reports and already has the configuration — but none of it is exposed as a filter on the attendance list.
+
+## 2. Existing state (reused, not rebuilt)
+
+- **Period math already exists:** `fusion.clock.report._calculate_current_period(frequency, anchor_str, reference_date) → (start, end)` (`models/clock_report.py:457`). Handles `weekly` (7d), `biweekly` (14d), `semi_monthly`, `monthly`; uses floor division so dates *before* the anchor resolve correctly; anchor defaults to first-of-month when unset.
+- **Setting already exists:** Settings → Fusion Clock → Pay Period: **Frequency** (`fusion_clock.pay_period_type`, default `biweekly`) + **Anchor Date** (`fusion_clock.pay_period_start`, `YYYY-MM-DD`). Decision (brainstorming): **reuse this as the single source of truth** — no new setting.
+- **Attendance search view already inherited:** `view_hr_attendance_search_fusion_clock` (`views/hr_attendance_views.xml`) inherits `hr_attendance.hr_attendance_view_filter` and already adds custom filters — the natural home for the new period filters.
+- **TZ helpers:** `get_local_day_boundaries(env, date[, employee])` and `get_local_today(env)` in `models/tz_utils.py`.
+
+Decisions from brainstorming: reuse the Pay Period setting; provide **both** quick filters and a picker; the window **follows the Frequency setting** (one pay period; 2 weeks by default).
+
+## 3. Design
+
+### A. Shared period math (DRY)
+Extract the body of `_calculate_current_period` into a reusable, model-free helper so reports, filters, and the wizard share one implementation and never drift.
+
+`models/pay_period.py` (new):
+```python
+def compute_pay_period(frequency, anchor_str, reference_date) -> (date, date)
+ # identical logic to _calculate_current_period; pure function
+def period_length_days(frequency) -> int | None
+ # 7 for 'weekly', 14 for 'biweekly'/default, None for calendar-based (semi_monthly/monthly)
+def current_prev_next(frequency, anchor_str, today) -> dict
+ # {'current': (s,e), 'previous': (s,e), 'next': (s,e)} where
+ # previous = compute_pay_period(..., current_start - 1 day),
+ # next = compute_pay_period(..., current_end + 1 day) # works for ALL frequencies
+```
+`fusion.clock.report._calculate_current_period` becomes a thin delegator to `compute_pay_period` (no behaviour change).
+
+### B. Quick filters on the attendance list
+On `hr.attendance`, three **non-stored computed Boolean** fields, each with a `search` method (the compute returns `False` — display only; the search method does the work):
+- `x_fclk_in_current_period`, `x_fclk_in_previous_period`, `x_fclk_in_next_period`
+
+Each `_search_*(operator, value)`:
+1. Read `pay_period_type` + `pay_period_start` via `ICP.sudo().get_param`.
+2. Compute the window with `current_prev_next(...)` keyed on `get_local_today(env)`.
+3. Convert the date window to UTC bounds with `get_local_day_boundaries` (start → 00:00 local, end → next-day 00:00 / inclusive end-of-day).
+4. Return `['&', ('check_in', '>=', start_utc), ('check_in', '<', end_excl_utc)]`.
+
+Add three filters to `view_hr_attendance_search_fusion_clock`:
+```xml
+
+
+
+```
+
+### C. "Pick Pay Period" wizard
+`wizard/clock_period_picker_wizard.py` (new) — transient `fusion.clock.period.picker`:
+- `date_start` (Date, required) — default = current period start (`current_prev_next(...)['current'][0]`).
+- `date_end` (Date, required) — default = current period end; **editable**.
+- `@api.onchange('date_start')`: if `period_length_days(freq)` is not None → `date_end = date_start + length - 1`; else (calendar frequencies) → `date_end = compute_pay_period(freq, anchor, date_start)[1]`. (User can still override `date_end` → covers "set both, or just the start and auto-calc".)
+- `action_apply()` returns an `ir.actions.act_window`:
+ ```python
+ return {
+ 'type': 'ir.actions.act_window', 'name': f"Attendances · {date_start} – {date_end}",
+ 'res_model': 'hr.attendance', 'view_mode': 'list,form',
+ 'domain': ['&', ('check_in','>=', start_utc), ('check_in','<', end_excl_utc)],
+ 'target': 'current',
+ }
+ ```
+ (`start_utc`/`end_excl_utc` from `get_local_day_boundaries` on `date_start` / `date_end`.)
+
+`wizard/clock_period_picker_views.xml` (new): a small form (date_start, date_end, **Apply** + **Cancel**) and an `ir.actions.act_window` opening it as a dialog (`target="new"`).
+
+### D. Entry points
+- **Menu:** `views/clock_menus.xml` — add **Fusion Clock → Attendance → Bi-Weekly Period** (`sequence` after All Attendances), `groups="group_fusion_clock_manager,group_fusion_clock_team_lead"`, action = the picker wizard.
+- **Dashboard tile:** add an **onViewBiweekly()** handler (opens the picker wizard act_window) and a "🗓 Bi-Weekly Period" tile in the dashboard Quick Actions, inside the existing `t-if="state.team"` block (so only leads/managers see it).
+
+### E. Settings
+No new setting. Clarify the Anchor Date help/label in `res_config_settings_views.xml` to note it is the bi-weekly week-start used by **both** the reports and the attendance period filter.
+
+## 4. Permissions
+Filters, menu, and dashboard tile are gated to **manager + team-lead** (the attendance list itself is already gated to them in `clock_menus.xml`). Search methods read `ir.config_parameter` via `sudo()` (config only — no employee data). The returned domains run through Odoo's normal ACL/record rules, so a team-lead still sees only their own reports' attendance rows. No new data exposure.
+
+## 5. Edge cases
+- **No anchor set** → `compute_pay_period` falls back to first-of-month (existing behaviour); filters and picker still resolve a sane window. The picker pre-fills `date_start` with the computed current start so it is never blank.
+- **Frequency = semi_monthly / monthly** → window follows it; previous/next via `current_start − 1` / `current_end + 1` handles calendar stepping; picker auto-end uses the calendar period end containing the chosen start.
+- **TZ / DST** → date windows convert through `get_local_day_boundaries`, so a UTC `check_in` is matched against local pay-period days; end is exclusive next-day-00:00 to include the whole last day.
+- **date_end before date_start** in the picker → `@api.constrains` raises a friendly `ValidationError`.
+
+## 6. Testing (`tests/test_pay_period.py`, `@tagged('-at_install','post_install','fusion_clock')`)
+- `compute_pay_period` for weekly / biweekly / semi_monthly / monthly, including a reference date **before** the anchor (negative offset) and exact boundary days.
+- `current_prev_next` returns contiguous, non-overlapping windows for biweekly (prev_end + 1 day == current_start, current_end + 1 == next_start).
+- Create attendances spanning two bi-weekly periods; assert the **Current** search filter returns only current-period rows and **Previous** only previous-period rows.
+- Wizard: default `date_start` == current period start; `onchange` sets `date_end = start + 13` for biweekly; `action_apply` returns an act_window whose domain bounds equal the local-day UTC boundaries of the chosen window.
+
+## 7. Out of scope (YAGNI)
+Custom OWL toolbar dropdown on the list (native Filters menu + wizard instead); per-employee differing pay periods; editing the anchor from the picker; saving favourite/named periods; touching the gantt view.
+
+## 8. Files touched
+- New: `models/pay_period.py`, `wizard/clock_period_picker_wizard.py`, `wizard/clock_period_picker_views.xml`, `tests/test_pay_period.py`
+- Modify: `models/__init__.py`, `models/clock_report.py` (delegate), `models/hr_attendance.py` (3 fields + search methods), `wizard/__init__.py`, `views/hr_attendance_views.xml` (3 filters), `views/clock_menus.xml` (menu item), `views/res_config_settings_views.xml` (label text), `static/src/js/fusion_clock_dashboard.js` + `static/src/xml/fusion_clock_dashboard.xml` (tile), `__manifest__.py` (data entry for the wizard view + version bump)
+
+## 9. Deployment
+Local test on the dev container (when available), then the standard entech path: bump version, `git commit --only` the explicit paths, push **origin + gitea**, upgrade entech (`pct exec 111`, native `odoo.service`, DB `admin`, `--http-port=0 --gevent-port=0`), verify web 200 + installed version, hard-refresh.