diff --git a/specs/README.md b/specs/README.md
new file mode 100644
index 0000000..05e92fd
--- /dev/null
+++ b/specs/README.md
@@ -0,0 +1,50 @@
+# Functional Specifications
+
+This directory contains the functional specifications for the `checkboxes.js` jQuery plugin. Each file describes **what** a feature must do — the observable behaviour, edge cases, and invariants — independently of implementation details.
+
+## Who is this for?
+
+- Contributors adding or changing behaviour: read the spec first, then update tests, then code.
+- Reviewers: verify that a PR's code and tests match what the spec says.
+- Users: understand exactly what to expect from each method.
+
+## Relationship to other documentation
+
+| This directory | `tests/specs/` | `docs/index.html` |
+|---|---|---|
+| Prose description of expected behaviour | Executable verification of behaviour | Interactive demos and usage examples |
+| Source of truth for *what* | Source of truth for *how it's verified* | Source of truth for *how to use it* |
+
+## Files
+
+| Spec | Feature | Test file |
+|---|---|---|
+| [plugin.md](plugin.md) | Plugin instantiation, jQuery integration, no-conflict | `tests/specs/jquery_checkboxes_spec.js` |
+| [check.md](check.md) | `check()` — check all eligible checkboxes | `tests/specs/jquery_checkboxes_check_spec.js` |
+| [uncheck.md](uncheck.md) | `uncheck()` — uncheck all eligible checkboxes | `tests/specs/jquery_checkboxes_uncheck_spec.js` |
+| [toggle.md](toggle.md) | `toggle()` — flip state of all eligible checkboxes | `tests/specs/jquery_checkboxes_toggle_spec.js` |
+| [max.md](max.md) | `max(n)` — limit how many checkboxes can be checked | `tests/specs/jquery_checkboxes_max_spec.js` |
+| [range.md](range.md) | `range(enable)` — Shift+click range selection | `tests/specs/jquery_checkboxes_range_spec.js` |
+| [data-api.md](data-api.md) | Declarative Data API via `data-*` attributes | *(no dedicated test file yet)* |
+
+## Cross-cutting invariant: the "visible AND enabled" gate
+
+`check()`, `uncheck()`, and `toggle()` all share the same eligibility rule: a checkbox is only affected if it is both **visible** (not hidden via CSS) and **not disabled**. A checkbox that fails either condition is left completely unchanged.
+
+The `mixed.html` fixture concretely captures all four combinations:
+
+| Checkbox | Visible | Enabled | Affected by bulk methods? |
+|---|---|---|---|
+| `#5` | No (display:none) | Yes | No |
+| `#6` | No (display:none) | Yes | No |
+| `#7` | Yes | No (disabled) | No |
+| `#8` | Yes | No (disabled) | No |
+| `#1–#4, #9–#10` | Yes | Yes | **Yes** |
+
+## Keeping specs in sync
+
+When changing behaviour:
+1. Update the relevant spec file first.
+2. Update or add tests in `tests/specs/` to match.
+3. Change the implementation in `src/jquery.checkboxes.js`.
+4. Update `docs/index.html` if the user-facing API changed.
diff --git a/specs/check.md b/specs/check.md
new file mode 100644
index 0000000..5b3a1d0
--- /dev/null
+++ b/specs/check.md
@@ -0,0 +1,45 @@
+# `check()`
+
+> Sets all eligible checkboxes in the context to the checked state.
+
+## Signature
+
+```js
+$(context).checkboxes('check');
+```
+
+No parameters.
+
+## Behavior
+
+1. Every checkbox within the context that is both **visible** and **not disabled** is set to the checked state (`checked = true`).
+2. A `change` event is triggered on each affected checkbox.
+
+## Edge Cases
+
+- **Disabled checkboxes** — their state is left unchanged (whether previously checked or unchecked).
+- **Hidden checkboxes** (`display:none` or otherwise not `:visible`) — their state is left unchanged.
+- **Already-checked checkboxes** — remain checked; the `change` event is still triggered on them.
+- **Empty context** — no checkboxes are found; no events are fired; no error is thrown.
+
+## Constraints
+
+- Both conditions must hold simultaneously: a visible-but-disabled checkbox is skipped, and a hidden-but-enabled checkbox is also skipped.
+
+## Events
+
+- `change` is triggered on every checkbox whose state was updated.
+
+## Data API
+
+```html
+Check all
+```
+
+See [data-api.md](data-api.md) for full Data API behaviour.
+
+## Related
+
+- Test file: `tests/specs/jquery_checkboxes_check_spec.js`
+- Fixture: `tests/fixtures/checked.html`
+- Docs section: `docs/index.html` (Checking all checkboxes)
diff --git a/specs/data-api.md b/specs/data-api.md
new file mode 100644
index 0000000..4eec39a
--- /dev/null
+++ b/specs/data-api.md
@@ -0,0 +1,75 @@
+# Data API
+
+> Declarative interface that wires up `checkboxes.js` behaviour through HTML `data-*` attributes, requiring no JavaScript.
+
+## Overview
+
+The Data API provides two integration points:
+
+1. **Click handler** — responds to clicks on trigger elements to invoke a method on a target context.
+2. **DOM-ready scanner** — reads `data-*` attributes on context elements at page load and calls the corresponding methods automatically.
+
+## Click Handler
+
+Any element with `data-toggle="checkboxes"` and `data-action=""` becomes a trigger. When clicked, the plugin invokes `` on the target context.
+
+### Identifying the context
+
+The target context is resolved in this order:
+
+1. The value of the `data-context` attribute on the trigger element (used as a jQuery selector).
+2. The hash fragment of the trigger element's `href` attribute (e.g. `href="#my-list"` → `$('#my-list')`).
+
+`data-context` takes priority over `href`.
+
+### Default action prevention
+
+If the trigger element is **not** a checkbox itself, the browser's default action for that element (e.g. navigation for an ``) is prevented. If the trigger element is a checkbox, the default action is not prevented so the checkbox remains interactive.
+
+### Examples
+
+```html
+
+Check all
+
+
+
+
+
+ ...
+
+```
+
+## DOM-Ready Scanner
+
+At DOM ready the plugin scans every element matching `[data-toggle^=checkboxes]` and reads all its `data-*` attributes (the `toggle` attribute itself is excluded). Each attribute is treated as a method call:
+
+```
+data-="" → $(element).checkboxes('', )
+```
+
+This allows `max` and `range` to be configured declaratively:
+
+```html
+
+
+ ...
+
+
+
+
+ ...
+
+```
+
+## Constraints
+
+- The click handler is attached once at plugin load time as a **delegated** listener on `document`. It works for elements present in the DOM at any time, including those added dynamically after page load.
+- The DOM-ready scanner runs **once**. Elements added dynamically after DOM ready are not processed by the scanner. If you add a `[data-toggle^=checkboxes]` element later, call `$(element).checkboxes('', value)` manually.
+- If neither `data-context` nor `href` resolves to a non-empty jQuery set, or if `data-action` is absent, the click handler does nothing.
+- Unknown `data-action` values are silently ignored (the plugin's method dispatch no-ops on unknown names).
+
+## Related
+
+- Source: `src/jquery.checkboxes.js` (`dataApiClickHandler`, `dataApiDomReadyHandler`)
+- *(No dedicated test file yet — `data-api` behaviour is covered informally by the interactive demos in `docs/index.html`)*
diff --git a/specs/max.md b/specs/max.md
new file mode 100644
index 0000000..720365b
--- /dev/null
+++ b/specs/max.md
@@ -0,0 +1,54 @@
+# `max(n)`
+
+> Limits how many checkboxes can be checked simultaneously in a context by disabling all unchecked ones once the limit is reached.
+
+## Signature
+
+```js
+$(context).checkboxes('max', n);
+```
+
+| Parameter | Type | Required | Description |
+|---|---|---|---|
+| `n` | `number` | Yes | Maximum number of checkboxes that may be checked at the same time. Pass `0` (or any non-positive value) to remove the limit. |
+
+## Behavior
+
+1. After `max(n)` is called with a positive integer `n`, a `click` listener is attached to the context.
+2. After each click on any checkbox in the context, if the number of `:checked` checkboxes equals `n`, every unchecked checkbox in the context is disabled (`disabled = true`).
+3. If the count of `:checked` checkboxes drops below `n`, every unchecked checkbox is re-enabled (`disabled = false`).
+4. Calling `max(0)` (or any non-positive value) removes the click listener. Checkboxes that were disabled by the max mechanism are **not** automatically re-enabled when the limit is removed.
+5. Calling `max(n)` a second time replaces the previous listener; only the most recent value of `n` is enforced.
+
+## Edge Cases
+
+- **Setting max when checkboxes are already checked** — the limit is enforced only on subsequent click events; the existing state at the time `max()` is called is not validated retroactively.
+- **Unchecking when at the limit** — the click listener fires, the count drops below `n`, and unchecked checkboxes are re-enabled immediately.
+- **Checkboxes that are already natively disabled** — the max mechanism writes `disabled = true/false` to them too, which can overwrite their original disabled state.
+- **Clicking beyond the limit** — once all unchecked checkboxes are disabled, further clicks cannot increase the checked count.
+
+## Constraints
+
+- **`click`-only enforcement**: the limit is applied via a `click` event listener. Programmatic calls to `check()` or `toggle()` bypass the listener entirely and can result in more than `n` checkboxes being checked.
+- The limit applies to the entire context, not per-row or per-group.
+- The enforcement condition is `=== n` (strict equality, not `>= n`). If programmatic manipulation produces a count above `n`, unchecked checkboxes are not disabled until the count drops to exactly `n` through subsequent clicks.
+
+## Events
+
+No additional events are fired by the `max` mechanism itself. Normal browser `click` and `change` events on checkboxes are unaffected.
+
+## Data API
+
+```html
+
+ ...
+
+```
+
+The `data-max` attribute is read once at DOM-ready by the Data API scanner and calls `max(3)` on the element. See [data-api.md](data-api.md).
+
+## Related
+
+- Test file: `tests/specs/jquery_checkboxes_max_spec.js`
+- Fixture: `tests/fixtures/unchecked.html`
+- Docs section: `docs/index.html` (Limit max number of checked checkboxes)
diff --git a/specs/plugin.md b/specs/plugin.md
new file mode 100644
index 0000000..4268710
--- /dev/null
+++ b/specs/plugin.md
@@ -0,0 +1,42 @@
+# Plugin
+
+> The `checkboxes` jQuery plugin registers a `Checkboxes` instance on a context element and exposes its methods through the standard jQuery plugin interface.
+
+## Signature
+
+```js
+$(context).checkboxes(method [, ...args]);
+```
+
+| Parameter | Type | Required | Description |
+|---|---|---|---|
+| `method` | `string` | No | Name of the method to invoke (`'check'`, `'uncheck'`, `'toggle'`, `'max'`, `'range'`). |
+| `...args` | any | No | Arguments forwarded to the method. |
+
+## Behavior
+
+1. Calling `$(context).checkboxes(method)` on a jQuery-wrapped element creates a `Checkboxes` instance for that element and stores it in jQuery's internal data store under the key `'checkboxes'`.
+2. The same instance is reused on every subsequent call — the instance is created once per element.
+3. The named `method` is looked up on the instance and called with any extra arguments. Unrecognised method names are silently ignored.
+4. `$.fn.checkboxes` is chainable: it returns the original jQuery object so calls can be chained.
+5. `$.fn.checkboxes.Constructor` holds a reference to the `Checkboxes` class.
+
+## Edge Cases
+
+- **Empty selection** (`$().checkboxes()`) — no instance is created; no error is thrown; the empty jQuery set is returned.
+- **Unknown method name** — silently ignored; the instance is still created and stored.
+- **Method called before instance exists** — the instance is created on the first call, regardless of the method name.
+
+## No-Conflict
+
+`$.fn.checkboxes.noConflict()` restores the previous value of `$.fn.checkboxes` (if another plugin occupied that name before this one was loaded) and returns the plugin function so it can be assigned to a different name:
+
+```js
+let checkboxes = $.fn.checkboxes.noConflict();
+$.fn.myCheckboxes = checkboxes;
+```
+
+## Related
+
+- Test file: `tests/specs/jquery_checkboxes_spec.js`
+- Source: `src/jquery.checkboxes.js`
diff --git a/specs/range.md b/specs/range.md
new file mode 100644
index 0000000..6ade728
--- /dev/null
+++ b/specs/range.md
@@ -0,0 +1,57 @@
+# `range(enable)`
+
+> Enables Shift+click range selection: holding Shift while clicking a checkbox checks or unchecks all visible checkboxes between the last-clicked checkbox and the current one.
+
+## Signature
+
+```js
+$(context).checkboxes('range', enable);
+```
+
+| Parameter | Type | Required | Description |
+|---|---|---|---|
+| `enable` | `boolean` | Yes | `true` to enable range selection; `false` to disable it. |
+
+## Behavior
+
+1. After `range(true)` is called, a `click` listener is attached to the context.
+2. Every click on a checkbox (Shift held or not) updates an internal **anchor** — the last-clicked checkbox.
+3. When the user Shift+clicks a checkbox and an anchor exists, all visible checkboxes between the anchor and the clicked checkbox (inclusive, in DOM order) are set to the same checked state as the clicked checkbox.
+4. Disabled checkboxes within the range are skipped; their state is not changed.
+5. A `change` event is triggered on each checkbox whose state was updated by the range operation.
+6. Calling `range(false)` removes the click listener; subsequent Shift+clicks behave as normal browser checkbox clicks.
+
+## Edge Cases
+
+- **First click without Shift** — recorded as the new anchor; no range action; normal checkbox toggle occurs.
+- **Shift+click with no existing anchor** — no range action; the clicked checkbox toggles normally and becomes the new anchor.
+- **Range of one** — Shift+clicking the same checkbox as the anchor affects only that checkbox.
+- **Reverse direction** — Shift+clicking a checkbox that appears before the anchor in the DOM still selects the full range between them. Direction does not matter.
+- **Hidden checkboxes in the range** — excluded from range computation. The range is calculated over the index of `:checkbox:visible` elements, so hidden checkboxes do not occupy a slot.
+- **Disabled checkboxes in the range** — included in the index range but their state is not changed.
+- **Calling `range(true)` twice** — attaches a second listener on top of the first; range actions fire twice per click. Call `range(false)` first to reset, or rely on the Data API which calls it only once.
+
+## Constraints
+
+- The range is computed using the visible-checkbox index at the moment of the Shift+click. Checkboxes added or removed between the anchor click and the Shift+click may shift indices and produce unexpected ranges.
+- The state applied to the entire range is the **final checked state of the Shift+clicked checkbox** (i.e., the state after the browser has toggled it), not the state of the anchor.
+
+## Events
+
+- `change` is triggered on each checkbox in the range whose state was modified.
+
+## Data API
+
+```html
+
+ ...
+
+```
+
+The `data-range` attribute is read once at DOM-ready by the Data API scanner and calls `range(true)` on the element. See [data-api.md](data-api.md).
+
+## Related
+
+- Test file: `tests/specs/jquery_checkboxes_range_spec.js` *(existence only — Shift+click behaviour has no automated test yet)*
+- Fixture: `tests/fixtures/mixed.html`
+- Docs section: `docs/index.html` (Range selection of checkboxes)
diff --git a/specs/toggle.md b/specs/toggle.md
new file mode 100644
index 0000000..3fa903d
--- /dev/null
+++ b/specs/toggle.md
@@ -0,0 +1,47 @@
+# `toggle()`
+
+> Flips the checked state of every eligible checkbox in the context individually.
+
+## Signature
+
+```js
+$(context).checkboxes('toggle');
+```
+
+No parameters.
+
+## Behavior
+
+1. Every checkbox within the context that is both **visible** and **not disabled** has its state inverted: checked becomes unchecked, unchecked becomes checked.
+2. Each checkbox is evaluated and flipped **independently** — the result depends on that checkbox's own current state, not on the majority state of the context.
+3. A `change` event is triggered on the full set of affected checkboxes after all states have been flipped.
+
+## Edge Cases
+
+- **Disabled checkboxes** — their state is left unchanged.
+- **Hidden checkboxes** (`display:none` or otherwise not `:visible`) — their state is left unchanged.
+- **Mixed state** — a context with 3 checked and 2 unchecked eligible checkboxes will result in 3 unchecked and 2 checked; there is no "majority wins" logic.
+- **Empty context** — no checkboxes are found; no events are fired; no error is thrown.
+
+## Constraints
+
+- Both visibility and enabled conditions must hold simultaneously (same gate as `check()` and `uncheck()`).
+- The `change` event fires once on the set, after all individual flips are done — not once per checkbox.
+
+## Events
+
+- `change` is triggered on the set of affected checkboxes (fired once after all flips).
+
+## Data API
+
+```html
+Toggle all
+```
+
+See [data-api.md](data-api.md) for full Data API behaviour.
+
+## Related
+
+- Test file: `tests/specs/jquery_checkboxes_toggle_spec.js`
+- Fixture: `tests/fixtures/mixed.html`
+- Docs section: `docs/index.html` (Toggling all checkboxes)
diff --git a/specs/uncheck.md b/specs/uncheck.md
new file mode 100644
index 0000000..abd6bdf
--- /dev/null
+++ b/specs/uncheck.md
@@ -0,0 +1,45 @@
+# `uncheck()`
+
+> Sets all eligible checkboxes in the context to the unchecked state.
+
+## Signature
+
+```js
+$(context).checkboxes('uncheck');
+```
+
+No parameters.
+
+## Behavior
+
+1. Every checkbox within the context that is both **visible** and **not disabled** is set to the unchecked state (`checked = false`).
+2. A `change` event is triggered on each affected checkbox.
+
+## Edge Cases
+
+- **Disabled checkboxes** — their state is left unchanged (whether previously checked or unchecked).
+- **Hidden checkboxes** (`display:none` or otherwise not `:visible`) — their state is left unchanged.
+- **Already-unchecked checkboxes** — remain unchecked; the `change` event is still triggered on them.
+- **Empty context** — no checkboxes are found; no events are fired; no error is thrown.
+
+## Constraints
+
+- Both conditions must hold simultaneously: a visible-but-disabled checkbox is skipped, and a hidden-but-enabled checkbox is also skipped.
+
+## Events
+
+- `change` is triggered on every checkbox whose state was updated.
+
+## Data API
+
+```html
+Uncheck all
+```
+
+See [data-api.md](data-api.md) for full Data API behaviour.
+
+## Related
+
+- Test file: `tests/specs/jquery_checkboxes_uncheck_spec.js`
+- Fixture: `tests/fixtures/mixed.html`
+- Docs section: `docs/index.html` (Unchecking all checkboxes)