diff --git a/docs/api/calendar.md b/docs/api/calendar.md
new file mode 100644
index 0000000..99c1756
--- /dev/null
+++ b/docs/api/calendar.md
@@ -0,0 +1,328 @@
+---
+description: "Calendar and date helpers."
+---
+
+# `calendar`
+
+Calendar and date helpers.
+
+> [!WARNING]
+>
+> This module is still under development and may not be stable. The API is
+> incomplete and may change in future versions.
+
+## Usage
+
+```lua
+cal = require "mods.calendar"
+
+print(cal.weekday(2026, 3, 26)) --> 4
+```
+
+## Functions
+
+| Function | Description |
+| ------------------------------------------------------ | --------------------------------- |
+| [`getfirstweekday()`](#fn-getfirstweekday) | Return the default first weekday. |
+| [`setfirstweekday(firstweekday)`](#fn-setfirstweekday) | Set the default first weekday. |
+
+**Calendar Calculations**:
+
+| Function | Description |
+| ------------------------------------------- | ----------------------------------------------------------------------- |
+| [`isleap(year)`](#fn-isleap) | Return `true` for leap years. |
+| [`leapdays(y1, y2)`](#fn-leapdays) | Return the number of leap years from `y1` up to but not including `y2`. |
+| [`monthrange(year, month)`](#fn-monthrange) | Return the first weekday and number of days for a month. |
+| [`weekday(year, month, day)`](#fn-weekday) | Return weekday number where Monday is `1` and Sunday is `7`. |
+
+**Formatting**:
+
+| Function | Description |
+| ----------------------------------------------------- | ------------------------------------------- |
+| [`weekheader(width?, firstweekday?)`](#fn-weekheader) | Return the formatted weekday header string. |
+
+**Iterators**:
+
+| Function | Description |
+| -------------------------------------------------------- | ---------------------------------------------------------------------- |
+| [`monthdays(year, month, firstweekday?)`](#fn-monthdays) | Iterate `(year, month, day, weekday)` tuples for a full calendar grid. |
+| [`weekdays(firstweekday?)`](#fn-weekdays) | Iterate weekday numbers for one full week. |
+
+
+
+### `getfirstweekday()`
+
+Return the default first weekday.
+
+**Return**:
+
+- `firstweekday` (`1|2|3|4|5|6|7`)
+
+**Example**:
+
+```lua
+local cal = mods.calendar
+print(cal.getfirstweekday()) --> 1
+```
+
+> [!NOTE]
+>
+> This returns the same value as `cal.firstweekday`.
+
+
+
+### `setfirstweekday(firstweekday)`
+
+Set the default first weekday.
+
+**Parameters**:
+
+- `firstweekday` (`1|2|3|4|5|6|7`)
+
+**Example**:
+
+```lua
+local cal = mods.calendar
+cal.setfirstweekday(cal.SUNDAY)
+```
+
+> [!NOTE]
+>
+> This updates the same value as `cal.firstweekday = ...`.
+
+### Calendar Calculations
+
+
+
+#### `isleap(year)`
+
+Return `true` for leap years.
+
+**Parameters**:
+
+- `year` (`integer`)
+
+**Return**:
+
+- `isLeap` (`boolean`)
+
+**Example**:
+
+```lua
+local cal = mods.calendar
+print(cal.isleap(2024)) --> true
+```
+
+
+
+#### `leapdays(y1, y2)`
+
+Return the number of leap years from `y1` up to but not including `y2`.
+
+**Parameters**:
+
+- `y1` (`integer`)
+- `y2` (`integer`)
+
+**Return**:
+
+- `count` (`integer`)
+
+**Example**:
+
+```lua
+local cal = mods.calendar
+print(cal.leapdays(2000, 2025)) --> 7
+```
+
+
+
+#### `monthrange(year, month)`
+
+Return the first weekday and number of days for a month.
+
+**Parameters**:
+
+- `year` (`integer`)
+- `month` (`integer`)
+
+**Return**:
+
+- `weekday` (`1|2|3|4|5|6|7`)
+- `ndays` (`integer`)
+
+**Example**:
+
+```lua
+local cal = mods.calendar
+wday, ndays = cal.monthrange(2026, 2)
+print(wday, ndays) --> 7 28
+```
+
+
+
+#### `weekday(year, month, day)`
+
+Return weekday number where Monday is `1` and Sunday is `7`.
+
+**Parameters**:
+
+- `year` (`integer`)
+- `month` (`1|2|3|4|5|6|7|8|9|10|11|12`)
+- `day`
+ (`1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31`)
+
+**Return**:
+
+- `weekday` (`1|2|3|4|5|6|7`)
+
+**Example**:
+
+```lua
+local cal = mods.calendar
+print(cal.weekday(2026, 3, 26)) --> 4
+```
+
+### Formatting
+
+
+
+#### `weekheader(width?, firstweekday?)`
+
+Return the formatted weekday header string.
+
+**Parameters**:
+
+- `width?` (`integer`)
+- `firstweekday?` (`1|2|3|4|5|6|7`)
+
+**Return**:
+
+- `header` (`string`)
+
+**Example**:
+
+```lua
+local cal = mods.calendar
+
+print(cal.weekheader(1, cal.SUNDAY)) --> "S M T W T F S"
+print(cal.weekheader(2, cal.SUNDAY)) --> "Su Mo Tu We Th Fr Sa"
+print(cal.weekheader(3, cal.SUNDAY)) --> "Sun Mon Tue Wed Thu Fri Sat"
+```
+
+### Iterators
+
+
+
+#### `monthdays(year, month, firstweekday?)`
+
+Iterate `(year, month, day, weekday)` tuples for a full calendar grid.
+
+**Parameters**:
+
+- `year` (`integer`)
+- `month` (`1|2|3|4|5|6|7|8|9|10|11|12`)
+- `firstweekday?` (`1|2|3|4|5|6|7`)
+
+**Return**:
+
+- `iter`
+ (`fun():year:integer,month:modsCalendarMonth,day:modsCalendarMonthday,weekday:modsCalendarWeekday`)
+
+**Example**:
+
+```lua
+local List = mods.list
+local cal = mods.calendar
+local str = mods.str
+
+local header = cal.weekheader(2)
+local lines = List({
+ str.center(("%s %d"):format(cal.months[cal.FEBRUARY], 2026), #header),
+ header,
+})
+
+local cells = List()
+for _, m, d, _ in cal.monthdays(2026, cal.FEBRUARY) do
+ cells:append(m == cal.FEBRUARY and ("%2d"):format(d) or " ")
+ if #cells == 7 then
+ lines:append(cells:join(" "))
+ cells = List()
+ end
+end
+
+print(lines:join("\n"))
+-- February 2026
+-- Mo Tu We Th Fr Sa Su
+-- 1
+-- 2 3 4 5 6 7 8
+-- 9 10 11 12 13 14 15
+-- 16 17 18 19 20 21 22
+-- 23 24 25 26 27 28
+```
+
+
+
+#### `weekdays(firstweekday?)`
+
+Iterate weekday numbers for one full week.
+
+**Parameters**:
+
+- `firstweekday?` (`1|2|3|4|5|6|7`)
+
+**Return**:
+
+- `iter` (`fun():modsCalendarWeekday`)
+
+**Example**:
+
+```lua
+local cal = mods.calendar
+local weekdays = {}
+for day in cal.weekdays() do
+ weekdays[#weekdays + 1] = day
+end
+print(table.concat(weekdays, ", ")) --> "1, 2, 3, 4, 5, 6, 7"
+```
+
+## Fields
+
+
+
+### `days` (`mods.List`)
+
+Weekday names indexed from `1` (Monday) to `7` (Sunday).
+
+```lua
+print(cal.days[1]) --> Monday
+print(cal.days[7]) --> Sunday
+```
+
+
+
+### `firstweekday` (`1|2|3|4|5|6|7`)
+
+The default first weekday field.
+
+```lua
+print(cal.firstweekday) --> 1
+cal.firstweekday = cal.SUNDAY
+print(cal.firstweekday) --> 7
+```
+
+> [!NOTE]
+>
+> Reading or writing this property is equivalent to calling `getfirstweekday()`
+> or `setfirstweekday()`.
+
+
+
+### `months` (`mods.List`)
+
+Month names indexed from `1` to `12`.
+
+```lua
+print(cal.months[1]) --> January
+print(cal.months[12]) --> December
+```
diff --git a/docs/api/date.md b/docs/api/date.md
new file mode 100644
index 0000000..c39f439
--- /dev/null
+++ b/docs/api/date.md
@@ -0,0 +1,1392 @@
+---
+description: "Timezone-naive date helpers and immutable date values."
+---
+
+# `date`
+
+Timezone-naive date helpers and immutable date values.
+
+## Usage
+
+```lua
+local Date = require "mods.date"
+
+local a = Date("2026-03-30T14:45:06")
+local b = Date("2026-03-30 14:45:06.123")
+local c = Date("2026-03-31")
+local d = Date("12-25-1995", "MM-DD-YYYY")
+local e = Date({ year = 2026, month = 3, day = 30, hour = 14, min = 45 })
+local f = Date({ year = 2026 })
+
+print(a) --> 2026-03-30 14:45:06
+print(b.ms) --> 123
+print(a:format("YYYY/MM/DD HH:mm:ss")) --> 2026/03/30 14:45:06
+print(a:is_before(c)) --> true
+print(a < c) --> true
+print(d) --> 1995-12-25 00:00:00
+print(e.sec) --> 0
+print(f) --> 2026-01-01 00:00:00
+```
+
+> [!NOTE]
+>
+> - String inputs accept [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)
+> forms, variants using a space instead of `T`, and custom formats via
+> `Date(input, pattern)`:
+>
+> ```lua
+> Date("2026-03-30T14:45:06")
+> Date("2026-03-30 14:45:06")
+> Date("12/25/1995", "MM/DD/YYYY")
+> ```
+>
+> - When `input` is a number, it is treated as Unix milliseconds. Use
+> [`Date.unix(ts)`](#fn-unix) if you have a timestamp in seconds.
+>
+> ```lua
+> local a = Date(1745155206123) -- Milliseconds
+> local b = Date.unix(1745155206.123) -- Seconds
+> print(a == b) --> true
+> ```
+>
+> - When calling `Date` without arguments, it uses
+> [mstime](https://github.com/luamod/mstime) for millisecond precision if
+> installed; otherwise it falls back to
+> [`os.time`](https://www.lua.org/manual/5.1/manual.html#pdf-os.time).
+
+## Functions
+
+| Function | Description |
+| --------------------------------- | ------------------------------------------------------------------------ |
+| [`new(input, pattern?)`](#fn-new) | Create a Date from a string using an optional pattern. |
+| [`new(input?)`](#fn-new) | Create a Date from a Unix timestamp (milliseconds) or a DateParts table. |
+
+**Arithmetic**:
+
+| Function | Description |
+| ----------------------------------------- | ------------------------------------------------------------------- |
+| [`add(amount, unit?)`](#fn-add) | Return a copy shifted by the given amount and unit. |
+| [`diff(date, unit?)`](#fn-diff) | Return the signed difference to another Date in the requested unit. |
+| [`subtract(amount, unit?)`](#fn-subtract) | Return a copy shifted backward by the given amount and unit. |
+
+**Boundaries**:
+
+| Function | Description |
+| ------------------------------ | --------------------------------------------- |
+| [`endof(unit)`](#fn-endof) | Return the end boundary for the given unit. |
+| [`startof(unit)`](#fn-startof) | Return the start boundary for the given unit. |
+
+**Calendar**:
+
+| Function | Description |
+| ----------------------------------------------------- | --------------------------------------------------------------------------- |
+| [`day_of_year(day_of_year_number?)`](#fn-day-of-year) | Return or set the day of the year. |
+| [`is_leap_year()`](#fn-is-leap-year) | Return `true` when the value's year is a leap year. |
+| [`iso_week(iso_week_number?)`](#fn-iso-week) | Return or set the ISO week-of-year number. |
+| [`iso_week_year()`](#fn-iso-week-year) | Return the ISO week-year for the current date. |
+| [`iso_weekday(iso_weekday_number?)`](#fn-iso-weekday) | Return or set the ISO weekday number where Monday is `1` and Sunday is `7`. |
+| [`iso_weeks_in_year()`](#fn-iso-weeks-in-year) | Return the number of ISO weeks in the current date's calendar year. |
+| [`month_days()`](#fn-month-days) | Return the number of days in the value's month. |
+| [`quarter(quarter_number?)`](#fn-quarter) | Return or set the quarter of the year. |
+| [`week(week_number?)`](#fn-week) | Return or set the non-ISO week-of-year number. |
+| [`week_year()`](#fn-week-year) | Return the non-ISO week-year for the current date. |
+| [`weekday(weekday_number?)`](#fn-weekday) | Return or set the locale-relative weekday like Day.js `weekday()`. |
+| [`weeks_in_year()`](#fn-weeks-in-year) | Return the number of weeks in the current locale week-year. |
+
+**Compare**:
+
+| Function | Description |
+| --------------------------- | ----------------------------------------------------------- |
+| [`max(...)`](#fn-max) | Return the latest value from the given dates. |
+| [`min(...)`](#fn-min) | Return the earliest value from the given dates. |
+| [`minmax(...)`](#fn-minmax) | Return the earliest and latest values from the given dates. |
+
+**Comparison**:
+
+| Function | Description |
+| ---------------------------------------------------------------- | ----------------------------------------------------------------- |
+| [`is_after(date)`](#fn-is-after) | Return `true` when the value is later than `other`. |
+| [`is_before(date)`](#fn-is-before) | Return `true` when the value is earlier than `other`. |
+| [`is_between(start_date, end_date, inclusive?)`](#fn-is-between) | Return `true` when the value lies between two bounds. |
+| [`is_same(date)`](#fn-is-same) | Return `true` when the value is equal to `other`. |
+| [`is_same_or_after(date)`](#fn-is-same-or-after) | Return `true` when the value is later than or equal to `other`. |
+| [`is_same_or_before(date)`](#fn-is-same-or-before) | Return `true` when the value is earlier than or equal to `other`. |
+| [`is_today()`](#fn-is-today) | Return `true` when the value falls on the current local day. |
+| [`is_tomorrow()`](#fn-is-tomorrow) | Return `true` when the value falls on the next local day. |
+| [`is_yesterday()`](#fn-is-yesterday) | Return `true` when the value falls on the previous local day. |
+
+**Duration**:
+
+| Function | Description |
+| --------------------------------------- | --------------------------------------------------------------------------- |
+| [`is_duration(value)`](#fn-is-duration) | Return `true` when the value is a duration created by `date.duration(...)`. |
+
+**Formatting**:
+
+| Function | Description |
+| ------------------------------- | ------------------------------------------------------------------------------------------------------- |
+| [`format(pattern)`](#fn-format) | Format the Date with tokens like `YYYY`, `MMM`, `dddd`, `Do`, `Q`, `hh`, `k`, `X`, `x`, `A`, and `SSS`. |
+| [`tostring()`](#fn-tostring) | Return the default string form `YYYY-MM-DD HH:mm:ss`. |
+
+**Relative Time**:
+
+| Function | Description |
+| ------------------------------------------- | -------------------------------------------------------------- |
+| [`from(date, without_suffix?)`](#fn-from) | Return relative time from another Date to this one. |
+| [`from_now(without_suffix?)`](#fn-from-now) | Return relative time from the current local time to this Date. |
+| [`to(date, without_suffix?)`](#fn-to) | Return relative time from this Date to another one. |
+| [`to_now(without_suffix?)`](#fn-to-now) | Return relative time from this Date to the current local time. |
+
+**Unix**:
+
+| Function | Description |
+| ----------------------------- | ------------------------------------------------------------------- |
+| [`unix(timestamp)`](#fn-unix) | Create a Date from a Unix timestamp in whole or fractional seconds. |
+
+**Validation**:
+
+| Function | Description |
+| -------------------------------------------- | ----------------------------------------------------------- |
+| [`is_valid(input?, pattern?)`](#fn-is-valid) | Return `true` when the input can be parsed as a valid Date. |
+
+**Metamethods**:
+
+| Function | Description |
+| ------------------------------ | ----------------------------------------------------------------------- |
+| [`__add(a, b)`](#fn-add) | Return a copy shifted by integer milliseconds. |
+| [`__eq(date)`](#fn-eq) | Return `true` when both Date values have identical components. |
+| [`__le(date)`](#fn-le) | Return `true` when the left Date is earlier than or equal to the right. |
+| [`__lt(date)`](#fn-lt) | Return `true` when the left Date is earlier than the right. |
+| [`__sub(a, b)`](#fn-sub) | Return either a shifted copy or a millisecond delta. |
+| [`__tostring()`](#fn-tostring) | Return the same result as `tostring()` when coerced to a string. |
+
+
+
+### `new(input, pattern?)`
+
+Create a Date from a string using an optional pattern.
+
+**Parameters**:
+
+- `input` (`string`): The date string to parse.
+- `pattern?` (`string`): The format pattern.
+
+**Return**:
+
+- **value** (`mods.Date`)
+
+**Example**:
+
+```lua
+local d1 = Date("2026-03-30")
+local d2 = Date("12-25-1995", "MM-DD-YYYY")
+```
+
+
+
+### `new(input?)`
+
+Create a Date from a Unix timestamp (milliseconds) or a DateParts table.
+
+**Parameters**:
+
+- `input?` (`number|mods.DateParts`): Unix timestamp (ms) or table of date
+ components.
+
+**Return**:
+
+- **value** (`mods.Date`)
+
+**Example**:
+
+```lua
+local d1 = Date(1745155206123)
+local d2 = Date({ year = 2026, month = 3 })
+```
+
+### Arithmetic
+
+
+
+#### `add(amount, unit?)`
+
+Return a copy shifted by the given amount and unit.
+
+**Parameters**:
+
+- `amount` (`integer|mods.DateDurationParts`): Signed amount to add, or a
+ duration-style table.
+- `unit?`
+ (`'ms'|'milliseconds'|'millisecond'|'s'|'secs'|'sec'|'seconds'|'second'|'m'|'mins'|'min'|'minutes'|'minute'|'h'|'hours'|'hour'|'d'|'days'|'day'|'w'|'weeks'|'week'|'M'|'months'|'month'|'q'|'quarters'|'quarter'|'y'|'years'|'year'`):
+ Unit for the addition.
+
+**Return**:
+
+- `shifted` (`mods.Date`): Shifted date value.
+
+**Example**:
+
+```lua
+local d = Date("2026-03-30T14:45:06")
+
+print(d:add(2, "day")) --> 2026-04-01 14:45:06
+print(d:add(1, "quarter")) --> 2026-06-30 14:45:06
+print(d:add(1, "month")) --> 2026-04-30 14:45:06
+print(d:add(250, "ms")) --> 2026-03-30 14:45:06.250
+print(d:add({ month = 1, day = 2 })) --> 2026-05-02 14:45:06
+```
+
+
+
+#### `diff(date, unit?)`
+
+Return the signed difference to another Date in the requested unit.
+
+**Parameters**:
+
+- `date` (`mods.Date`): Date to compare against.
+- `unit?`
+ (`'ms'|'milliseconds'|'millisecond'|'s'|'secs'|'sec'|'seconds'|'second'|'m'|'mins'|'min'|'minutes'|'minute'|'h'|'hours'|'hour'|'d'|'days'|'day'|'w'|'weeks'|'week'|'M'|'months'|'month'|'q'|'quarters'|'quarter'|'y'|'years'|'year'`):
+ Unit used for the difference. Defaults to `"ms"`.
+
+**Return**:
+
+- `delta` (`integer`): Signed difference in whole units.
+
+**Example**:
+
+```lua
+local a = Date("2026-03-30T12:00:00")
+local b = Date("2026-02-28T12:00:00")
+print(a:diff(b, "month")) --> 1
+print(a:diff(b, "day")) --> 30
+```
+
+
+
+#### `subtract(amount, unit?)`
+
+Return a copy shifted backward by the given amount and unit.
+
+**Parameters**:
+
+- `amount` (`integer|mods.DateDurationParts`): Signed amount to subtract, or a
+ duration-style table.
+- `unit?`
+ (`'ms'|'milliseconds'|'millisecond'|'s'|'secs'|'sec'|'seconds'|'second'|'m'|'mins'|'min'|'minutes'|'minute'|'h'|'hours'|'hour'|'d'|'days'|'day'|'w'|'weeks'|'week'|'M'|'months'|'month'|'q'|'quarters'|'quarter'|'y'|'years'|'year'`):
+ Unit for the subtraction.
+
+**Return**:
+
+- `shifted` (`mods.Date`): Shifted date value.
+
+**Example**:
+
+```lua
+local d = Date("2026-03-30T14:45:06")
+print(d:subtract(2, "day")) --> 2026-03-28 14:45:06
+print(d:subtract(1, "quarter")) --> 2025-12-30 14:45:06
+print(d:subtract(1, "month")) --> 2026-02-28 14:45:06
+print(d:subtract(250, "ms")) --> 2026-03-30 14:45:05.750
+print(d:subtract({ month = 1, day = 1 })) --> 2026-02-27 14:45:06
+```
+
+### Boundaries
+
+
+
+#### `endof(unit)`
+
+Return the end boundary for the given unit.
+
+**Parameters**:
+
+- `unit` (`mods.DateUnit|"isoWeek"`): Boundary unit.
+
+**Return**:
+
+- `bounded` (`mods.Date`): Date clamped to the end of the unit.
+
+**Example**:
+
+```lua
+local d = Date("2026-03-30T14:45:06")
+print(d:endof("month")) --> 2026-03-31 23:59:59
+print(d:endof("week")) --> 2026-04-05 23:59:59
+print(d:endof("isoWeek")) --> 2026-04-05 23:59:59
+```
+
+`"isoWeek"` is also supported here as a boundary-only unit.
+
+
+
+#### `startof(unit)`
+
+Return the start boundary for the given unit.
+
+**Parameters**:
+
+- `unit` (`mods.DateUnit|"isoWeek"`): Boundary unit.
+
+**Return**:
+
+- `bounded` (`mods.Date`): Date clamped to the start of the unit.
+
+**Example**:
+
+```lua
+local d = Date("2026-03-30T14:45:06")
+print(d:startof("day")) --> 2026-03-30 00:00:00
+print(d:startof("quarter")) --> 2026-01-01 00:00:00
+print(d:startof("isoWeek")) --> 2026-03-30 00:00:00
+```
+
+`"isoWeek"` is also supported here as a boundary-only unit.
+
+### Calendar
+
+
+
+#### `day_of_year(day_of_year_number?)`
+
+Return or set the day of the year.
+
+**Parameters**:
+
+- `day_of_year_number?` (`integer`): Day-of-year to set.
+
+**Return**:
+
+- `dayOrDate` (`integer|mods.Date`): Current day-of-year number, or a shifted
+ Date when `day_of_year_number` is provided.
+
+**Example**:
+
+```lua
+local d = Date("2026-03-30")
+print(d:day_of_year()) --> 89
+print(d:day_of_year(1)) --> 2026-01-01 00:00:00
+```
+
+
+
+#### `is_leap_year()`
+
+Return `true` when the value's year is a leap year.
+
+**Return**:
+
+- `isLeapYear` (`boolean`): Whether the year is a leap year.
+
+**Example**:
+
+```lua
+print(Date("2024-02-29"):is_leap_year()) --> true
+```
+
+
+
+#### `iso_week(iso_week_number?)`
+
+Return or set the ISO week-of-year number.
+
+**Parameters**:
+
+- `iso_week_number?` (`integer`): ISO week number to set.
+
+**Return**:
+
+- `isoWeekOrDate` (`integer|mods.Date`): Current ISO week number, or a shifted
+ Date when `iso_week_number` is provided.
+
+**Example**:
+
+```lua
+local d = Date("2026-03-30")
+print(d:iso_week()) --> 14
+print(d:iso_week(15)) --> 2026-04-06 00:00:00
+```
+
+
+
+#### `iso_week_year()`
+
+Return the ISO week-year for the current date.
+
+**Return**:
+
+- `isoWeekYear` (`integer`): ISO week-year.
+
+**Example**:
+
+```lua
+print(Date("2021-01-01"):iso_week_year()) --> 2020
+```
+
+
+
+#### `iso_weekday(iso_weekday_number?)`
+
+Return or set the ISO weekday number where Monday is `1` and Sunday is `7`.
+
+**Parameters**:
+
+- `iso_weekday_number?` (`integer`): ISO weekday to set.
+
+**Return**:
+
+- `isoWeekdayOrDate` (`modsCalendarWeekday|mods.Date`): Current ISO weekday
+ number, or a shifted Date when `iso_weekday_number` is provided.
+
+**Example**:
+
+```lua
+local d = Date("2026-03-30")
+print(d:iso_weekday()) --> 1
+print(d:iso_weekday(7)) --> 2026-04-05 00:00:00
+```
+
+
+
+#### `iso_weeks_in_year()`
+
+Return the number of ISO weeks in the current date's calendar year.
+
+**Return**:
+
+- `isoWeeksInYear` (`integer`): Number of ISO weeks in the current date's
+ calendar year.
+
+**Example**:
+
+```lua
+local d = Date("2016-01-01")
+print(Date("2016-01-01"):iso_weeks_in_year()) --> 52
+print(Date("2016-06-01"):iso_weeks_in_year()) --> 52
+```
+
+
+
+#### `month_days()`
+
+Return the number of days in the value's month.
+
+**Return**:
+
+- `ndays` (`modsCalendarMonthday`): Number of days in the current month.
+
+**Example**:
+
+```lua
+print(Date("2024-02-01"):month_days()) --> 29
+```
+
+
+
+#### `quarter(quarter_number?)`
+
+Return or set the quarter of the year.
+
+**Parameters**:
+
+- `quarter_number?` (`integer`): Quarter to set.
+
+**Return**:
+
+- `quarterOrDate` (`integer|mods.Date`): Current quarter number, or a shifted
+ Date when `quarter_number` is provided.
+
+**Example**:
+
+```lua
+local d = Date("2026-03-30")
+print(d:quarter()) --> 1
+print(d:quarter(2)) --> 2026-06-30 00:00:00
+```
+
+
+
+#### `week(week_number?)`
+
+Return or set the non-ISO week-of-year number.
+
+**Parameters**:
+
+- `week_number?` (`integer`): Week number to set.
+
+**Return**:
+
+- `weekOrDate` (`integer|mods.Date`): Current week-of-year number, or a shifted
+ Date when `week_number` is provided.
+
+**Example**:
+
+```lua
+local d = Date("2026-03-30")
+print(d:week()) --> 14
+print(d:week(15)) --> 2026-04-06 00:00:00
+```
+
+
+
+#### `week_year()`
+
+Return the non-ISO week-year for the current date.
+
+**Return**:
+
+- `weekYear` (`integer`): Week-year.
+
+**Example**:
+
+```lua
+print(Date("2021-01-01"):week_year()) --> 2021
+```
+
+
+
+#### `weekday(weekday_number?)`
+
+Return or set the locale-relative weekday like Day.js `weekday()`.
+
+**Parameters**:
+
+- `weekday_number?` (`integer`): Locale-relative weekday to set.
+
+**Return**:
+
+- `weekdayOrDate` (`integer|mods.Date`): Current locale-relative weekday number,
+ or a shifted Date when `weekday_number` is provided.
+
+**Example**:
+
+```lua
+local d = Date("2026-03-30")
+print(d:weekday()) --> 0
+print(d:weekday(7)) --> 2026-04-06 00:00:00
+print(d:weekday(-7)) --> 2026-03-23 00:00:00
+```
+
+The getter returns a number in the range `0..6`, relative to the current
+`mods.calendar.firstweekday`. Passing an integer returns a shifted copy in the
+same locale-relative week space, with negative and overflow values moving into
+previous or next weeks.
+
+
+
+#### `weeks_in_year()`
+
+Return the number of weeks in the current locale week-year.
+
+**Return**:
+
+- `weeksInYear` (`integer`): Number of weeks.
+
+**Example**:
+
+```lua
+print(Date("2026-03-30"):weeks_in_year()) --> 52
+```
+
+### Compare
+
+
+
+#### `max(...)`
+
+Return the latest value from the given dates.
+
+**Parameters**:
+
+- `...` (`mods.Date|mods.Date[]`): Date values to compare. Each argument may be
+ a date or a list of dates.
+
+**Return**:
+
+- `date` (`mods.Date`): Latest date value.
+
+**Example**:
+
+```lua
+local a = Date("2026-03-30")
+local b = Date("2026-03-31")
+print(Date.max(a, b)) --> 2026-03-31 00:00:00
+print(Date.max({ a, b })) --> 2026-03-31 00:00:00
+```
+
+
+
+#### `min(...)`
+
+Return the earliest value from the given dates.
+
+**Parameters**:
+
+- `...` (`mods.Date|mods.Date[]`): Date values to compare. Each argument may be
+ a date or a list of dates.
+
+**Return**:
+
+- `date` (`mods.Date`): Earliest date value.
+
+**Example**:
+
+```lua
+local a = Date("2026-03-30")
+local b = Date("2026-03-28")
+print(Date.min(a, b)) --> 2026-03-28 00:00:00
+print(Date.min({ a, b })) --> 2026-03-28 00:00:00
+```
+
+
+
+#### `minmax(...)`
+
+Return the earliest and latest values from the given dates.
+
+**Parameters**:
+
+- `...` (`mods.Date|mods.Date[]`): Date values to compare. Each argument may be
+ a date or a list of dates.
+
+**Return**:
+
+- `minDate` (`mods.Date`): Earliest date value.
+- `maxDate` (`mods.Date`): Latest date value.
+
+**Example**:
+
+```lua
+local a = Date("2026-03-30")
+local b = Date("2026-03-28")
+local c = Date("2026-03-31")
+local min_date, max_date = Date.minmax(a, b, c)
+print(min_date) --> 2026-03-28 00:00:00
+print(max_date) --> 2026-03-31 00:00:00
+```
+
+### Comparison
+
+
+
+#### `is_after(date)`
+
+Return `true` when the value is later than `other`.
+
+**Parameters**:
+
+- `date` (`mods.Date`): Date to compare against.
+
+**Return**:
+
+- `isAfter` (`boolean`): Whether the value is later than `date`.
+
+**Example**:
+
+```lua
+local a = Date("2026-03-31T12:00:00")
+local b = Date("2026-03-30T12:00:00")
+print(a:is_after(b)) --> true
+```
+
+
+
+#### `is_before(date)`
+
+Return `true` when the value is earlier than `other`.
+
+**Parameters**:
+
+- `date` (`mods.Date`): Date to compare against.
+
+**Return**:
+
+- `isBefore` (`boolean`): Whether the value is earlier than `date`.
+
+**Example**:
+
+```lua
+local a = Date("2026-03-30T12:00:00")
+local b = Date("2026-03-31T12:00:00")
+print(a:is_before(b)) --> true
+```
+
+
+
+#### `is_between(start_date, end_date, inclusive?)`
+
+Return `true` when the value lies between two bounds.
+
+Bounds may be passed in either order. By default the comparison is exclusive;
+pass `true` as the third argument to include the endpoints.
+
+**Parameters**:
+
+- `start_date` (`mods.Date`): One bound.
+- `end_date` (`mods.Date`): The other bound.
+- `inclusive?` (`boolean`): Whether to include the endpoints. Defaults to
+ `false`.
+
+**Return**:
+
+- `isBetween` (`boolean`): Whether the value lies between the two bounds.
+
+**Example**:
+
+```lua
+local d = Date("2026-03-30T12:00:00")
+local a = Date("2026-03-30T00:00:00")
+local b = Date("2026-03-31T00:00:00")
+print(d:is_between(a, b)) --> true
+print(a:is_between(a, b)) --> false
+print(a:is_between(a, b, true)) --> true
+```
+
+
+
+#### `is_same(date)`
+
+Return `true` when the value is equal to `other`.
+
+**Parameters**:
+
+- `date` (`mods.Date`): Date to compare against.
+
+**Return**:
+
+- `isSame` (`boolean`): Whether the value is equal to `date`.
+
+**Example**:
+
+```lua
+local a = Date("2026-03-30T12:00:00")
+local b = Date("2026-03-30T12:00:00")
+print(a:is_same(b)) --> true
+```
+
+
+
+#### `is_same_or_after(date)`
+
+Return `true` when the value is later than or equal to `other`.
+
+**Parameters**:
+
+- `date` (`mods.Date`): Date to compare against.
+
+**Return**:
+
+- `isSameOrAfter` (`boolean`): Whether the value is later than or equal to
+ `date`.
+
+**Example**:
+
+```lua
+local a = Date("2026-03-31T12:00:00")
+local b = Date("2026-03-30T12:00:00")
+local c = Date("2026-03-31T12:00:00")
+print(a:is_same_or_after(b)) --> true
+print(a:is_same_or_after(c)) --> true
+```
+
+
+
+#### `is_same_or_before(date)`
+
+Return `true` when the value is earlier than or equal to `other`.
+
+**Parameters**:
+
+- `date` (`mods.Date`): Date to compare against.
+
+**Return**:
+
+- `isSameOrBefore` (`boolean`): Whether the value is earlier than or equal to
+ `date`.
+
+**Example**:
+
+```lua
+local a = Date("2026-03-30T12:00:00")
+local b = Date("2026-03-30T12:00:00")
+local c = Date("2026-03-31T12:00:00")
+print(a:is_same_or_before(b)) --> true
+print(a:is_same_or_before(c)) --> true
+```
+
+
+
+#### `is_today()`
+
+Return `true` when the value falls on the current local day.
+
+**Return**:
+
+- `isToday` (`boolean`): Whether the value is on today in local time.
+
+**Example**:
+
+```lua
+print(Date():is_today()) --> true
+```
+
+
+
+#### `is_tomorrow()`
+
+Return `true` when the value falls on the next local day.
+
+**Return**:
+
+- `isTomorrow` (`boolean`): Whether the value is on tomorrow in local time.
+
+**Example**:
+
+```lua
+print(Date():add(1, "day"):is_tomorrow()) --> true
+```
+
+
+
+#### `is_yesterday()`
+
+Return `true` when the value falls on the previous local day.
+
+**Return**:
+
+- `isYesterday` (`boolean`): Whether the value is on yesterday in local time.
+
+**Example**:
+
+```lua
+print(Date():subtract(1, "day"):is_yesterday()) --> true
+```
+
+### Duration
+
+
+
+#### `is_duration(value)`
+
+Return `true` when the value is a duration created by `date.duration(...)`.
+
+**Parameters**:
+
+- `value` (`any`): Value to test.
+
+**Return**:
+
+- `isDuration` (`boolean`): Whether the value is a `mods.Duration`.
+
+**Example**:
+
+```lua
+local shift = date.duration({ day = 2 })
+print(date.is_duration(shift)) --> true
+print(date.is_duration({ day = 2 })) --> false
+```
+
+### Formatting
+
+
+
+#### `format(pattern)`
+
+Format the Date with tokens like `YYYY`, `MMM`, `dddd`, `Do`, `Q`, `hh`, `k`,
+`X`, `x`, `A`, and `SSS`.
+
+**Parameters**:
+
+- `pattern` (`string`): Format pattern using supported tokens.
+
+**Return**:
+
+- `formatted` (`string`): Formatted datetime string.
+
+**Example**:
+
+```lua
+local d = Date("2026-03-30T14:45:06.123")
+local ts = Date(1523520536123)
+print(d:format("YYYY/MM/DD HH:mm:ss.SSS")) --> 2026/03/30 14:45:06.123
+print(d:format("ddd, MMM Do YYYY h:mm A")) --> Mon, Mar 30th 2026 2:45 PM
+print(d:format("LLLL")) --> Monday, March 30, 2026 2:45 PM
+print(d:format("GGGG-[W]WW")) --> 2026-W14
+print(ts:format("X x")) --> 1523520536 1523520536123
+```
+
+> [!NOTE]
+>
+> Wrap literal text in `[...]` to escape it, for example:
+>
+> ```lua
+> d:format("GGGG-[W]WW") -- 2026-W14
+> d:format("[hours:]HH") -- hours:14
+> ```
+
+**Supported tokens**:
+
+| Token | Example | Meaning |
+| ------ | ------------------ | ---------------------------------- |
+| `YY` | `26` | 2-digit year |
+| `YYYY` | `2026` | 4-digit year |
+| `Q` | `1-4` | Quarter |
+| `Qo` | `1st..4th` | Ordinal quarter |
+| `M` | `1-12` | Month |
+| `MM` | `03-12` | Month, zero-padded |
+| `MMM` | `Jan-Dec` | Short month name |
+| `MMMM` | `January-December` | Full month name |
+| `D` | `1-31` | Day of month |
+| `DD` | `01-31` | Day of month, zero-padded |
+| `DDD` | `1-366` | Day of year |
+| `DDDD` | `001-366` | Day of year, zero-padded |
+| `d` | `0-6` | Weekday number where Sunday is `0` |
+| `e` | `0-6` | Weekday number where Sunday is `0` |
+| `E` | `1-7` | ISO weekday number |
+| `dd` | `Su-Sa` | Minimal weekday name |
+| `ddd` | `Sun-Sat` | Short weekday name |
+| `dddd` | `Sunday-Saturday` | Full weekday name |
+| `Do` | `1st..31th` | Ordinal day of month |
+| `H` | `0-23` | 24-hour |
+| `HH` | `00-23` | 24-hour, zero-padded |
+| `h` | `1-12` | 12-hour |
+| `hh` | `01-12` | 12-hour, zero-padded |
+| `k` | `1-24` | 1-24 hour |
+| `kk` | `01-24` | 1-24 hour, zero-padded |
+| `m` | `0-59` | Minute |
+| `mm` | `00-59` | Minute, zero-padded |
+| `s` | `0-59` | Second |
+| `ss` | `00-59` | Second, zero-padded |
+| `S` | `0-9` | Hundreds digit of milliseconds |
+| `SS` | `00-99` | First two digits of milliseconds |
+| `SSS` | `000-999` | Millisecond, zero-padded |
+| `w` | `1-53` | Week of year |
+| `ww` | `01-53` | Week of year, zero-padded |
+| `wo` | `1st..53rd` | Ordinal week of year |
+| `W` | `1-53` | ISO week of year |
+| `WW` | `01-53` | ISO week of year, zero-padded |
+| `GG` | `26` | 2-digit ISO week-year |
+| `GGGG` | `2026` | ISO week-year |
+| `gggg` | `2026` | Week-year |
+| `a` | `am pm` | Meridiem lowercase |
+| `A` | `AM PM` | Meridiem uppercase |
+| `x` | `1523520536123` | Unix timestamp in milliseconds |
+| `X` | `1523520536` | Unix timestamp in seconds |
+
+English preset aliases:
+
+| Alias | Expands to |
+| ------ | --------------------------- |
+| `LT` | `h:mm A` |
+| `LTS` | `h:mm:ss A` |
+| `L` | `MM/DD/YYYY` |
+| `LL` | `MMMM D, YYYY` |
+| `LLL` | `MMMM D, YYYY h:mm A` |
+| `LLLL` | `dddd, MMMM D, YYYY h:mm A` |
+| `l` | `M/D/YYYY` |
+| `ll` | `MMM D, YYYY` |
+| `lll` | `MMM D, YYYY h:mm A` |
+| `llll` | `ddd, MMM D, YYYY h:mm A` |
+
+
+
+#### `tostring()`
+
+Return the default string form `YYYY-MM-DD HH:mm:ss`.
+
+**Return**:
+
+- `s` (`string`): Default datetime string.
+
+**Example**:
+
+```lua
+print(Date("2026-03-30T14:45:06.123")) --> 2026-03-30 14:45:06.123
+```
+
+### Relative Time
+
+
+
+#### `from(date, without_suffix?)`
+
+Return relative time from another Date to this one.
+
+By default the result includes a suffix like `ago` or a prefix like `in`. Pass
+`true` to omit that suffix or prefix.
+
+**Parameters**:
+
+- `date` (`mods.Date`): Reference date.
+- `without_suffix?` (`boolean`): Whether to omit `ago` / `in`.
+
+**Return**:
+
+- `relative` (`string`): Relative time string.
+
+**Example**:
+
+```lua
+local a = date("2026-03-30T14:45:06")
+local b = date("2026-03-30T12:45:06")
+print(a:from(b)) --> in 2 hours
+print(a:from(b, true)) --> 2 hours
+```
+
+
+
+#### `from_now(without_suffix?)`
+
+Return relative time from the current local time to this Date.
+
+**Parameters**:
+
+- `without_suffix?` (`boolean`): Whether to omit `ago` / `in`.
+
+**Return**:
+
+- `relative` (`string`): Relative time string.
+
+**Example**:
+
+```lua
+local d = date():add(1, "day")
+print(d:from_now()) --> in a day
+```
+
+
+
+#### `to(date, without_suffix?)`
+
+Return relative time from this Date to another one.
+
+By default the result includes a suffix like `ago` or a prefix like `in`. Pass
+`true` to omit that suffix or prefix.
+
+**Parameters**:
+
+- `date` (`mods.Date`): Reference date.
+- `without_suffix?` (`boolean`): Whether to omit `ago` / `in`.
+
+**Return**:
+
+- `relative` (`string`): Relative time string.
+
+**Example**:
+
+```lua
+local a = date("2026-03-30T12:45:06")
+local b = date("2026-03-30T14:45:06")
+print(a:to(b)) --> in 2 hours
+print(a:to(b, true)) --> 2 hours
+```
+
+
+
+#### `to_now(without_suffix?)`
+
+Return relative time from this Date to the current local time.
+
+**Parameters**:
+
+- `without_suffix?` (`boolean`): Whether to omit `ago` / `in`.
+
+**Return**:
+
+- `relative` (`string`): Relative time string.
+
+**Example**:
+
+```lua
+local d = date():subtract(1, "day")
+print(d:to_now()) --> in a day
+```
+
+### Unix
+
+
+
+#### `unix(timestamp)`
+
+Create a Date from a Unix timestamp in whole or fractional seconds.
+
+**Parameters**:
+
+- `timestamp` (`number`): Unix timestamp in whole or fractional seconds.
+
+**Return**:
+
+- `date` (`mods.Date`): Date value for the given Unix timestamp.
+
+**Example**:
+
+```lua
+print(Date.unix(1318781876)) --> 2011-10-16 18:17:56
+print(Date.unix(1318781876.721).year) --> 2011
+```
+
+### Validation
+
+
+
+#### `is_valid(input?, pattern?)`
+
+Return `true` when the input can be parsed as a valid Date.
+
+Unlike `Date(...)`, this helper never raises for invalid input; it just returns
+`false`.
+
+**Parameters**:
+
+- `input?` (`string|number|mods.DateParts`): Value accepted by `Date(...)`.
+ `nil` returns `false`.
+- `pattern?` (`string`): Custom parse pattern used for string input.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the input is parseable as a valid Date.
+
+**Example**:
+
+```lua
+print(Date.is_valid()) --> false
+print(Date.is_valid("2026-03-30")) --> true
+print(Date.is_valid("2026-02-29")) --> false
+print(Date.is_valid("12-25-1995", "MM-DD-YYYY")) --> true
+```
+
+### Metamethods
+
+
+
+#### `__add(a, b)`
+
+Return a copy shifted by integer milliseconds.
+
+This works as either `date + ms` or `ms + date`.
+
+**Parameters**:
+
+- `a` (`integer|mods.Date`): Milliseconds to add, or another Date.
+- `b` (`integer|mods.Date`): Milliseconds to add, or another Date.
+
+**Return**:
+
+- `sum` (`mods.Date`): Sum of the two dates.
+
+**Example**:
+
+```lua
+local d = Date("2026-03-30T14:45:06")
+print((d + 250)) --> 2026-03-30 14:45:06.250
+print((250 + d)) --> 2026-03-30 14:45:06.250
+```
+
+
+
+#### `__eq(date)`
+
+Return `true` when both Date values have identical components.
+
+**Parameters**:
+
+- `date` (`mods.Date`): Date to compare against.
+
+**Return**:
+
+- `isEqual` (`boolean`): `true` if both dates are equal, `false` otherwise.
+
+**Example**:
+
+```lua
+print(Date("2026-03-30") == Date("2026-03-30")) --> true
+```
+
+
+
+#### `__le(date)`
+
+Return `true` when the left Date is earlier than or equal to the right.
+
+**Parameters**:
+
+- `date` (`mods.Date`): Date to compare against.
+
+**Return**:
+
+- `isEarlierOrEqual` (`boolean`): `true` if the left date is earlier or equal,
+ `false` otherwise.
+
+**Example**:
+
+```lua
+print(Date("2026-03-30") <= Date("2026-03-30")) --> true
+```
+
+
+
+#### `__lt(date)`
+
+Return `true` when the left Date is earlier than the right.
+
+**Parameters**:
+
+- `date` (`mods.Date`): Date to compare against.
+
+**Return**:
+
+- `isEarlier` (`boolean`): `true` if the left date is earlier, `false`
+ otherwise.
+
+**Example**:
+
+```lua
+print(Date("2026-03-30") < Date("2026-03-31")) --> true
+```
+
+
+
+#### `__sub(a, b)`
+
+Return either a shifted copy or a millisecond delta.
+
+When subtracting an integer, it shifts by that many milliseconds. When
+subtracting another Date, it returns the signed millisecond difference.
+
+**Parameters**:
+
+- `a` (`integer|mods.Date`): Milliseconds to subtract, or another Date.
+- `b` (`integer|mods.Date`): Milliseconds to subtract, or another Date.
+
+**Return**:
+
+- `delta` (`mods.Date|integer`): Difference between dates.
+
+**Example**:
+
+```lua
+local a = Date("2026-03-30T14:45:06.250")
+local b = Date("2026-03-30T14:45:06")
+print((a - 250)) --> 2026-03-30 14:45:06
+print(a - b) --> 250
+```
+
+
+
+#### `__tostring()`
+
+Return the same result as `tostring()` when coerced to a string.
+
+**Return**:
+
+- `string` (`string`): representation of the date.
+
+**Example**:
+
+```lua
+print(Date("2026-03-30T14:45:06")) --> 2026-03-30 14:45:06
+```
+
+## Fields
+
+| Field | Description |
+| ----------------- | ------------------------------------------------------------ |
+| [`day`](#day) | Day-of-month component. |
+| [`hour`](#hour) | Hour component. |
+| [`min`](#min) | Minute component. |
+| [`month`](#month) | Month component. |
+| [`ms`](#ms) | Millisecond component. |
+| [`sec`](#sec) | Second component. |
+| [`wday`](#wday) | ISO weekday component where Monday is `1` and Sunday is `7`. |
+| [`yday`](#yday) | Day-of-year component starting at `1`. |
+| [`year`](#year) | Year component. |
+
+
+
+### `day` (`modsCalendarMonthday`)
+
+Day-of-month component.
+
+```lua
+print(Date("2026-03-30").day) --> 30
+```
+
+
+
+### `hour` (`integer`)
+
+Hour component.
+
+```lua
+print(Date("2026-03-30T14:45:06").hour) --> 14
+```
+
+
+
+### `min` (`integer`)
+
+Minute component.
+
+```lua
+print(Date("2026-03-30T14:45:06").min) --> 45
+```
+
+
+
+### `month` (`modsCalendarMonth`)
+
+Month component.
+
+```lua
+print(Date("2026-03-30").month) --> 3
+```
+
+
+
+### `ms` (`integer`)
+
+Millisecond component.
+
+```lua
+print(Date("2026-03-30T14:45:06.123").ms) --> 123
+```
+
+
+
+### `sec` (`integer`)
+
+Second component.
+
+```lua
+print(Date("2026-03-30T14:45:06").sec) --> 6
+```
+
+
+
+### `wday` (`modsCalendarWeekday`)
+
+ISO weekday component where Monday is `1` and Sunday is `7`.
+
+```lua
+print(Date("2026-03-30").wday) --> 1
+```
+
+
+
+### `yday` (`integer`)
+
+Day-of-year component starting at `1`.
+
+```lua
+print(Date("2026-03-30").yday) --> 89
+```
+
+
+
+### `year` (`integer`)
+
+Year component.
+
+```lua
+print(Date("2026-03-30").year) --> 2026
+```
diff --git a/docs/api/duration.md b/docs/api/duration.md
new file mode 100644
index 0000000..3386860
--- /dev/null
+++ b/docs/api/duration.md
@@ -0,0 +1,446 @@
+---
+description:
+ "Reusable immutable duration values for date arithmetic and formatting."
+---
+
+# `duration`
+
+Reusable immutable duration values for date arithmetic and formatting.
+
+## Usage
+
+```lua
+local Duration = require "mods.duration"
+
+local shift = Duration({ day = 2, hour = 3 })
+print(shift:format("D [days] HH:mm")) --> 2 days 03:00
+```
+
+## Functions
+
+| Function | Description |
+| --------------------------------------- | -------------------------------------------------------------------------------------------------------- |
+| [`is_duration(value)`](#fn-is-duration) | Return `true` when the value is a duration created by `mods.duration(...)` or `mods.date.duration(...)`. |
+| [`new(input, unit?)`](#fn-new) | Create a duration from a numeric amount and unit. |
+| [`new(input?)`](#fn-new) | Create a duration from numeric parts, an ISO 8601 string, or another duration. |
+
+**Duration**:
+
+| Function | Description |
+| ------------------------------------------------------------- | ------------------------------------------------------------------------- |
+| [`add(value, unit?)`](#fn-add) | Return a new duration with another duration or unit amount added. |
+| [`as(unit)`](#fn-as) | Return the duration expressed in the requested unit. |
+| [`clone()`](#fn-clone) | Return a shallow copy of the duration value. |
+| [`compare(other)`](#fn-compare) | Compare this duration to another duration-like value. |
+| [`equals(other)`](#fn-equals) | Return `true` when both duration values have identical components. |
+| [`format(pattern)`](#fn-format) | Format the duration using duration tokens like `Y`, `MM`, `DD`, and `HH`. |
+| [`humanize(with_suffix_or_options?, options?)`](#fn-humanize) | Return a human-readable relative-style phrase for the duration. |
+| [`normalize()`](#fn-normalize) | Return a compacted duration using the module's canonical carry rules. |
+| [`subtract(value, unit?)`](#fn-subtract) | Return a new duration with another duration or unit amount subtracted. |
+| [`to_iso()`](#fn-to-iso) | Return an ISO 8601 duration string. |
+| [`tostring()`](#fn-tostring) | Return a debug-friendly string representation of the duration. |
+
+**Metamethods**:
+
+| Function | Description |
+| ---------------------------------- | ------------------------------------------------------------------------------ |
+| [`__call(input, unit?)`](#fn-call) | Create a duration from a numeric amount and unit. |
+| [`__call(input?)`](#fn-call) | Create a duration from numeric parts, an ISO 8601 string, or another duration. |
+| [`__eq(duration)`](#fn-eq) | Return `true` when both duration values have identical components. |
+| [`__tostring()`](#fn-tostring) | Return the same result as `tostring()` when coerced to a string. |
+
+
+
+### `is_duration(value)`
+
+Return `true` when the value is a duration created by `mods.duration(...)` or
+`mods.date.duration(...)`.
+
+**Parameters**:
+
+- `value` (`any`)
+
+**Return**:
+
+- `isDuration` (`boolean`)
+
+**Example**:
+
+```lua
+local Duration = require "mods.duration"
+print(Duration.is_duration(Duration({ day = 2 }))) --> true
+print(Duration.is_duration({ day = 2 })) --> false
+```
+
+
+
+### `new(input, unit?)`
+
+Create a duration from a numeric amount and unit.
+
+**Parameters**:
+
+- `input` (`number`): Numeric amount to convert into a duration.
+- `unit?` (`mods.DateUnit`): Unit used with the numeric amount. Defaults to
+ `"ms"`.
+
+**Return**:
+
+- `duration` (`mods.Duration`)
+
+**Example**:
+
+```lua
+local d = Duration(90, "minute")
+```
+
+
+
+### `new(input?)`
+
+Create a duration from numeric parts, an ISO 8601 string, or another duration.
+
+**Parameters**:
+
+- `input?` (`string|mods.DurationParts|mods.Duration`): Duration parts, an ISO
+ 8601 string, or another duration.
+
+**Return**:
+
+- `duration` (`mods.Duration`)
+
+**Example**:
+
+```lua
+local a = Duration({ day = 2, hour = 3 })
+local b = Duration("PT1H30M")
+```
+
+### Duration
+
+
+
+#### `add(value, unit?)`
+
+Return a new duration with another duration or unit amount added.
+
+**Parameters**:
+
+- `value` (`number|mods.DurationParts|mods.Duration`): Signed amount to add, or
+ another duration value.
+- `unit?` (`mods.DateUnit`): Unit used when `value` is a number.
+
+**Return**:
+
+- `duration` (`mods.Duration`)
+
+**Example**:
+
+```lua
+local Duration = require "mods.duration"
+local a = Duration({ day = 2 })
+local b = a:add(3, "hour")
+print(b:format("D [days] HH:mm:ss")) --> 2 days 03:00:00
+```
+
+
+
+#### `as(unit)`
+
+Return the duration expressed in the requested unit.
+
+**Parameters**:
+
+- `unit` (`mods.DateUnit`)
+
+**Return**:
+
+- `amount` (`number`)
+
+**Example**:
+
+```lua
+local Duration = require "mods.duration"
+local d = Duration({ day = 1, hour = 12 })
+print(d:as("hour")) --> 36
+```
+
+
+
+#### `clone()`
+
+Return a shallow copy of the duration value.
+
+**Return**:
+
+- `duration` (`mods.Duration`)
+
+**Example**:
+
+```lua
+local Duration = require "mods.duration"
+local d = Duration({ month = 1, day = 2 })
+local copy = d:clone()
+print(copy == d, rawequal(copy, d)) --> true false
+```
+
+
+
+#### `compare(other)`
+
+Compare this duration to another duration-like value.
+
+Returns `-1` when smaller, `0` when equal, and `1` when larger.
+
+**Parameters**:
+
+- `other` (`number|string|mods.DurationParts|mods.Duration`)
+
+**Return**:
+
+- `ordering` (`integer`)
+
+**Example**:
+
+```lua
+local Duration = require "mods.duration"
+print(Duration({ day = 1 }):compare({ hour = 24 })) --> 0
+```
+
+
+
+#### `equals(other)`
+
+Return `true` when both duration values have identical components.
+
+**Parameters**:
+
+- `other` (`any`): Value to compare against.
+
+**Return**:
+
+- `isEqual` (`boolean`)
+
+**Example**:
+
+```lua
+local Duration = require "mods.duration"
+local a = Duration({ day = 2 })
+local b = Duration({ day = 2 })
+print(a:equals(b)) --> true
+```
+
+
+
+#### `format(pattern)`
+
+Format the duration using duration tokens like `Y`, `MM`, `DD`, and `HH`.
+
+**Parameters**:
+
+- `pattern` (`string`): Format pattern using supported duration tokens.
+
+**Return**:
+
+- `formatted` (`string`)
+
+**Example**:
+
+```lua
+local Duration = require "mods.duration"
+local d = Duration({ day = 2, hour = 3, minute = 4 })
+print(d:format("D [days] HH:mm")) --> 2 days 03:04
+```
+
+
+
+#### `humanize(with_suffix_or_options?, options?)`
+
+Return a human-readable relative-style phrase for the duration.
+
+By default this returns the bare phrase without `ago` / `in`. Pass `true` to
+include relative wording. You can also pass an options table for abbreviated
+output or explicit unit clamping.
+
+**Parameters**:
+
+- `with_suffix_or_options?` (`boolean|mods.DurationHumanizeOptions`): Whether to
+ include `ago` / `in` style wording, or an options table.
+- `options?` (`mods.DurationHumanizeOptions`): Additional options when the first
+ argument is a boolean.
+
+**Return**:
+
+- `humanized` (`string`)
+
+**Example**:
+
+```lua
+local Duration = require "mods.duration"
+local d = Duration({ day = 3 })
+print(d:humanize()) --> 3 days
+print(d:humanize(true)) --> in 3 days
+print(d:humanize({ short = true })) --> 3d
+```
+
+
+
+#### `normalize()`
+
+Return a compacted duration using the module's canonical carry rules.
+
+**Return**:
+
+- `duration` (`mods.Duration`)
+
+**Example**:
+
+```lua
+local Duration = require "mods.duration"
+print(Duration({ minute = 90 }):normalize()) --> duration(hours=1, minutes=30)
+```
+
+
+
+#### `subtract(value, unit?)`
+
+Return a new duration with another duration or unit amount subtracted.
+
+**Parameters**:
+
+- `value` (`number|mods.DurationParts|mods.Duration`): Signed amount to
+ subtract, or another duration value.
+- `unit?` (`mods.DateUnit`): Unit used when `value` is a number.
+
+**Return**:
+
+- `duration` (`mods.Duration`)
+
+**Example**:
+
+```lua
+local Duration = require "mods.duration"
+local a = Duration({ day = 2, hour = 3 })
+local b = a:subtract(3, "hour")
+print(b:format("D [days] HH:mm:ss")) --> 2 days 00:00:00
+```
+
+
+
+#### `to_iso()`
+
+Return an ISO 8601 duration string.
+
+**Return**:
+
+- `iso` (`string`)
+
+**Example**:
+
+```lua
+local Duration = require "mods.duration"
+print(Duration({ hour = 1, minute = 30 }):to_iso()) --> PT1H30M
+```
+
+
+
+#### `tostring()`
+
+Return a debug-friendly string representation of the duration.
+
+**Return**:
+
+- `s` (`string`)
+
+**Example**:
+
+```lua
+local Duration = require "mods.duration"
+print(Duration({ day = 2, hour = 3 })) --> duration(days=2, hours=3)
+```
+
+### Metamethods
+
+
+
+#### `__call(input, unit?)`
+
+Create a duration from a numeric amount and unit.
+
+**Parameters**:
+
+- `input` (`number`): Numeric amount to convert into a duration.
+- `unit?` (`mods.DateUnit`): Unit used with the numeric amount. Defaults to
+ `"ms"`.
+
+**Return**:
+
+- `duration` (`mods.Duration`)
+
+**Example**:
+
+```lua
+local Duration = require "mods.duration"
+local d = Duration(90, "minute")
+```
+
+
+
+#### `__call(input?)`
+
+Create a duration from numeric parts, an ISO 8601 string, or another duration.
+
+**Parameters**:
+
+- `input?` (`string|mods.DurationParts|mods.Duration`): Duration parts, an ISO
+ 8601 string, or another duration.
+
+**Return**:
+
+- `duration` (`mods.Duration`)
+
+**Example**:
+
+```lua
+local Duration = require "mods.duration"
+local a = Duration({ day = 2, hour = 3 })
+local b = Duration("PT1H30M")
+```
+
+
+
+#### `__eq(duration)`
+
+Return `true` when both duration values have identical components.
+
+**Parameters**:
+
+- `duration` (`mods.Duration`): Duration to compare against.
+
+**Return**:
+
+- `isEqual` (`boolean`)
+
+**Example**:
+
+```lua
+local Duration = require "mods.duration"
+print(Duration({ day = 2 }) == Duration({ day = 2 })) --> true
+```
+
+
+
+#### `__tostring()`
+
+Return the same result as `tostring()` when coerced to a string.
+
+**Return**:
+
+- `s` (`string`)
+
+**Example**:
+
+```lua
+local Duration = require "mods.duration"
+print(Duration({ day = 2 })) --> duration(days=2)
+```
diff --git a/docs/api/fs.md b/docs/api/fs.md
new file mode 100644
index 0000000..a646a9b
--- /dev/null
+++ b/docs/api/fs.md
@@ -0,0 +1,653 @@
+---
+description: "Filesystem I/O, metadata, and filesystem path operations."
+---
+
+# `fs`
+
+Filesystem I/O, metadata, and filesystem path operations.
+
+> [!NOTE]
+>
+> This module requires
+> [LuaFileSystem (`lfs`)](https://github.com/lunarmodules/luafilesystem).
+
+## Usage
+
+```lua
+fs = require "mods.fs"
+
+fs.mkdir("tmp/cache/app", true)
+fs.write_text("tmp/cache/app/data.txt", "hello")
+print(fs.read_text("tmp/cache/app/data.txt")) --> "hello"
+```
+
+## Functions
+
+**Existence Checks**:
+
+| Function | Description |
+| ------------------------------ | ------------------------------------------------------------ |
+| [`exists(path)`](#fn-exists) | Return `true` when a path exists. |
+| [`lexists(path)`](#fn-lexists) | Return `true` when a path exists without following symlinks. |
+
+**Filesystem Mutations**:
+
+| Function | Description |
+| -------------------------------------------- | ----------------------------------------------------------------------------- |
+| [`cd(path)`](#fn-cd) | Change the current working directory. |
+| [`cp(src, dst)`](#fn-cp) | Copy a file or directory tree. |
+| [`cwd()`](#fn-cwd) | Return the current working directory. |
+| [`link(path, linkpath)`](#fn-link) | Create a hard link. |
+| [`mkdir(path, parents?)`](#fn-mkdir) | Create a directory. |
+| [`rename(oldname, newname)`](#fn-rename) | Rename or move a filesystem entry. |
+| [`rm(path, recursive?)`](#fn-rm) | Remove a filesystem entry, or a directory tree when `recursive` is `true`. |
+| [`symlink(path, linkpath)`](#fn-symlink) | Create a symbolic link. |
+| [`touch(path)`](#fn-touch) | Create file if missing without truncating, or update timestamps if it exists. |
+| [`write_bytes(path, data)`](#fn-write-bytes) | Write full file in binary mode. |
+| [`write_text(path, data)`](#fn-write-text) | Write full file in text mode. |
+
+**Metadata**:
+
+| Function | Description |
+| -------------------------------- | ---------------------------------------------------------------------------------- |
+| [`getatime(path)`](#fn-getatime) | Return last access time. |
+| [`getctime(path)`](#fn-getctime) | Return metadata change time. |
+| [`getmtime(path)`](#fn-getmtime) | Return last modification time. |
+| [`getsize(path)`](#fn-getsize) | Return file size in bytes. |
+| [`lstat(path)`](#fn-lstat) | Return symlink-aware file attributes. |
+| [`samefile(a, b)`](#fn-samefile) | Return whether two paths refer to the same file, or `nil` and an error on failure. |
+| [`stat(path)`](#fn-stat) | Return file attributes. |
+
+**Reading**:
+
+| Function | Description |
+| ------------------------------------- | -------------------------------------- |
+| [`dir(path, opts?)`](#fn-dir) | Iterator over items in `path`. |
+| [`listdir(path, opts?)`](#fn-listdir) | Return direct children of a directory. |
+| [`read_bytes(path)`](#fn-read-bytes) | Read full file in binary mode. |
+| [`read_text(path)`](#fn-read-text) | Read full file in text mode. |
+
+### Existence Checks
+
+
+
+#### `exists(path)`
+
+Return `true` when a path exists.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `exists` (`boolean`): True when the path exists.
+
+**Example**:
+
+```lua
+fs.exists("README.md") --> true
+```
+
+> [!NOTE]
+>
+> Broken symlinks return `false`.
+
+
+
+#### `lexists(path)`
+
+Return `true` when a path exists without following symlinks.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `exists` (`boolean`): True when the path or symlink entry exists.
+
+**Example**:
+
+```lua
+fs.lexists("README.md") --> true
+```
+
+> [!NOTE]
+>
+> Broken symlinks return `true`.
+
+### Filesystem Mutations
+
+
+
+#### `cd(path)`
+
+Change the current working directory.
+
+**Parameters**:
+
+- `path` (`string`): Directory path to switch into.
+
+**Return**:
+
+- `changed` (`true?`): `true` when the directory change succeeds, or `nil` on
+ failure.
+- `errmsg` (`string?`): Error message when the change fails.
+
+**Example**:
+
+```lua
+fs.cd("src")
+```
+
+
+
+#### `cp(src, dst)`
+
+Copy a file or directory tree.
+
+**Parameters**:
+
+- `src` (`string`): Source path.
+- `dst` (`string`): Destination path.
+
+**Return**:
+
+- `copied` (`true?`): `true` when copying succeeds, or `nil` on failure.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.cp("a.txt", "b.txt")
+fs.cp("src", "backup/src")
+```
+
+
+
+#### `cwd()`
+
+Return the current working directory.
+
+**Return**:
+
+- `cwd` (`string?`): Current working directory, or `nil` on failure.
+- `errmsg` (`string?`): Error message when the lookup fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.cwd()
+```
+
+
+
+#### `link(path, linkpath)`
+
+Create a hard link.
+
+**Parameters**:
+
+- `path` (`string`): Existing path to link to.
+- `linkpath` (`string`): New link path to create.
+
+**Return**:
+
+- `linked` (`true?`): `true` when link creation succeeds, or `nil` on failure.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.link("target.txt", "hardlink.txt")
+```
+
+
+
+#### `mkdir(path, parents?)`
+
+Create a directory.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `parents?` (`boolean`): Create missing parent directories when `true`.
+
+**Return**:
+
+- `created` (`true?`): `true` when directory creation succeeds, or `nil` on
+ failure.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.mkdir("tmp/a/b", true)
+```
+
+
+
+#### `rename(oldname, newname)`
+
+Rename or move a filesystem entry.
+
+**Parameters**:
+
+- `oldname` (`string`): Existing path.
+- `newname` (`string`): Replacement path.
+
+**Return**:
+
+- `renamed` (`true?`): `true` when the rename succeeds, or `nil` on failure.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.rename("old.txt", "new.txt")
+```
+
+> [!NOTE]
+>
+> This is an alias for `os.rename`.
+
+
+
+#### `rm(path, recursive?)`
+
+Remove a filesystem entry, or a directory tree when `recursive` is `true`.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `recursive?` (`boolean`): Remove a directory tree recursively when `true`.
+
+**Return**:
+
+- `removed` (`true?`): `true` when removal succeeds, or `nil` on failure.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.rm("tmp.txt") --> true, nil
+fs.rm("tmp/cache", true) --> true, nil
+```
+
+
+
+#### `symlink(path, linkpath)`
+
+Create a symbolic link.
+
+**Parameters**:
+
+- `path` (`string`): Path to reference from the new symlink.
+- `linkpath` (`string`): New symlink path to create.
+
+**Return**:
+
+- `linked` (`true?`): `true` when link creation succeeds, or `nil` on failure.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.symlink("target.txt", "symlink.txt")
+```
+
+
+
+#### `touch(path)`
+
+Create file if missing without truncating, or update timestamps if it exists.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `touched` (`true?`): `true` when the file exists after touch, or `nil` on
+ failure.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.touch("tmp.txt") --> true, nil
+```
+
+
+
+#### `write_bytes(path, data)`
+
+Write full file in binary mode.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `data` (`string`): Input data.
+
+**Return**:
+
+- `written` (`true?`): `true` when writing succeeds, or `nil` on failure.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.write_bytes("tmp.bin", "abc") --> true, nil
+```
+
+
+
+#### `write_text(path, data)`
+
+Write full file in text mode.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `data` (`string`): Input data.
+
+**Return**:
+
+- `written` (`true?`): `true` when writing succeeds, or `nil` on failure.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.write_text("tmp.txt", "abc") --> true, nil
+```
+
+### Metadata
+
+
+
+#### `getatime(path)`
+
+Return last access time.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `timestamp` (`number?`): Access time (seconds since epoch).
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.getatime("README.md") --> 1712345678
+```
+
+
+
+#### `getctime(path)`
+
+Return metadata change time.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `timestamp` (`number?`): Change time (seconds since epoch).
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.getctime("README.md") --> 1712345678
+```
+
+
+
+#### `getmtime(path)`
+
+Return last modification time.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `timestamp` (`number?`): Modification time (seconds since epoch).
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.getmtime("README.md") --> 1712345678
+```
+
+
+
+#### `getsize(path)`
+
+Return file size in bytes.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `size` (`integer?`): File size in bytes.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.getsize("README.md") --> 1234
+```
+
+
+
+#### `lstat(path)`
+
+Return symlink-aware file attributes.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `attrs` (`LuaFileSystem.Attributes?`): Symlink-aware attributes, or `nil` on
+ failure.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.lstat("README.md")
+```
+
+
+
+#### `samefile(a, b)`
+
+Return whether two paths refer to the same file, or `nil` and an error on
+failure.
+
+**Parameters**:
+
+- `a` (`string`): Input path.
+- `b` (`string`): Input path.
+
+**Return**:
+
+- `isSameFile` (`boolean?`): True when both paths refer to the same file.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.samefile("README.md", "README.md") --> true
+```
+
+
+
+#### `stat(path)`
+
+Return file attributes.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `attrs`
+ (`string|integer|LuaFileSystem.AttributeMode|LuaFileSystem.Attributes?`): File
+ attributes, or `nil` on failure.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.stat("README.md")
+```
+
+### Reading
+
+
+
+#### `dir(path, opts?)`
+
+Iterator over items in `path`.
+
+**Options**:
+
+- `recursive`: recurse into subdirectories; defaults to `false`.
+- `hidden`: include hidden entries; defaults to `true`.
+- `follow`: recurse into symlinked directories; defaults to `false`.
+- `type`: filter by entry type, such as `"file"` or `"directory"`; defaults to
+ `nil`.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `opts?`
+ (`{hidden?:boolean, recursive?:boolean, follow?:boolean, type?:modsFsEntryType}`):
+ Optional traversal options.
+
+**Return**:
+
+- `iterator`
+ (`(fun(state:table, prev?:string):basename:string?, type:modsFsEntryType?)?`):
+ Iterator, or `nil` on failure.
+- `state` (`table|string`): Iterator state on success, or error message on
+ failure.
+
+**Example**:
+
+```lua
+for name, type in fs.dir(path.cwd(), { recursive = true }) do
+ print(name, type)
+end
+```
+
+
+
+#### `listdir(path, opts?)`
+
+Return direct children of a directory.
+
+**Options**:
+
+- `recursive`: recurse into subdirectories; defaults to `false`.
+- `hidden`: include hidden entries; defaults to `true`.
+- `follow`: recurse into symlinked directories; defaults to `false`.
+- `type`: filter by entry type, such as `"file"` or `"directory"`; defaults to
+ `nil`.
+- `names`: return basenames; defaults to `false`.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `opts?`
+ (`{hidden?:boolean, recursive?:boolean, follow?:boolean, type?:modsFsEntryType, names?:boolean}`):
+ Optional traversal options.
+
+**Return**:
+
+- `paths` (`mods.List?`): Direct child paths, or basenames when
+ `opts.names` is `true`.
+- `err` (`string?`): Error message when traversal setup fails.
+
+**Example**:
+
+```lua
+fs.listdir("src")
+fs.listdir("src", { names = true })
+```
+
+
+
+#### `read_bytes(path)`
+
+Read full file in binary mode.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `body` (`string?`): File contents read in binary mode, or `nil` on failure.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.read_bytes("README.md")
+```
+
+
+
+#### `read_text(path)`
+
+Read full file in text mode.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `body` (`string?`): File contents read in text mode, or `nil` on failure.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.read_text("README.md")
+```
diff --git a/docs/api/glob.md b/docs/api/glob.md
new file mode 100644
index 0000000..045a163
--- /dev/null
+++ b/docs/api/glob.md
@@ -0,0 +1,267 @@
+---
+description: "Glob-style matching and filesystem expansion helpers."
+---
+
+# `glob`
+
+Glob-style matching and filesystem expansion helpers.
+
+## Usage
+
+```lua
+glob = require "mods.glob"
+
+print(glob.match("src/mods/fs.lua", "**/*.lua")) --> true
+print(glob.match("DATA.TXT", "*.txt", true)) --> true
+print(glob.filter({ "a.lua", "b.txt" }, "*.lua")[1]) --> "a.lua"
+print(glob.glob("src", "*.lua")[1])
+```
+
+## Supported wildcards
+
+- `*`: match zero or more characters within one path segment.
+
+ ```lua
+ match("main.lua", "*.lua")
+ ```
+
+- `?`: match exactly one character within one path segment.
+
+ ```lua
+ match("a1.lua", "a?.lua")
+ ```
+
+- `[]`: match one character from a bracket class like `[a-z]`.
+
+ ```lua
+ match("file7.lua", "file[0-9].lua")
+ ```
+
+- `[!]`: negate a bracket class, like `[!0-9]`.
+
+ ```lua
+ match("filex.lua", "file[!0-9].lua")
+ ```
+
+- `{a,b}`: match one of several brace alternatives.
+
+ ```lua
+ match("init.lua", "init.{lua,luac}")
+ ```
+
+- `**`: match across path segments recursively.
+
+ ```lua
+ match("src/mods/fs.lua", "**/*.lua")
+ ```
+
+## Functions
+
+**Glob Operations**:
+
+| Function | Description |
+| --------------------------------------------------- | ----------------------------------------------------------- |
+| [`escape(s)`](#fn-escape) | Escape glob metacharacters in a literal string. |
+| [`filter(names, pattern, ignorecase?)`](#fn-filter) | Return the values from `names` that match the glob pattern. |
+| [`glob(path, pattern?, opts?)`](#fn-glob) | Return glob matches under `path`. |
+| [`has_magic(s)`](#fn-has-magic) | Return whether a pattern contains glob metacharacters. |
+| [`iglob(path, pattern?, opts?)`](#fn-iglob) | Iterator over glob matches under `path`. |
+| [`match(path, pattern, ignorecase?)`](#fn-match) | Match a path against a glob pattern. |
+| [`translate(pattern)`](#fn-translate) | Translate one glob segment into an equivalent Lua pattern. |
+
+### Glob Operations
+
+
+
+#### `escape(s)`
+
+Escape glob metacharacters in a literal string.
+
+**Parameters**:
+
+- `s` (`string`): Input literal string.
+
+**Return**:
+
+- `pattern` (`string`): Escaped glob pattern.
+
+**Example**:
+
+```lua
+glob.escape("a*b") --> "a\\*b"
+```
+
+
+
+#### `filter(names, pattern, ignorecase?)`
+
+Return the values from `names` that match the glob pattern.
+
+**Parameters**:
+
+- `names` (`string[]`): Input names.
+- `pattern` (`string`): Input glob pattern.
+- `ignorecase?` (`boolean`): Override platform-default case matching.
+
+**Return**:
+
+- `matches` (`mods.List`): Matching values from `names`.
+
+**Example**:
+
+```lua
+glob.filter({ "a.lua", "b.txt", "c.lua" }, "*.lua") --> { "a.lua", "c.lua" }
+```
+
+
+
+#### `glob(path, pattern?, opts?)`
+
+Return glob matches under `path`.
+
+**Options**:
+
+- `hidden`: include hidden paths; defaults to `true`.
+- `recursive`: recurse into subdirectories; defaults to `false`.
+- `follow`: recurse into symlinked directories; defaults to `false`.
+- `ignorecase`: use case-insensitive matching; defaults to platform semantics.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `pattern?` (`string`): Optional pattern to match.
+- `opts?`
+ (`{hidden?:boolean, recursive?:boolean, follow?:boolean, ignorecase?:boolean}`):
+ Optional glob options.
+
+**Return**:
+
+- `paths` (`mods.List`): Matching paths under `path`.
+
+**Example**:
+
+```lua
+glob.glob("src", "*.lua")
+glob.glob("src", "*.lua", { recursive = true })
+```
+
+
+
+#### `has_magic(s)`
+
+Return whether a pattern contains glob metacharacters.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `has_magic` (`boolean`): True when the string contains glob syntax.
+
+**Example**:
+
+```lua
+glob.has_magic("foo.txt") --> false
+glob.has_magic("*.txt") --> true
+```
+
+
+
+#### `iglob(path, pattern?, opts?)`
+
+Iterator over glob matches under `path`.
+
+**Options**:
+
+- `hidden`: include hidden paths; defaults to `true`.
+- `recursive`: recurse into subdirectories; defaults to `false`.
+- `follow`: recurse into symlinked directories; defaults to `false`.
+- `ignorecase`: use case-insensitive matching; defaults to platform semantics.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `pattern?` (`string`): Optional pattern to match.
+- `opts?`
+ (`{hidden?:boolean, recursive?:boolean, follow?:boolean, ignorecase?:boolean}`):
+ Optional glob options.
+
+**Return**:
+
+- `iterator` (`(fun(state:table, prev?:string): (path:string?))`): Iterator
+ function.
+- `state` (`table`): Iterator state table.
+- `initial` (`nil`): Initial iterator value.
+
+**Example**:
+
+```lua
+for path in glob.iglob("src", "*.lua") do
+ print(path)
+end
+```
+
+
+
+#### `match(path, pattern, ignorecase?)`
+
+Match a path against a glob pattern.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `pattern` (`string`): Input glob pattern.
+- `ignorecase?` (`boolean`): Override platform-default case matching.
+
+**Return**:
+
+- `matches` (`boolean`): True when the path matches the pattern.
+
+**Example**:
+
+```lua
+glob.match("src/mods/fs.lua", "**/*.lua") --> true
+```
+
+
+
+#### `translate(pattern)`
+
+Translate one glob segment into an equivalent Lua pattern.
+
+**Parameters**:
+
+- `pattern` (`string`): Input glob segment.
+
+**Return**:
+
+- `lua_pattern` (`string`): Lua pattern string.
+
+**Example**:
+
+```lua
+local s = "init.lua"
+local pattern = "*.lua"
+local matches = glob.match(s, pattern)
+local translated_matches = s:match(glob.translate(pattern)) ~= nil
+print(matches == translated_matches) --> true
+```
+
+> [!NOTE]
+>
+> - `*` and `?` stay within a single path segment.
+>
+> ```lua
+> local pattern = "*.txt"
+> print(glob.translate(pattern)) --> "^[^/]*%.txt$"
+> print(glob.match("foo/bar.txt", pattern)) --> false
+> ```
+>
+> - `**` and `{a,b}` need higher-level matching logic.
+>
+> ```lua
+> pattern = "src/{x,y}.lua"
+> print(("src/x.lua"):match(glob.translate(pattern))) --> nil
+> print(glob.match("src/x.lua", pattern)) --> true
+> ```
diff --git a/docs/api/is.md b/docs/api/is.md
new file mode 100644
index 0000000..0505cbd
--- /dev/null
+++ b/docs/api/is.md
@@ -0,0 +1,558 @@
+---
+description: "Type predicates for Lua values and filesystem path types."
+---
+
+# `is`
+
+Type predicates for Lua values and filesystem path types.
+
+## Usage
+
+```lua
+is = require "mods.is"
+
+ok = is.number(3.14) --> true
+ok = is("hello", "string") --> true
+ok = is.table({}) --> true
+```
+
+> [!NOTE]
+>
+> Function names are case-insensitive.
+>
+> ```lua
+> is.table({}) --> true
+> is.Table({}) --> true
+> is.tAbLe({}) --> true
+> ```
+
+
+
+> [!IMPORTANT]
+>
+> Path checks require **LuaFileSystem**
+> ([`lfs`](https://github.com/lunarmodules/luafilesystem)) and raise an error if
+> it is not installed.
+
+
+
+## `is()`
+
+`is` is also callable as `is(value, type)` to check if a value is of a given
+type.
+
+```lua
+is("hello", "string") --> true
+is("hello", "String") --> true
+is("hello", "STRING") --> true
+```
+
+## Functions
+
+**Path Checks**:
+
+| Function | Description |
+| ------------------------- | ------------------------------------------------------------ |
+| [`block(v)`](#fn-block) | Returns `true` when `v` is a block device path. |
+| [`char(v)`](#fn-char) | Returns `true` when `v` is a character device path. |
+| [`device(v)`](#fn-device) | Returns `true` when `v` is a block or character device path. |
+| [`dir(v)`](#fn-dir) | Returns `true` when `v` is a directory path. |
+| [`fifo(v)`](#fn-fifo) | Returns `true` when `v` is a FIFO path. |
+| [`file(v)`](#fn-file) | Returns `true` when `v` is a file path. |
+| [`link(v)`](#fn-link) | Returns `true` when `v` is a symlink path. |
+| [`path(v)`](#fn-path) | Returns `true` when `v` is a valid filesystem path. |
+| [`socket(v)`](#fn-socket) | Returns `true` when `v` is a socket path. |
+
+**Type Checks**:
+
+| Function | Description |
+| ----------------------------- | -------------------------------------- |
+| [`boolean(v)`](#fn-boolean) | Returns `true` when `v` is a boolean. |
+| [`function(v)`](#fn-function) | Returns `true` when `v` is a function. |
+| [`nil(v)`](#fn-nil) | Returns `true` when `v` is `nil`. |
+| [`number(v)`](#fn-number) | Returns `true` when `v` is a number. |
+| [`string(v)`](#fn-string) | Returns `true` when `v` is a string. |
+| [`table(v)`](#fn-table) | Returns `true` when `v` is a table. |
+| [`thread(v)`](#fn-thread) | Returns `true` when `v` is a thread. |
+| [`userdata(v)`](#fn-userdata) | Returns `true` when `v` is userdata. |
+
+**Value Checks**:
+
+| Function | Description |
+| ----------------------------- | ------------------------------------------- |
+| [`callable(v)`](#fn-callable) | Returns `true` when `v` is callable. |
+| [`false(v)`](#fn-false) | Returns `true` when `v` is exactly `false`. |
+| [`falsy(v)`](#fn-falsy) | Returns `true` when `v` is falsy. |
+| [`integer(v)`](#fn-integer) | Returns `true` when `v` is an integer. |
+| [`true(v)`](#fn-true) | Returns `true` when `v` is exactly `true`. |
+| [`truthy(v)`](#fn-truthy) | Returns `true` when `v` is truthy. |
+
+### Path Checks
+
+
+
+#### `block(v)`
+
+Returns `true` when `v` is a block device path.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isBlock` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.block("/dev/sda")
+```
+
+
+
+#### `char(v)`
+
+Returns `true` when `v` is a character device path.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isChar` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.char("/dev/null")
+```
+
+
+
+#### `device(v)`
+
+Returns `true` when `v` is a block or character device path.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isDevice` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.device("/dev/null")
+```
+
+
+
+#### `dir(v)`
+
+Returns `true` when `v` is a directory path.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isDir` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.dir("/tmp")
+```
+
+
+
+#### `fifo(v)`
+
+Returns `true` when `v` is a FIFO path.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isFifo` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.fifo("/path/to/fifo")
+```
+
+
+
+#### `file(v)`
+
+Returns `true` when `v` is a file path.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isFile` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.file("README.md")
+```
+
+
+
+#### `link(v)`
+
+Returns `true` when `v` is a symlink path.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isLink` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.link("/path/to/link")
+```
+
+
+
+#### `path(v)`
+
+Returns `true` when `v` is a valid filesystem path.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isPath` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.path("README.md")
+```
+
+> [!NOTE]
+>
+> Returns `true` for broken symlinks.
+
+
+
+#### `socket(v)`
+
+Returns `true` when `v` is a socket path.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isSocket` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.socket("/path/to/socket")
+```
+
+### Type Checks
+
+
+
+#### `boolean(v)`
+
+Returns `true` when `v` is a boolean.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isBoolean` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.boolean(true)
+```
+
+
+
+#### `function(v)`
+
+Returns `true` when `v` is a function.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isFunction` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.Function(function() end)
+```
+
+
+
+#### `nil(v)`
+
+Returns `true` when `v` is `nil`.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isNil` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.Nil(nil)
+```
+
+
+
+#### `number(v)`
+
+Returns `true` when `v` is a number.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isNumber` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.number(3.14)
+```
+
+
+
+#### `string(v)`
+
+Returns `true` when `v` is a string.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isString` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.string("hello")
+```
+
+
+
+#### `table(v)`
+
+Returns `true` when `v` is a table.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isTable` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.table({})
+```
+
+
+
+#### `thread(v)`
+
+Returns `true` when `v` is a thread.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isThread` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.thread(coroutine.create(function() end))
+```
+
+
+
+#### `userdata(v)`
+
+Returns `true` when `v` is userdata.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isUserdata` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.userdata(io.stdout)
+```
+
+### Value Checks
+
+
+
+#### `callable(v)`
+
+Returns `true` when `v` is callable.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isCallable` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.callable(function() end)
+```
+
+
+
+#### `false(v)`
+
+Returns `true` when `v` is exactly `false`.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isFalse` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.False(false)
+```
+
+
+
+#### `falsy(v)`
+
+Returns `true` when `v` is falsy.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isFalsy` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.falsy(false)
+```
+
+
+
+#### `integer(v)`
+
+Returns `true` when `v` is an integer.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isInteger` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.integer(42)
+```
+
+
+
+#### `true(v)`
+
+Returns `true` when `v` is exactly `true`.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isTrue` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.True(true)
+```
+
+
+
+#### `truthy(v)`
+
+Returns `true` when `v` is truthy.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isTruthy` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+is.truthy("non-empty")
+```
diff --git a/docs/api/json.md b/docs/api/json.md
new file mode 100644
index 0000000..f123b42
--- /dev/null
+++ b/docs/api/json.md
@@ -0,0 +1,165 @@
+---
+description: "JSON encoding and decoding helpers."
+---
+
+# `json`
+
+JSON encoding and decoding helpers.
+
+> [!NOTE]
+>
+> This module aims to implement strict
+> [RFC 8259](https://www.rfc-editor.org/rfc/rfc8259) JSON behavior.
+
+## Usage
+
+```lua
+local json = require "mods.json"
+
+local encoded = json.encode({ ok = true, value = 42 })
+local decoded = json.decode(encoded)
+
+print(encoded) --> {"ok":true,"value":42}
+print(decoded.ok) --> true
+print(decoded.value) --> 42
+```
+
+## Behavior
+
+- booleans, strings, and finite numbers map directly to JSON values
+
+```lua
+print(json.encode(true)) --> true
+print(json.encode("hi")) --> "hi"
+print(json.encode(12.5)) --> 12.5
+```
+
+- `json.null` encodes to `null`, and decoded `null` becomes `json.null`
+
+```lua
+local value = json.decode('{"a":null}')
+print(value.a == json.null) --> true
+print(json.encode(json.null)) --> null
+```
+
+- tables encode as arrays or objects based on their keys
+
+```lua
+print(json.encode({})) --> []
+print(json.encode({ "a", "b" })) --> ["a","b"]
+print(json.encode({ a = 1 })) --> {"a":1}
+```
+
+- standalone JSON values like booleans, strings, and numbers are valid
+
+```lua
+print(json.decode("true")) --> true
+print(json.decode('"text"')) --> text
+print(json.decode("42")) --> 42
+```
+
+- encoding rejects unsupported Lua types, cyclic tables, mixed tables, sparse
+ arrays, `NaN`, and infinities
+
+```lua
+local t = {}
+t.self = t
+
+assert(json.encode(function() end))
+--> unsupported type: function
+
+assert(json.encode(t))
+--> cannot encode cyclic table
+
+assert(json.encode({ [1] = "a", b = true }))
+--> cannot encode mixed table
+
+assert(json.encode({ [1] = "a", [3] = "c" }))
+--> cannot encode sparse array
+
+assert(json.encode(0 / 0))
+--> cannot encode NaN
+
+assert(json.encode(math.huge))
+--> cannot encode infinity
+```
+
+- decoding rejects comments, trailing commas, single-quoted strings, and invalid
+ escapes
+
+```lua
+assert(json.decode('{"a":1,}'))
+--> expected string key at line 1, column 8
+
+assert(json.decode('{"a":1}// comment'))
+--> unexpected trailing content at line 1, column 8
+
+assert(json.decode("{'a':1}"))
+--> expected string key at line 1, column 2
+
+assert(json.decode('["\\x"]'))
+--> invalid escape sequence at line 1, column 3
+```
+
+## Functions
+
+
+
+### `decode(s)`
+
+Decode a JSON string into Lua values.
+
+**Parameters**:
+
+- `s` (`string`): JSON string.
+
+**Return**:
+
+- `value` (`any`): Decoded Lua value.
+- `err` (`string?`): Error message when decoding fails.
+
+**Example**:
+
+```lua
+local value = json.decode('{"user":"Ada","active":true,"note":null}')
+print(value.user) --> Ada
+print(value.active) --> true
+print(value.note == json.null) --> true
+```
+
+
+
+### `encode(value, opts?)`
+
+Encode a Lua value as JSON.
+
+**Parameters**:
+
+- `value` (`any`): Lua value to encode.
+- `opts?` (`{sort_keys?:boolean, indent?:string}`): Encoding options.
+
+**Return**:
+
+- `json` (`string?`): JSON string.
+- `err` (`string?`): Error message when encoding fails.
+
+**Example**:
+
+```lua
+local s = json.encode({
+ ok = true,
+ items = { 1, 2, 3 },
+}, {
+ indent = " ",
+})
+
+print(s)
+-- {
+-- "ok": true,
+-- "items": [
+-- 1,
+-- 2,
+-- 3
+-- ]
+-- }
+```
diff --git a/docs/api/keyword.md b/docs/api/keyword.md
new file mode 100644
index 0000000..738d8c4
--- /dev/null
+++ b/docs/api/keyword.md
@@ -0,0 +1,140 @@
+---
+description: "Helpers for Lua keywords and identifiers."
+---
+
+# `keyword`
+
+Helpers for Lua keywords and identifiers.
+
+## Usage
+
+```lua
+kw = require "mods.keyword"
+
+kw.iskeyword("local")) --> true
+kw.isidentifier("hello_world") --> true
+```
+
+## Functions
+
+**Collections**:
+
+| Function | Description |
+| ------------------------ | ------------------------------------- |
+| [`kwlist()`](#fn-kwlist) | Return Lua keywords as a `mods.List`. |
+| [`kwset()`](#fn-kwset) | Return Lua keywords as a `mods.Set`. |
+
+**Normalization**:
+
+| Function | Description |
+| ----------------------------------------------------- | ---------------------------------------------- |
+| [`normalize_identifier(s)`](#fn-normalize-identifier) | Normalize an input into a safe Lua identifier. |
+
+**Predicates**:
+
+| Function | Description |
+| ------------------------------------- | ------------------------------------------------------------- |
+| [`isidentifier(v)`](#fn-isidentifier) | Return `true` when `v` is a valid non-keyword Lua identifier. |
+| [`iskeyword(v)`](#fn-iskeyword) | Return `true` when `v` is a reserved Lua keyword. |
+
+### Collections
+
+
+
+#### `kwlist()`
+
+Return Lua keywords as a `mods.List`.
+
+**Return**:
+
+- `words` (`mods.List`): List of Lua keywords.
+
+**Example**:
+
+```lua
+kw.kwlist():contains("and") --> true
+kw.kwlist():contains("global") --> true -- Lua 5.5+
+```
+
+
+
+#### `kwset()`
+
+Return Lua keywords as a `mods.Set`.
+
+**Return**:
+
+- `words` (`mods.Set`): Set of Lua keywords.
+
+**Example**:
+
+```lua
+kw.kwlset():contains("and") --> true
+kw.kwlset():contains("global") --> true -- Lua 5.5+
+```
+
+### Normalization
+
+
+
+#### `normalize_identifier(s)`
+
+Normalize an input into a safe Lua identifier.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `identifier` (`string`): Normalized Lua identifier.
+
+**Example**:
+
+```lua
+kw.normalize_identifier(" 2 bad-name ") --> "_2_bad_name"
+```
+
+### Predicates
+
+
+
+#### `isidentifier(v)`
+
+Return `true` when `v` is a valid non-keyword Lua identifier.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isIdentifier` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+kw.isidentifier("hello_world") --> true
+kw.isidentifier("local") --> false
+```
+
+
+
+#### `iskeyword(v)`
+
+Return `true` when `v` is a reserved Lua keyword.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isKeyword` (`boolean`): Whether the check succeeds.
+
+**Example**:
+
+```lua
+kw.iskeyword("function") --> true
+kw.iskeyword("hello") --> false
+```
diff --git a/docs/api/list.md b/docs/api/list.md
new file mode 100644
index 0000000..7a6bba1
--- /dev/null
+++ b/docs/api/list.md
@@ -0,0 +1,1343 @@
+---
+description:
+ "A list class for creating, transforming, and querying sequences of values."
+---
+
+# `List`
+
+A list class for creating, transforming, and querying sequences of values.
+
+## Usage
+
+```lua
+List = require "mods.List"
+
+ls = List({ "a" }):append("b")
+print(ls:contains("b")) --> true
+print(ls:index("b")) --> 2
+```
+
+> [!NOTE]
+>
+> `List(t)` wraps `t` with the `List` metatable in place. It does not copy or
+> filter table values. `List(t):copy()` or `List.copy(t)` both copy only `1..#t`
+> and wrap `t` as a List.
+
+## Functions
+
+**Access**:
+
+| Function | Description |
+| ---------------------- | ------------------------------------------- |
+| [`first()`](#fn-first) | Return the first element or `nil` if empty. |
+| [`last()`](#fn-last) | Return the last element or `nil` if empty. |
+
+**Copies**:
+
+| Function | Description |
+| -------------------- | ---------------------------------- |
+| [`copy()`](#fn-copy) | Return a shallow copy of the list. |
+
+**Mutation**:
+
+| Function | Description |
+| ------------------------------ | -------------------------------------------------------------------- |
+| [`append()`](#fn-append) | Append a value to the end of the list. |
+| [`clear()`](#fn-clear) | Remove all elements from the list. |
+| [`extend(t)`](#fn-extend) | Extend the list with another list or set. |
+| [`extract(pred)`](#fn-extract) | Extract values matching the predicate and remove them from the list. |
+| [`insert(pos, v)`](#fn-insert) | Insert a value at the given position. |
+| [`insert(v)`](#fn-insert) | Append a value to the end of the list. |
+| [`pop()`](#fn-pop) | Remove and return the last element. |
+| [`pop(pos)`](#fn-pop) | Remove and return the element at the given position. |
+| [`prepend(v)`](#fn-prepend) | Insert a value at the start of the list. |
+| [`remove(v)`](#fn-remove) | Remove the first matching value. |
+| [`shuffle(rng?)`](#fn-shuffle) | Shuffle the list in place. |
+| [`sort(comp?)`](#fn-sort) | Sort the list in place. |
+
+**Predicates**:
+
+| Function | Description |
+| -------------------------- | ------------------------------------------------- |
+| [`all(pred)`](#fn-all) | Return `true` if all values match the predicate. |
+| [`any(pred)`](#fn-any) | Return `true` if any value matches the predicate. |
+| [`equals(ls)`](#fn-equals) | Compare two lists using shallow element equality. |
+| [`le(ls)`](#fn-le) | Compare two lists lexicographically. |
+| [`lt(ls)`](#fn-lt) | Compare two lists lexicographically. |
+
+**Queries**:
+
+| Function | Description |
+| -------------------------------- | ----------------------------------------------------------- |
+| [`contains(v)`](#fn-contains) | Return `true` if the list contains the value. |
+| [`count(v)`](#fn-count) | Count how many times a value appears. |
+| [`index(v)`](#fn-index) | Return the index of the first matching value. |
+| [`index_if(pred)`](#fn-index-if) | Return the index of the first value matching the predicate. |
+| [`isempty()`](#fn-isempty) | Return whether the list has no elements. |
+| [`len()`](#fn-len) | Return the number of elements in the list. |
+
+**Transforms**:
+
+| Function | Description |
+| ------------------------------------- | -------------------------------------------------------------------- |
+| [`concat(sep?, i?, j?)`](#fn-concat) | Concatenate list values using Lua's native `table.concat` behavior. |
+| [`difference(t)`](#fn-difference) | Return a new list with values not in the given list or set. |
+| [`drop(n)`](#fn-drop) | Return a new list without the first n elements. |
+| [`filter(pred)`](#fn-filter) | Return a new list with values matching the predicate. |
+| [`flatten()`](#fn-flatten) | Flatten one level of nested lists. |
+| [`foreach(fn)`](#fn-foreach) | Apply a function to each element (for side effects). |
+| [`group_by(fn)`](#fn-group-by) | Group list values by a computed key. |
+| [`intersection(t)`](#fn-intersection) | Return values that are also present in the given list or set. |
+| [`invert()`](#fn-invert) | Invert values to indices in a new table. |
+| [`join(sep?, quoted?)`](#fn-join) | Join list values into a string. |
+| [`keypath()`](#fn-keypath) | Render list items as a table-access key path. |
+| [`map(fn)`](#fn-map) | Return a new list by mapping each value. |
+| [`mirror()`](#fn-mirror) | Mirror values into a new table as both keys and values. |
+| [`mul(n)`](#fn-mul) | Return a new list repeated `n` times (list multiplication behavior). |
+| [`reduce(fn, init?)`](#fn-reduce) | Reduce the list to a single value using an accumulator. |
+| [`reverse()`](#fn-reverse) | Reverse the list in place. |
+| [`slice(i?, j?)`](#fn-slice) | Return a new list containing items from i to j (inclusive). |
+| [`take(n)`](#fn-take) | Return the first n elements as a new list. |
+| [`toset()`](#fn-toset) | Convert the list to a set. |
+| [`tostring()`](#fn-tostring) | Render the list to a string via the regular method form. |
+| [`uniq()`](#fn-uniq) | Return a new list with duplicates removed (first occurrence kept). |
+| [`zip(t)`](#fn-zip) | Zip two collections into a list of 2-element tables. |
+
+**Metamethods**:
+
+| Function | Description |
+| ------------------------------ | --------------------------------------------------------------------------------------------------------------- |
+| [`__add(ls)`](#fn-add) | Extend the left-hand list in place with right-hand values, then return the same left-hand list reference (`+`). |
+| [`__eq(ls)`](#fn-eq) | Compare two lists using shallow element equality (`==`). |
+| [`__le(ls)`](#fn-le) | Compare two lists lexicographically (`<=`). |
+| [`__lt(ls)`](#fn-lt) | Compare two lists lexicographically (`<`). |
+| [`__mul(n)`](#fn-mul) | Repeat a list `n` times (`*`). |
+| [`__sub(ls)`](#fn-sub) | Return values from the left list that are not present in the right list (`-`). |
+| [`__tostring()`](#fn-tostring) | Render the list to a string like `{ "a", "b", 1 }`. |
+
+### Access
+
+
+
+#### `first()`
+
+Return the first element or `nil` if empty.
+
+**Return**:
+
+- `firstValue` (`any`): First value, or `nil` if empty.
+
+**Example**:
+
+```lua
+v = List({ "a", "b" }):first() --> "a"
+```
+
+
+
+#### `last()`
+
+Return the last element or `nil` if empty.
+
+**Return**:
+
+- `lastValue` (`any`): Last value, or `nil` if empty.
+
+**Example**:
+
+```lua
+v = List({ "a", "b" }):last() --> "b"
+```
+
+### Copies
+
+
+
+#### `copy()`
+
+Return a shallow copy of the list.
+
+**Return**:
+
+- `ls` (`mods.List`): New list.
+
+**Example**:
+
+```lua
+c = List({ "a", "b" }):copy() --> { "a", "b" }
+```
+
+### Mutation
+
+
+
+#### `append()`
+
+Append a value to the end of the list.
+
+**Return**:
+
+- `self` (`T`): Current list.
+
+**Example**:
+
+```lua
+ls = List({ "a" }):append("b") --> { "a", "b" }
+```
+
+
+
+#### `clear()`
+
+Remove all elements from the list.
+
+**Return**:
+
+- `self` (`T`): Current list.
+
+**Example**:
+
+```lua
+ls = List({ "a", "b" }):clear() --> { }
+```
+
+
+
+#### `extend(t)`
+
+Extend the list with another list or set.
+
+**Parameters**:
+
+- `t` (`mods.List|mods.Set|any[]`): Values to append.
+
+**Return**:
+
+- `self` (`T`): Current list.
+
+**Example**:
+
+```lua
+ls = List({ "a" }):extend({ "b", "c" }) --> { "a", "b", "c" }
+ls = List({ "a" }):extend(Set({ "b", "c" })) --> { "a", "b", "c" }
+```
+
+> [!NOTE]
+>
+> `extend` is also available through the `+` operator.
+
+
+
+#### `extract(pred)`
+
+Extract values matching the predicate and remove them from the list.
+
+**Parameters**:
+
+- `pred` (`fun(v:any):boolean`): Predicate function.
+
+**Return**:
+
+- `ls` (`mods.List`): Extracted values.
+
+**Example**:
+
+```lua
+ls = List({ "a", "bb", "c" })
+is_len_1 = function(v) return #v == 1 end
+ex = ls:extract(is_len_1) --> ex = { "a", "c" }, ls = { "bb" }
+```
+
+
+
+#### `insert(pos, v)`
+
+Insert a value at the given position.
+
+**Parameters**:
+
+- `pos` (`integer`): Insert position.
+- `v` (`any`): Value to insert.
+
+**Return**:
+
+- `self` (`T`): Current list.
+
+**Example**:
+
+```lua
+ls = List({ "a", "c" }):insert(2, "b") --> { "a", "b", "c" }
+```
+
+
+
+#### `insert(v)`
+
+Append a value to the end of the list.
+
+**Parameters**:
+
+- `v` (`any`): Value to append.
+
+**Return**:
+
+- `self` (`T`): Current list.
+
+**Example**:
+
+```lua
+ls = List({ "a", "b" }):insert("c") --> { "a", "b", "c" }
+```
+
+
+
+#### `pop()`
+
+Remove and return the last element.
+
+**Return**:
+
+- `removedValue` (`any`): Removed value.
+
+**Example**:
+
+```lua
+ls = List({ "a", "b" })
+v = ls:pop() --> v == "b"; ls is { "a" }
+```
+
+
+
+#### `pop(pos)`
+
+Remove and return the element at the given position.
+
+**Parameters**:
+
+- `pos` (`integer`): Numeric value.
+
+**Return**:
+
+- `removedValue` (`any`): Removed value.
+
+**Example**:
+
+```lua
+ls = List({ "a", "b", "c" })
+v = ls:pop(2) --> v == "b"; ls is { "a", "c" }
+```
+
+
+
+#### `prepend(v)`
+
+Insert a value at the start of the list.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `self` (`T`): Current list.
+
+**Example**:
+
+```lua
+ls = List({ "b", "c" })
+ls:prepend("a") --> { "a", "b", "c" }
+```
+
+
+
+#### `remove(v)`
+
+Remove the first matching value.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `self` (`T`): Current list.
+
+**Example**:
+
+```lua
+ls = List({ "a", "b", "b" })
+ls:remove("b") --> { "a", "b" }
+```
+
+
+
+#### `shuffle(rng?)`
+
+Shuffle the list in place.
+
+**Parameters**:
+
+- `rng?` (`fun(lo:integer, hi:integer):integer`): Optional random index picker;
+ defaults to `math.random`.
+
+**Return**:
+
+- `self` (`T`): Current list.
+
+**Example**:
+
+```lua
+ls = List({ "a", "b", "c" }):shuffle() --> { "b", "c", "a" } -- order varies
+```
+
+
+
+#### `sort(comp?)`
+
+Sort the list in place.
+
+**Parameters**:
+
+- `comp?` (`fun(a:any, b:any):boolean`): Optional comparison function (defaults
+ to `nil`).
+
+**Return**:
+
+- `self` (`T`): Current list.
+
+**Example**:
+
+```lua
+ls = List({ 3, 1, 2 })
+ls:sort() --> { 1, 2, 3 }
+
+words = List({ "ccc", "a", "bb" })
+words:sort(function(a, b)
+ return #a < #b
+end) --> { "a", "bb", "ccc" }
+```
+
+### Predicates
+
+
+
+#### `all(pred)`
+
+Return `true` if all values match the predicate.
+
+**Parameters**:
+
+- `pred` (`fun(v:any):boolean`): Predicate function.
+
+**Return**:
+
+- `allMatch` (`boolean`): Whether the condition is met.
+
+**Example**:
+
+```lua
+is_even = function(v) return v % 2 == 0 end
+ok = List({ 2, 4 }):all(is_even) --> true
+```
+
+> [!NOTE]
+>
+> Empty lists return `true`.
+
+
+
+#### `any(pred)`
+
+Return `true` if any value matches the predicate.
+
+**Parameters**:
+
+- `pred` (`fun(v:any):boolean`): Predicate function.
+
+**Return**:
+
+- `anyMatch` (`boolean`): Whether the condition is met.
+
+**Example**:
+
+```lua
+has_len_2 = function(v) return #v == 2 end
+ok = List({ "a", "bb" }):any(has_len_2) --> true
+```
+
+
+
+#### `equals(ls)`
+
+Compare two lists using shallow element equality.
+
+**Parameters**:
+
+- `ls` (`mods.List|any[]`): Other list value.
+
+**Return**:
+
+- `isEqual` (`boolean`): Whether the condition is met.
+
+**Example**:
+
+```lua
+a = List({ "x", "y" })
+b = List({ "x", "y" })
+ok = a:equals(b) --> true
+```
+
+> [!NOTE]
+>
+> - `equals` is also available through the `==` operator when both operands are
+> `List`.
+>
+> ```lua
+> a = List({ "a", 1 })
+> b = List({ "a", 1 })
+> ok = (a == b) --> true
+> ```
+>
+> - Unlike `==`, this method also works when `ls` is a plain array table.
+>
+> ```lua
+> a = List({ "a", 1 })
+> b = { "a", 1 }
+> ok = a:equals(b) --> true
+> ```
+>
+> - `equals` checks only array positions (`1..#list`), so extra non-array keys
+> are ignored:
+>
+> ```lua
+> t = {}
+> a = List({ "a", t })
+> b = { "a", t, a = 1 }
+> ok = a:equals(b) --> true
+> ```
+
+
+
+#### `le(ls)`
+
+Compare two lists lexicographically.
+
+**Parameters**:
+
+- `ls` (`mods.List|any[]`): Other list value.
+
+**Return**:
+
+- `isLessOrEqual` (`boolean`): Whether the condition is met.
+
+**Example**:
+
+```lua
+ok = List({ 1, 2 }):le({ 1, 2 }) --> true
+ok = List({ 1, 2 }):le({ 1, 1 }) --> false
+```
+
+> [!NOTE]
+>
+> `le` is also available through the `<=` operator.
+
+
+
+#### `lt(ls)`
+
+Compare two lists lexicographically.
+
+**Parameters**:
+
+- `ls` (`mods.List|any[]`): Other list value.
+
+**Return**:
+
+- `isLess` (`boolean`): Whether the condition is met.
+
+**Example**:
+
+```lua
+ok = List({ 1, 2 }):lt({ 1, 3 }) --> true
+ok = List({ 1, 2 }):lt({ 1, 2, 0 }) --> true
+```
+
+> [!NOTE]
+>
+> `lt` is also available through the `<` operator.
+
+### Queries
+
+
+
+#### `contains(v)`
+
+Return `true` if the list contains the value.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `isPresent` (`boolean`): True when `v` is present in the list.
+
+**Example**:
+
+```lua
+ok = List({ "a", "b" }):contains("b") --> true
+```
+
+
+
+#### `count(v)`
+
+Count how many times a value appears.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `res` (`integer`): Result count.
+
+**Example**:
+
+```lua
+n = List({ "a", "b", "b" }):count("b") --> 2
+```
+
+
+
+#### `index(v)`
+
+Return the index of the first matching value.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+
+**Return**:
+
+- `index` (`integer?`): Result index, or nil when not found.
+
+**Example**:
+
+```lua
+i = List({ "a", "b", "c", "b" }):index("b") --> 2
+```
+
+
+
+#### `index_if(pred)`
+
+Return the index of the first value matching the predicate.
+
+**Parameters**:
+
+- `pred` (`fun(v:any):boolean`): Predicate function.
+
+**Return**:
+
+- `index` (`integer?`): Result index, or nil when no value matches.
+
+**Example**:
+
+```lua
+gt_1 = function(x) return x > 1 end
+i = List({ 1, 2, 3 }):index_if(gt_1) --> 2
+```
+
+
+
+#### `isempty()`
+
+Return whether the list has no elements.
+
+**Return**:
+
+- `empty` (`boolean`): `true` when the list has no elements.
+
+**Example**:
+
+```lua
+ok = List():isempty() --> true
+```
+
+
+
+#### `len()`
+
+Return the number of elements in the list.
+
+**Return**:
+
+- `count` (`integer`): Element count.
+
+**Example**:
+
+```lua
+n = List({ "a", "b", "c" }):len() --> 3
+```
+
+> [!NOTE]
+>
+> Uses Lua's `#` operator.
+
+### Transforms
+
+
+
+#### `concat(sep?, i?, j?)`
+
+Concatenate list values using Lua's native `table.concat` behavior.
+
+**Parameters**:
+
+- `sep?` (`string`): Optional separator value (defaults to `""`).
+- `i?` (`integer`): Optional start index (defaults to `1`).
+- `j?` (`integer`): Optional end index (defaults to `#self`).
+
+**Return**:
+
+- `concatenated` (`string`): Concatenated string.
+
+**Example**:
+
+```lua
+s = List({ "a", "b", "c" }):concat(",") --> "a,b,c"
+```
+
+> [!NOTE]
+>
+> This method forwards to `table.concat` directly and keeps its strict element
+> rules.
+
+
+
+#### `difference(t)`
+
+Return a new list with values not in the given list or set.
+
+**Parameters**:
+
+- `t` (`mods.List|mods.Set|any[]`): Values to remove.
+
+**Return**:
+
+- `ls` (`T`): New list.
+
+**Example**:
+
+```lua
+d = List({ "a", "b", "c" }):difference({ "b" }) --> { "a", "c" }
+```
+
+> [!NOTE]
+>
+> `difference` is also available through the `-` operator.
+
+
+
+#### `drop(n)`
+
+Return a new list without the first n elements.
+
+**Parameters**:
+
+- `n` (`integer`): Numeric value.
+
+**Return**:
+
+- `ls` (`mods.List`): New list.
+
+**Example**:
+
+```lua
+t = List({ "a", "b", "c" }):drop(1) --> { "b", "c" }
+```
+
+
+
+#### `filter(pred)`
+
+Return a new list with values matching the predicate.
+
+**Parameters**:
+
+- `pred` (`fun(v:any):boolean`): Predicate function.
+
+**Return**:
+
+- `ls` (`mods.List`): New list.
+
+**Example**:
+
+```lua
+is_len_1 = function(v) return #v == 1 end
+f = List({ "a", "bb", "c" }):filter(is_len_1) --> { "a", "c" }
+```
+
+
+
+#### `flatten()`
+
+Flatten one level of nested lists.
+
+**Return**:
+
+- `ls` (`mods.List`): New list.
+
+**Example**:
+
+```lua
+f = List({ { "a", "b" }, { "c" } }):flatten() --> { "a", "b", "c" }
+```
+
+
+
+#### `foreach(fn)`
+
+Apply a function to each element (for side effects).
+
+**Parameters**:
+
+- `fn` (`fun(v:any)`): Callback function.
+
+**Return**:
+
+- `none` (`nil`)
+
+**Example**:
+
+```lua
+List({ "a", "b" }):foreach(print)
+--> prints -> a
+--> prints -> b
+```
+
+
+
+#### `group_by(fn)`
+
+Group list values by a computed key.
+
+**Parameters**:
+
+- `fn` (`fun(v:any):any`): Callback function.
+
+**Return**:
+
+- `groups` (`table`): Groups keyed by the callback result.
+
+**Example**:
+
+```lua
+words = { "aa", "b", "ccc", "dd" }
+g = List(words):group_by(string.len) --> { {"b"}, { "aa", "dd" }, { "ccc" } }
+```
+
+
+
+#### `intersection(t)`
+
+Return values that are also present in the given list or set.
+
+**Parameters**:
+
+- `t` (`mods.List|mods.Set|any[]`): Other list/set.
+
+**Return**:
+
+- `ls` (`mods.List`): New list.
+
+**Example**:
+
+```lua
+i = List({ "a", "b", "a", "c" }):intersection({ "a", "c" })
+--> { "a", "a", "c" }
+```
+
+> [!NOTE]
+>
+> Order is preserved from the original list.
+
+
+
+#### `invert()`
+
+Invert values to indices in a new table.
+
+**Return**:
+
+- `idxByValue` (`table`): Table mapping each value to its last index.
+
+**Example**:
+
+```lua
+t = List({ "a", "b", "c" }):invert() --> { a = 1, b = 2, c = 3 }
+```
+
+
+
+#### `join(sep?, quoted?)`
+
+Join list values into a string.
+
+**Parameters**:
+
+- `sep?` (`string`): Optional separator value (defaults to `""`).
+- `quoted?` (`boolean`): Optional boolean flag (defaults to `false`).
+
+**Return**:
+
+- `joined` (`string`): Joined string.
+
+**Example**:
+
+```lua
+s = List({ "a", "b", "c" }):join(",") --> "a,b,c"
+s = List({ "a", "b", "c" }):join(", ", true) --> '"a", "b", "c"'
+```
+
+> [!NOTE]
+>
+> Values are converted with `tostring` before joining. Set `quoted = true` to
+> quote string values.
+
+
+
+#### `keypath()`
+
+Render list items as a table-access key path.
+
+**Return**:
+
+- `keyPath` (`string`): Key-path string.
+
+**Example**:
+
+```lua
+p = List({ "ctx", "users", 1, "name" }):keypath() --> "ctx.users[1].name"
+```
+
+
+
+#### `map(fn)`
+
+Return a new list by mapping each value.
+
+**Parameters**:
+
+- `fn` (`fun(value:T):any`): Callback function.
+
+**Return**:
+
+- `ls` (`mods.List`): New list.
+
+**Example**:
+
+```lua
+to_upper = function(v) return v:upper() end
+m = List({ "a", "b" }):map(to_upper) --> { "A", "B" }
+```
+
+
+
+#### `mirror()`
+
+Mirror values into a new table as both keys and values.
+
+**Return**:
+
+- `mirroredValues` (`table`): Table mapping each value to itself.
+
+**Example**:
+
+```lua
+t = List({ "a", "b", "c" }):mirror() --> { a = "a", b = "b", c = "c" }
+```
+
+
+
+#### `mul(n)`
+
+Return a new list repeated `n` times (list multiplication behavior).
+
+**Parameters**:
+
+- `n` (`integer`): Numeric value.
+
+**Return**:
+
+- `ls` (`mods.List`): New list.
+
+**Example**:
+
+```lua
+ls = List({ "a", "b" }):mul(3) --> { "a", "b", "a", "b", "a", "b" }
+```
+
+> [!NOTE]
+>
+> `mul` is also available through the `*` operator.
+
+
+
+#### `reduce(fn, init?)`
+
+Reduce the list to a single value using an accumulator.
+
+**Parameters**:
+
+- `fn` (`fun(acc:any, v:any):any`): Reducer function.
+- `init?` (`any`): Optional initial accumulator; for non-empty lists, `nil` or
+ omitted uses the first item.
+
+**Return**:
+
+- `reducedValue` (`any`): Reduced value.
+
+**Example**:
+
+```lua
+add = function(acc, v) return acc + v end
+sum = List({ 1, 2, 3 }):reduce(add, 0) --> 6
+sum = List({ 1, 2, 3 }):reduce(add, 10) --> 16
+```
+
+> [!NOTE]
+>
+> For empty lists, returns `init` unchanged (or `nil` when omitted).
+
+
+
+#### `reverse()`
+
+Reverse the list in place.
+
+**Return**:
+
+- `ls` (`mods.List`): Same list, reversed in place.
+
+**Example**:
+
+```lua
+r = List({ "a", "b", "c" }):reverse() --> { "c", "b", "a" }
+```
+
+
+
+#### `slice(i?, j?)`
+
+Return a new list containing items from i to j (inclusive).
+
+**Parameters**:
+
+- `i?` (`integer`): Optional start index (defaults to `1`).
+- `j?` (`integer`): Optional end index (defaults to `#self`).
+
+**Return**:
+
+- `ls` (`mods.List`): New list.
+
+**Example**:
+
+```lua
+t = List({ "a", "b", "c", "d" }):slice(2, 3) --> { "b", "c" }
+```
+
+> [!NOTE]
+>
+> Supports negative indices (-1 is last element).
+
+
+
+#### `take(n)`
+
+Return the first n elements as a new list.
+
+**Parameters**:
+
+- `n` (`integer`): Numeric value.
+
+**Return**:
+
+- `ls` (`mods.List`): New list.
+
+**Example**:
+
+```lua
+t = List({ "a", "b", "c" }):take(2) --> { "a", "b" }
+```
+
+
+
+#### `toset()`
+
+Convert the list to a set.
+
+**Return**:
+
+- `set` (`mods.Set`): New set.
+
+**Example**:
+
+```lua
+s = List({ "a", "b", "a" }):toset() --> { a = true, b = true }
+```
+
+> [!NOTE]
+>
+> Order is preserved from the original list.
+
+
+
+#### `tostring()`
+
+Render the list to a string via the regular method form.
+
+**Return**:
+
+- `renderedList` (`string`): Rendered list string.
+
+**Example**:
+
+```lua
+s = List({ "a", "b", 1 }):tostring() --> '{ "a", "b", 1 }'
+```
+
+
+
+#### `uniq()`
+
+Return a new list with duplicates removed (first occurrence kept).
+
+**Return**:
+
+- `ls` (`mods.List`): New list.
+
+**Example**:
+
+```lua
+u = List({ "a", "b", "a", "c" }):uniq() --> { "a", "b", "c" }
+```
+
+
+
+#### `zip(t)`
+
+Zip two collections into a list of 2-element tables.
+
+**Parameters**:
+
+- `t` (`mods.List|mods.Set|any[]`): Values to pair with.
+
+**Return**:
+
+- `ls` (`mods.List`): New list.
+
+**Example**:
+
+```lua
+z = List({ "a", "b" }):zip({ 1, 2 }) --> { {"a",1}, {"b",2} }
+z = List({ "a", "b" }):zip(Set({ 1, 2 })) --> { {"a",1}, {"b",2} }
+```
+
+> [!NOTE]
+>
+> Length is the minimum of both tables' lengths.
+
+### Metamethods
+
+
+
+#### `__add(ls)`
+
+Extend the left-hand list in place with right-hand values, then return the same
+left-hand list reference (`+`).
+
+**Parameters**:
+
+- `ls` (`mods.List|any[]`): Other list value.
+
+**Return**:
+
+- `self` (`mods.List|any[]`): Current list.
+
+**Example**:
+
+```lua
+a = List({ "a", "b" })
+b = { "c", "d" }
+c = a + b --> c and a are the same reference: { "a", "b", "c", "d" }
+```
+
+> [!NOTE]
+>
+> `+` operator is equivalent to `:extend(ls)`.
+
+
+
+#### `__eq(ls)`
+
+Compare two lists using shallow element equality (`==`).
+
+**Parameters**:
+
+- `ls` (`mods.List|any[]`): Other list value.
+
+**Return**:
+
+- `isEqual` (`boolean`): Whether the condition is met.
+
+**Example**:
+
+```lua
+a = List({ "a", { 1 } })
+b = List({ "a", { 1 } })
+ok = a == b --> false (different nested table references)
+
+t = { 1 }
+a = List({ "a", t })
+b = List({ "a", t })
+ok = a == b --> true (same nested table reference)
+```
+
+> [!NOTE]
+>
+> - `==` returns `false` for `List` vs plain-table comparisons. Use
+> `:equals(ls)` for `List` vs plain-table comparisons.
+>
+> ```lua
+> t = { "a", 1 }
+> a = List(t)
+> b = { "a", 1 }
+> ok = (a == b) --> false
+> ok = a:equals(b) --> true
+> ```
+>
+> - Like `:equals(ls)`, `==` compares only array positions (`1..#list`), so
+> extra non-array keys are ignored when both operands are `List`.
+>
+> ```lua
+> a = List({ "a", t })
+> b = List({ "a", t, extra = 1 })
+> ok = (a == b) --> true
+> ```
+
+
+
+#### `__le(ls)`
+
+Compare two lists lexicographically (`<=`).
+
+**Parameters**:
+
+- `ls` (`mods.List|any[]`): Other list value.
+
+**Return**:
+
+- `isLessOrEqual` (`boolean`): Whether the condition is met.
+
+**Example**:
+
+```lua
+ok = List({ 1, 2 }) <= List({ 1, 2 }) --> true
+```
+
+> [!NOTE]
+>
+> `<=` is equivalent to `:le(ls)`.
+
+
+
+#### `__lt(ls)`
+
+Compare two lists lexicographically (`<`).
+
+**Parameters**:
+
+- `ls` (`mods.List|any[]`): Other list value.
+
+**Return**:
+
+- `isLess` (`boolean`): Whether the condition is met.
+
+**Example**:
+
+```lua
+ok = List({ 1, 2 }) < List({ 1, 3 }) --> true
+```
+
+> [!NOTE]
+>
+> `<` is equivalent to `:lt(ls)`.
+
+
+
+#### `__mul(n)`
+
+Repeat a list `n` times (`*`).
+
+**Parameters**:
+
+- `n` (`integer|mods.List`): Right operand.
+
+**Return**:
+
+- `ls` (`mods.List`): New list.
+
+**Example**:
+
+```lua
+l1 = List({ "a", "b" }) * 3 --> { "a", "b", "a", "b", "a", "b" }
+l2 = 3 * List({ "a", "b" }) --> { "a", "b", "a", "b", "a", "b" }
+```
+
+> [!NOTE]
+>
+> `*` is equivalent to `:mul(n)`.
+
+
+
+#### `__sub(ls)`
+
+Return values from the left list that are not present in the right list (`-`).
+
+**Parameters**:
+
+- `ls` (`mods.List|any[]`): Other list value.
+
+**Return**:
+
+- `ls` (`mods.List`): New list.
+
+**Example**:
+
+```lua
+a = List({ "a", "b", "c" })
+b = { "b" }
+d = a - b --> { "a", "c" }
+```
+
+> [!NOTE]
+>
+> `-` operator is equivalent to `:difference(ls)`.
+
+
+
+#### `__tostring()`
+
+Render the list to a string like `{ "a", "b", 1 }`.
+
+**Return**:
+
+- `renderedList` (`string`): Rendered list string.
+
+**Example**:
+
+```lua
+s = tostring(List({ "a", "b", 1 })) --> '{ "a", "b", 1 }'
+```
diff --git a/docs/api/log.md b/docs/api/log.md
new file mode 100644
index 0000000..749b4f0
--- /dev/null
+++ b/docs/api/log.md
@@ -0,0 +1,94 @@
+---
+description:
+ "Logger factory that emits normalized records through an optional custom
+ handler."
+---
+
+# `log`
+
+Logger factory that emits normalized records through an optional custom handler.
+When `opts.handler` is omitted, records are written to `io.stderr`.
+
+## Usage
+
+```lua
+log = require "mods.log"
+
+local logger = log.new()
+logger:warn("config missing") --> writes: [WARN]: config missing
+```
+
+## Functions
+
+**Factory**:
+
+| Function | Description |
+| ----------------------- | -------------------- |
+| [`new(opts?)`](#fn-new) | Create a new logger. |
+
+**Logger Methods**:
+
+| Function | Description |
+| -------------------------------- | ----------------------------------------------------------- |
+| [`debug(...)`](#fn-debug) | Emit a `debug` record. |
+| [`error(...)`](#fn-error) | Emit an `error` record. |
+| [`info(...)`](#fn-info) | Emit an `info` record. |
+| [`log(levelname, ...)`](#fn-log) | Emit a record for `level` when it passes the logger filter. |
+| [`warn(...)`](#fn-warn) | Emit a `warn` record. |
+
+### Factory
+
+
+
+#### `new(opts?)`
+
+Create a new logger. **Parameters**:
+
+- `opts?` (`mods.log.new.opts`): Logger configuration.
+
+**Return**:
+
+- `logger` (`mods.log.logger`): Logger instance.
+
+### Logger Methods
+
+
+
+#### `debug(...)`
+
+Emit a `debug` record. **Parameters**:
+
+- `...` (`any`): Additional values joined with spaces.
+
+
+
+#### `error(...)`
+
+Emit an `error` record. **Parameters**:
+
+- `...` (`any`): Additional values joined with spaces.
+
+
+
+#### `info(...)`
+
+Emit an `info` record. **Parameters**:
+
+- `...` (`any`): Additional values joined with spaces.
+
+
+
+#### `log(levelname, ...)`
+
+Emit a record for `level` when it passes the logger filter. **Parameters**:
+
+- `levelname` (`string|"debug"|"info"|"warn"|"error"|"off"`): Log level to emit.
+- `...` (`any`): Additional values joined with spaces.
+
+
+
+#### `warn(...)`
+
+Emit a `warn` record. **Parameters**:
+
+- `...` (`any`): Additional values joined with spaces.
diff --git a/docs/api/ntpath.md b/docs/api/ntpath.md
new file mode 100644
index 0000000..cdb218c
--- /dev/null
+++ b/docs/api/ntpath.md
@@ -0,0 +1,76 @@
+---
+description: "Windows/NT-style path operations."
+---
+
+# `ntpath`
+
+Windows/NT-style path operations.
+
+> 💡Python `ntpath`-style behavior, ported to Lua.
+
+## Usage
+
+```lua
+ntpath = require "mods.ntpath"
+
+print(ntpath.join([[C:\]], "Users", "me")) --> "C:\Users\me"
+print(ntpath.normcase([[A/B\C]])) --> [[a\b\c]]
+print(ntpath.splitdrive([[C:\Users\me]])) --> "C:", [[\Users\me]]
+print(ntpath.isreserved([[C:\Temp\CON.txt]])) --> true
+```
+
+> ✨ Same API as `mods.path`, but with Windows/NT path semantics.
+
+## Functions
+
+
+
+### `_expand_percent_vars(p)`
+
+Expand percent-style variables in a string. **Parameters**:
+
+- `p` (`string`)
+
+**Return**:
+
+- `expanded` (`string`)
+
+
+
+### `ismount(path)`
+
+Return `true` when `path` points to a mount root.
+
+**Parameters**:
+
+- `path` (`string`): Path to inspect.
+
+**Return**:
+
+- `isMount` (`boolean`): `true` if the path resolves to a mount root.
+
+**Example**:
+
+```lua
+ntpath.ismount([[C:\]]) --> true
+```
+
+
+
+### `isreserved(path)`
+
+Return `true` when `path` contains a reserved NT filename.
+
+**Parameters**:
+
+- `path` (`string`): Path to inspect.
+
+**Return**:
+
+- `isReserved` (`boolean`): `true` if any component is NT-reserved.
+
+**Example**:
+
+```lua
+ntpath.isreserved([[a\CON.txt]]) --> true
+```
diff --git a/docs/api/operator.md b/docs/api/operator.md
new file mode 100644
index 0000000..31670a5
--- /dev/null
+++ b/docs/api/operator.md
@@ -0,0 +1,534 @@
+---
+description: "Lua operators exposed as functions."
+---
+
+# `operator`
+
+Lua operators exposed as functions.
+
+## Usage
+
+```lua
+operator = require "mods.operator"
+
+print(operator.add(1, 2)) --> 3
+```
+
+## Functions
+
+**Arithmetic**:
+
+| Function | Description |
+| ------------------------ | --------------------------------------------------------- |
+| [`add(a, b)`](#fn-add) | Add two numbers. |
+| [`div(a, b)`](#fn-div) | Divide `a` by `b` using Lua's floating-point division. |
+| [`idiv(a, b)`](#fn-idiv) | Divide `a` by `b` and return the floor-division quotient. |
+| [`mod(a, b)`](#fn-mod) | Return the modulo remainder of `a` divided by `b`. |
+| [`mul(a, b)`](#fn-mul) | Multiply two numbers. |
+| [`pow(a, b)`](#fn-pow) | Raise `a` to the power of `b`. |
+| [`sub(a, b)`](#fn-sub) | Subtract `b` from `a`. |
+| [`unm(a)`](#fn-unm) | Negate a number. |
+
+**Comparison**:
+
+| Function | Description |
+| ---------------------- | -------------------------------------------------- |
+| [`eq(a, b)`](#fn-eq) | Check whether two values are equal. |
+| [`ge(a, b)`](#fn-ge) | Check whether `a` is greater than or equal to `b`. |
+| [`gt(a, b)`](#fn-gt) | Check whether `a` is strictly greater than `b`. |
+| [`le(a, b)`](#fn-le) | Check whether `a` is less than or equal to `b`. |
+| [`lt(a, b)`](#fn-lt) | Check whether `a` is strictly less than `b`. |
+| [`neq(a, b)`](#fn-neq) | Check whether two values are not equal. |
+
+**Logical**:
+
+| Function | Description |
+| ------------------------ | ---------------------------------------------------- |
+| [`land(a, b)`](#fn-land) | Evaluate `a and b` with Lua short-circuit semantics. |
+| [`lnot(a)`](#fn-lnot) | Return the boolean negation of `a`. |
+| [`lor(a, b)`](#fn-lor) | Evaluate `a or b` with Lua short-circuit semantics. |
+
+**String & Length**:
+
+| Function | Description |
+| ---------------------------- | ---------------------------------------------------------------- |
+| [`concat(a, b)`](#fn-concat) | Concatenate two strings. |
+| [`len(a)`](#fn-len) | Return the length of a string or table using Lua's `#` operator. |
+
+**Tables & Calls**:
+
+| Function | Description |
+| ----------------------------------- | -------------------------------------------------------------- |
+| [`call(f, ...)`](#fn-call) | Call a function with variadic arguments and return its result. |
+| [`index(t, k)`](#fn-index) | Return the value at key/index `k` in table `t`. |
+| [`setindex(t, k, v)`](#fn-setindex) | Set `t[k] = v` and return the assigned value. |
+
+### Arithmetic
+
+
+
+#### `add(a, b)`
+
+Add two numbers.
+
+**Parameters**:
+
+- `a` (`number`): Left numeric value.
+- `b` (`number`): Right numeric value.
+
+**Return**:
+
+- `sum` (`number`): Sum of `a` and `b`.
+
+**Example**:
+
+```lua
+add(1, 2) --> 3
+```
+
+
+
+#### `div(a, b)`
+
+Divide `a` by `b` using Lua's floating-point division.
+
+**Parameters**:
+
+- `a` (`number`): Dividend value.
+- `b` (`number`): Divisor value.
+
+**Return**:
+
+- `quotient` (`number`): Quotient `a / b`.
+
+**Example**:
+
+```lua
+div(10, 4) --> 2.5
+```
+
+
+
+#### `idiv(a, b)`
+
+Divide `a` by `b` and return the floor-division quotient.
+
+**Parameters**:
+
+- `a` (`number`): Dividend value.
+- `b` (`number`): Divisor value.
+
+**Return**:
+
+- `quotient` (`integer`): Floor-division result.
+
+**Example**:
+
+```lua
+idiv(5, 2) --> 2
+```
+
+
+
+#### `mod(a, b)`
+
+Return the modulo remainder of `a` divided by `b`.
+
+**Parameters**:
+
+- `a` (`number`): Dividend value.
+- `b` (`number`): Divisor value.
+
+**Return**:
+
+- `remainder` (`number`): Remainder of `a % b`.
+
+**Example**:
+
+```lua
+mod(5, 2) --> 1
+```
+
+
+
+#### `mul(a, b)`
+
+Multiply two numbers.
+
+**Parameters**:
+
+- `a` (`number`): Left numeric value.
+- `b` (`number`): Right numeric value.
+
+**Return**:
+
+- `product` (`number`): Product `a * b`.
+
+**Example**:
+
+```lua
+mul(3, 4) --> 12
+```
+
+
+
+#### `pow(a, b)`
+
+Raise `a` to the power of `b`.
+
+**Parameters**:
+
+- `a` (`number`): Base value.
+- `b` (`number`): Exponent value.
+
+**Return**:
+
+- `power` (`number`): Result of `a ^ b`.
+
+**Example**:
+
+```lua
+pow(2, 4) --> 16
+```
+
+
+
+#### `sub(a, b)`
+
+Subtract `b` from `a`.
+
+**Parameters**:
+
+- `a` (`number`): Left numeric value.
+- `b` (`number`): Right numeric value.
+
+**Return**:
+
+- `difference` (`number`): Difference `a - b`.
+
+**Example**:
+
+```lua
+sub(5, 3) --> 2
+```
+
+
+
+#### `unm(a)`
+
+Negate a number.
+
+**Parameters**:
+
+- `a` (`number`): Input numeric value.
+
+**Return**:
+
+- `negated` (`number`): Result of `-a`.
+
+**Example**:
+
+```lua
+unm(3) --> -3
+```
+
+### Comparison
+
+
+
+#### `eq(a, b)`
+
+Check whether two values are equal.
+
+**Parameters**:
+
+- `a` (`any`): Left value.
+- `b` (`any`): Right value.
+
+**Return**:
+
+- `isEqual` (`boolean`): True when `a == b`.
+
+**Example**:
+
+```lua
+eq(1, 1) --> true
+```
+
+
+
+#### `ge(a, b)`
+
+Check whether `a` is greater than or equal to `b`.
+
+**Parameters**:
+
+- `a` (`number`): Left numeric value.
+- `b` (`number`): Right numeric value.
+
+**Return**:
+
+- `isGreaterOrEqual` (`boolean`): True when `a >= b`.
+
+**Example**:
+
+```lua
+ge(2, 2) --> true
+```
+
+
+
+#### `gt(a, b)`
+
+Check whether `a` is strictly greater than `b`.
+
+**Parameters**:
+
+- `a` (`number`): Left numeric value.
+- `b` (`number`): Right numeric value.
+
+**Return**:
+
+- `isGreater` (`boolean`): True when `a > b`.
+
+**Example**:
+
+```lua
+gt(3, 2) --> true
+```
+
+
+
+#### `le(a, b)`
+
+Check whether `a` is less than or equal to `b`.
+
+**Parameters**:
+
+- `a` (`number`): Left numeric value.
+- `b` (`number`): Right numeric value.
+
+**Return**:
+
+- `isLessOrEqual` (`boolean`): True when `a <= b`.
+
+**Example**:
+
+```lua
+le(2, 2) --> true
+```
+
+
+
+#### `lt(a, b)`
+
+Check whether `a` is strictly less than `b`.
+
+**Parameters**:
+
+- `a` (`number`): Left numeric value.
+- `b` (`number`): Right numeric value.
+
+**Return**:
+
+- `isLess` (`boolean`): True when `a < b`.
+
+**Example**:
+
+```lua
+lt(1, 2) --> true
+```
+
+
+
+#### `neq(a, b)`
+
+Check whether two values are not equal.
+
+**Parameters**:
+
+- `a` (`any`): Left value.
+- `b` (`any`): Right value.
+
+**Return**:
+
+- `isNotEqual` (`boolean`): True when `a ~= b`.
+
+**Example**:
+
+```lua
+neq(1, 2) --> true
+```
+
+### Logical
+
+
+
+#### `land(a, b)`
+
+Evaluate `a and b` with Lua short-circuit semantics.
+
+**Parameters**:
+
+- `a` (`T1`): First operand.
+- `b` (`T2`): Second operand.
+
+**Return**:
+
+- `andValue` (`T1|T2`): Result of `a and b`.
+
+**Example**:
+
+```lua
+land(true, false) --> false
+```
+
+
+
+#### `lnot(a)`
+
+Return the boolean negation of `a`.
+
+**Parameters**:
+
+- `a` (`any`): Input value.
+
+**Return**:
+
+- `isNot` (`boolean`): Result of `not a`.
+
+**Example**:
+
+```lua
+lnot(true) --> false
+```
+
+
+
+#### `lor(a, b)`
+
+Evaluate `a or b` with Lua short-circuit semantics.
+
+**Parameters**:
+
+- `a` (`T1`): First operand.
+- `b` (`T2`): Second operand.
+
+**Return**:
+
+- `orValue` (`T1|T2`): Result of `a or b`.
+
+**Example**:
+
+```lua
+lor(false, true) --> true
+```
+
+### String & Length
+
+
+
+#### `concat(a, b)`
+
+Concatenate two strings.
+
+**Parameters**:
+
+- `a` (`string`): Left string.
+- `b` (`string`): Right string.
+
+**Return**:
+
+- `concatenated` (`string`): Concatenated result `a .. b`.
+
+**Example**:
+
+```lua
+concat("a", "b") --> "ab"
+```
+
+
+
+#### `len(a)`
+
+Return the length of a string or table using Lua's `#` operator.
+
+**Parameters**:
+
+- `a` (`string|table`): Value supporting Lua's `#` operator.
+
+**Return**:
+
+- `length` (`integer`): Length computed by `#a`.
+
+**Example**:
+
+```lua
+len("abc") --> 3
+```
+
+### Tables & Calls
+
+
+
+#### `call(f, ...)`
+
+Call a function with variadic arguments and return its result.
+
+**Parameters**:
+
+- `f` (`fun(...:T1):T2`): Function to call.
+- `...` (`T1`): Additional arguments.
+
+**Return**:
+
+- `callResult` (`T2`): Return value(s) from `f(...)`.
+
+**Example**:
+
+```lua
+call(math.max, 1, 2) --> 2
+```
+
+
+
+#### `index(t, k)`
+
+Return the value at key/index `k` in table `t`.
+
+**Parameters**:
+
+- `t` (`table`): Source table.
+- `k` (`T`): Key/index value.
+
+**Return**:
+
+- `indexedValue` (`T`): Value stored at `t[k]`.
+
+**Example**:
+
+```lua
+index({ a = 1 }, "a") --> 1
+```
+
+
+
+#### `setindex(t, k, v)`
+
+Set `t[k] = v` and return the assigned value.
+
+**Parameters**:
+
+- `t` (`table`): Target table.
+- `k` (`any`): Key/index value.
+- `v` (`T`): Value to set.
+
+**Return**:
+
+- `assignedValue` (`T`): Assigned value `v`.
+
+**Example**:
+
+```lua
+setindex({}, "a", 1) --> 1
+```
diff --git a/docs/api/path.md b/docs/api/path.md
new file mode 100644
index 0000000..1e52564
--- /dev/null
+++ b/docs/api/path.md
@@ -0,0 +1,847 @@
+---
+description: "Cross-platform path operations with host-platform semantics."
+---
+
+# `path`
+
+Cross-platform path operations with host-platform semantics.
+
+## Usage
+
+```lua
+path = require "mods.path"
+
+print(path.join("src", "mods", "path.lua")) --> "src/mods/path.lua"
+print(path.normpath("a//b/./c")) --> "a/b/c"
+print(path.splitext("archive.tar.gz")) --> "archive.tar", ".gz"
+```
+
+## Functions
+
+| Function | Description |
+| ------------------------------------------------------- | ---------------------------- |
+| [`_splitext(path, sep, altsep?, extsep)`](#fn-splitext) | Split extension from a path. |
+
+**Anchors**:
+
+| Function | Description |
+| ---------------------------- | ------------------------------------------- |
+| [`anchor(path)`](#fn-anchor) | Return drive and root combined. |
+| [`drive(path)`](#fn-drive) | Return drive prefix when present. |
+| [`root(path)`](#fn-root) | Return root separator segment when present. |
+
+**Components**:
+
+| Function | Description |
+| -------------------------------- | ------------------------------------------------------------- |
+| [`parents(path)`](#fn-parents) | Return logical parent paths from nearest to farthest. |
+| [`parts(path)`](#fn-parts) | Split path into logical parts, including anchor when present. |
+| [`stem(path)`](#fn-stem) | Return filename without its final suffix. |
+| [`suffixes(path)`](#fn-suffixes) | Return all filename suffixes in order. |
+
+**Conversions**:
+
+| Function | Description |
+| -------------------------------- | --------------------------------------------------- |
+| [`as_posix(path)`](#fn-as-posix) | Convert backslashes (`\`) to forward slashes (`/`). |
+| [`as_uri(path)`](#fn-as-uri) | Convert a local path to a `file://` URI. |
+| [`from_uri(uri)`](#fn-from-uri) | Convert a `file://` URI to a local absolute path. |
+
+**Decomposition**:
+
+| Function | Description |
+| ------------------------------------ | -------------------------------------------------- |
+| [`basename(path)`](#fn-basename) | Return final path component. |
+| [`dirname(path)`](#fn-dirname) | Return directory portion of a path. |
+| [`split(path)`](#fn-split) | Split path into directory head and tail component. |
+| [`splitdrive(path)`](#fn-splitdrive) | Split drive prefix from remainder. |
+| [`splitext(path)`](#fn-splitext) | Split path into a root and extension. |
+| [`splitroot(path)`](#fn-splitroot) | Split path into drive, root, and tail components. |
+
+**Derived**:
+
+| Function | Description |
+| ----------------------------------------- | ------------------------------------------------ |
+| [`abspath(path)`](#fn-abspath) | Return normalized absolute path. |
+| [`commonpath(paths)`](#fn-commonpath) | Return longest common sub-path from a path list. |
+| [`commonprefix(paths)`](#fn-commonprefix) | Return longest common leading string prefix. |
+| [`relpath(path, start?)`](#fn-relpath) | Return `path` relative to optional `start` path. |
+
+**Environment**:
+
+| Function | Description |
+| ------------------------------------ | ----------------------------------------------------------------------- |
+| `cwd` | Return the current working directory path. |
+| [`expanduser(path)`](#fn-expanduser) | Expand `~` home segment when available. |
+| [`expandvars(path)`](#fn-expandvars) | Expand vars in a path (`$VAR`/`${VAR}` everywhere, `%VAR%` on Windows). |
+| [`home()`](#fn-home) | Return the current user's home directory path. |
+
+**Normalization**:
+
+| Function | Description |
+| -------------------------------- | ---------------------------------------------------- |
+| [`isabs(path)`](#fn-isabs) | Return `true` when `path` is absolute. |
+| [`join(path, ...)`](#fn-join) | Join path components. |
+| [`normcase(s)`](#fn-normcase) | Normalize path case using the active path semantics. |
+| [`normpath(path)`](#fn-normpath) | Normalize separators and dot segments. |
+
+**Relations**:
+
+| Function | Description |
+| ------------------------------------------------------- | --------------------------------------------------------------------------------------- |
+| [`is_relative_to(path, other)`](#fn-is-relative-to) | Return `true` when `path` is under `other`. |
+| [`relative_to(path, other, walk_up?)`](#fn-relative-to) | Return `path` relative to `other`, or `nil` with an error when it is not under `other`. |
+| [`with_name(path, name)`](#fn-with-name) | Return a path with the final filename replaced. |
+| [`with_stem(path, stem)`](#fn-with-stem) | Return a path with the final filename stem replaced. |
+| [`with_suffix(path, suffix)`](#fn-with-suffix) | Return a path with the final filename suffix replaced. |
+
+
+
+### `_splitext(path, sep, altsep?, extsep)`
+
+Split extension from a path. **Parameters**:
+
+- `path` (`string`)
+- `sep` (`string`)
+- `altsep?` (`string`)
+- `extsep` (`string`)
+
+**Return**:
+
+- `root` (`string`)
+- `ext` (`string`)
+
+### Anchors
+
+
+
+#### `anchor(path)`
+
+Return drive and root combined.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `anchor` (`string`): Drive and root anchor.
+
+**Example**:
+
+```lua
+path.anchor("c:\\") --> "c:\\"
+```
+
+
+
+#### `drive(path)`
+
+Return drive prefix when present.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `drivePrefix` (`string`): Drive prefix.
+
+**Example**:
+
+```lua
+path.drive("c:a/b") --> "c:"
+path.drive("a/b") --> ""
+```
+
+
+
+#### `root(path)`
+
+Return root separator segment when present.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `rootSeparator` (`string`): Root separator segment.
+
+**Example**:
+
+```lua
+path.root("/tmp/a.txt") --> "/"
+path.root("c:/") --> "\\"
+path.root("a/b") --> ""
+```
+
+### Components
+
+
+
+#### `parents(path)`
+
+Return logical parent paths from nearest to farthest.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `parents` (`mods.List`): Ancestor paths from nearest to farthest.
+
+**Example**:
+
+```lua
+path.parents("a/b/c") --> {"a/b", "a", "."}
+path.parents("c:a/b") --> {"c:a", "c:"}
+```
+
+
+
+#### `parts(path)`
+
+Split path into logical parts, including anchor when present.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `paths` (`mods.List`): Path parts including anchor when present.
+
+**Example**:
+
+```lua
+path.parts("a/b.txt") --> {"a", "b.txt"}
+path.parts("/a/b") --> {"/", "a", "b"}
+path.parts("c:a\\b") --> {"c:", "a", "b"}
+```
+
+
+
+#### `stem(path)`
+
+Return filename without its final suffix.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `stem` (`string`): Filename stem.
+
+**Example**:
+
+```lua
+path.stem("archive.tar.gz") --> "archive.tar"
+path.stem("c:a/b") --> "b"
+```
+
+
+
+#### `suffixes(path)`
+
+Return all filename suffixes in order.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `suffixes` (`mods.List`): Filename suffixes.
+
+**Example**:
+
+```lua
+path.suffixes("archive.tar.gz") --> {".tar", ".gz"}
+path.suffixes("a/b") --> {}
+```
+
+### Conversions
+
+
+
+#### `as_posix(path)`
+
+Convert backslashes (`\`) to forward slashes (`/`).
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `posixPath` (`string`): POSIX-style path.
+
+**Example**:
+
+```lua
+path.as_posix("a\\b\\c") --> "a/b/c"
+```
+
+
+
+#### `as_uri(path)`
+
+Convert a local path to a `file://` URI.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `fileUri` (`string?`): File URI.
+- `err` (`string?`): Error message when conversion fails.
+
+**Example**:
+
+```lua
+path.as_uri("/home/user/report.txt") --> "file:///home/user/report.txt"
+path.as_uri("c:/a/b.c") --> "file:///c:/a/b.c"
+path.as_uri("/a/b%#c") --> "file:///a/b%25%23c"
+```
+
+
+
+#### `from_uri(uri)`
+
+Convert a `file://` URI to a local absolute path.
+
+**Parameters**:
+
+- `uri` (`string`): URI value.
+
+**Return**:
+
+- `path` (`string?`): Resolved absolute path.
+- `err` (`string?`): Error message when conversion fails.
+
+**Example**:
+
+```lua
+path.from_uri("file://localhost/tmp/a.txt") --> "/tmp/a.txt"
+```
+
+### Decomposition
+
+
+
+#### `basename(path)`
+
+Return final path component.
+
+**Parameters**:
+
+- `path` (`string`): Path to inspect.
+
+**Return**:
+
+- `basename` (`string`): Final path component.
+
+**Example**:
+
+```lua
+path.basename("/a/b.txt") --> "b.txt"
+path.basename([[C:\a\b.txt]]) --> "b.txt"
+```
+
+
+
+#### `dirname(path)`
+
+Return directory portion of a path.
+
+**Parameters**:
+
+- `path` (`string`): Path to inspect.
+
+**Return**:
+
+- `dirname` (`string`): Parent directory path.
+
+**Example**:
+
+```lua
+path.dirname("/a/b.txt") --> "/a"
+path.dirname([[C:\a\b.txt]]) --> [[C:\a]]
+```
+
+
+
+#### `split(path)`
+
+Split path into directory head and tail component.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `head` (`string`): Directory portion.
+- `tail` (`string`): Final path component.
+
+**Example**:
+
+```lua
+path.split("/a/b.txt") --> "/a", "b.txt"
+```
+
+
+
+#### `splitdrive(path)`
+
+Split drive prefix from remainder.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `drive` (`string`): Drive or share prefix when present.
+- `rest` (`string`): Path remainder.
+
+**Example**:
+
+```lua
+path.splitdrive("/a/b") --> "", "/a/b"
+```
+
+> [!NOTE]
+>
+> On POSIX semantics the drive portion is always empty.
+
+
+
+#### `splitext(path)`
+
+Split path into a root and extension.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `root` (`string`): Path without the final extension.
+- `ext` (`string`): Final extension including leading dot.
+
+**Example**:
+
+```lua
+path.splitext("archive.tar.gz") --> "archive.tar", ".gz"
+```
+
+
+
+#### `splitroot(path)`
+
+Split path into drive, root, and tail components.
+
+**Parameters**:
+
+- `path` (`string`): Path to split.
+
+**Return**:
+
+- `drive` (`string`): Drive or share prefix (empty on POSIX).
+- `root` (`string`): Root separator segment.
+- `tail` (`string`): Remaining path without leading root separator.
+
+**Example**:
+
+```lua
+path.splitroot("/a/b") --> "", "/", "a/b"
+path.splitroot([[C:\a\b]]) --> "C:", [[\]], "a\\b"
+```
+
+### Derived
+
+
+
+#### `abspath(path)`
+
+Return normalized absolute path.
+
+**Parameters**:
+
+- `path` (`string`): Path to absolutize.
+
+**Return**:
+
+- `absolutePath` (`string`): Absolute normalized path.
+
+**Example**:
+
+```lua
+path.abspath("/a/./b") --> "/a/b"
+path.abspath([[C:\a\..\b]]) --> [[C:\b]]
+```
+
+
+
+#### `commonpath(paths)`
+
+Return longest common sub-path from a path list.
+
+**Parameters**:
+
+- `paths` (`string[]`): List of paths.
+
+**Return**:
+
+- `commonPath` (`string?`): Longest common sub-path.
+- `err` (`string?`): Error message when inputs are incompatible.
+
+**Example**:
+
+```lua
+path.commonpath({ "/a/b/c", "/a/b/d" }) --> "/a/b"
+path.commonpath({ [[C:\a\b\c]], [[c:/a/b/d]] }) --> [[C:\a\b]]
+```
+
+
+
+#### `commonprefix(paths)`
+
+Return longest common leading string prefix.
+
+**Parameters**:
+
+- `paths` (`string[]`): List of paths.
+
+**Return**:
+
+- `commonPrefix` (`string`): Longest common string prefix.
+
+**Example**:
+
+```lua
+path.commonprefix({"abc", "abd"}) --> "ab"
+path.commonprefix({"/home/swen/spam", "/home/swen/eggs"}) --> "/home/swen/"
+path.commonprefix({"abc", "xyz"}) --> ""
+```
+
+
+
+#### `relpath(path, start?)`
+
+Return `path` relative to optional `start` path.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `start?` (`string`): Optional base path.
+
+**Return**:
+
+- `relativePath` (`string?`): Relative path from `start` to `path`.
+- `err` (`string?`): Error message when the path cannot be made relative.
+
+**Example**:
+
+```lua
+path.relpath("/a/b/c", "/a") --> "b/c"
+path.relpath([[C:\a\b\c]], [[C:\a]]) --> [[b\c]]
+```
+
+### Environment
+
+
+
+#### `cwd`
+
+Return the current working directory path.
+
+#### `expanduser(path)`
+
+Expand `~` home segment when available.
+
+**Parameters**:
+
+- `path` (`string`): Path that may begin with `~`.
+
+**Return**:
+
+- `expandedPath` (`string?`): Path with the home segment expanded when
+ available.
+- `err` (`string?`): Error message when `~` expansion cannot be resolved.
+
+**Example**:
+
+```lua
+path.expanduser("~/tmp") --> "/tmp" (when HOME is set)
+path.expanduser([[x\y]]) --> [[x\y]]
+```
+
+
+
+#### `expandvars(path)`
+
+Expand vars in a path (`$VAR`/`${VAR}` everywhere, `%VAR%` on Windows).
+
+**Parameters**:
+
+- `path` (`string`): Path containing variable placeholders.
+
+**Return**:
+
+- `expandedPath` (`string`): Path with variable values substituted.
+
+**Example**:
+
+```lua
+path.expandvars("$HOME/bin") --> "/home/me/bin"
+path.expandvars("${XDG_CONFIG_HOME}/nvim") --> "/home/me/.config/nvim"
+path.expandvars("%USERPROFILE%\\bin") --> "C:\\Users\\me\\bin"
+path.expandvars("$UNKNOWN/bin") --> "$UNKNOWN/bin"
+```
+
+
+
+#### `home()`
+
+Return the current user's home directory path.
+
+**Return**:
+
+- `homePath` (`string?`): Home directory path when available.
+- `err` (`string?`): Error message when the home directory cannot be resolved.
+
+**Example**:
+
+```lua
+path.home()
+```
+
+### Normalization
+
+
+
+#### `isabs(path)`
+
+Return `true` when `path` is absolute.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `isAbsolute` (`boolean`): True when `path` is absolute.
+
+**Example**:
+
+```lua
+path.isabs("/a/b") --> true
+```
+
+
+
+#### `join(path, ...)`
+
+Join path components.
+
+**Parameters**:
+
+- `path` (`string`): Base path component.
+- `...` (`string`): Additional path components.
+
+**Return**:
+
+- `joinedPath` (`string`): Joined path.
+
+**Example**:
+
+```lua
+path.join("/usr", "bin") --> "/usr/bin"
+path.join([[C:/a]], [[b]]) --> [[C:/a\b]]
+```
+
+> [!NOTE]
+>
+> Single input is returned as-is.
+
+
+
+#### `normcase(s)`
+
+Normalize path case using the active path semantics.
+
+**Parameters**:
+
+- `s` (`string`): Input path value.
+
+**Return**:
+
+- `normalizedPath` (`string`): Path after case normalization.
+
+**Example**:
+
+```lua
+path.normcase("ABC") --> "abc"
+path.normcase("/A/B") --> "\\a\\b"
+```
+
+> [!NOTE]
+>
+> On POSIX semantics this returns the input unchanged. Use `mods.ntpath` to
+> force Windows-style case folding and separator normalization.
+
+
+
+#### `normpath(path)`
+
+Normalize separators and dot segments.
+
+**Parameters**:
+
+- `path` (`string`): Path to normalize.
+
+**Return**:
+
+- `normalizedPath` (`string`): Normalized path.
+
+**Example**:
+
+```lua
+path.normpath("/a//./b/..") --> "/a"
+path.normpath([[A/foo/../B]]) --> [[A\B]]
+```
+
+### Relations
+
+
+
+#### `is_relative_to(path, other)`
+
+Return `true` when `path` is under `other`.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `other` (`string`): Reference path.
+
+**Return**:
+
+- `isRelative` (`boolean`): True when `path` is under `other`.
+
+**Example**:
+
+```lua
+path.is_relative_to("a/b/c", "a/b") --> true
+path.is_relative_to("C:A/B", "c:a") --> true
+path.is_relative_to("a/b", "a/b/c") --> false
+```
+
+
+
+#### `relative_to(path, other, walk_up?)`
+
+Return `path` relative to `other`, or `nil` with an error when it is not under
+`other`.
+
+When `walk_up` is `true`, allow `..` segments to walk up to a shared prefix.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `other` (`string`): Reference path.
+- `walk_up?` (`boolean`): Allow walking up to a shared prefix.
+
+**Return**:
+
+- `relativePath` (`string?`): Path relative to `other`, or `nil` on error.
+- `err` (`string?`): Error message when the path cannot be made relative.
+
+**Example**:
+
+```lua
+path.relative_to("/a/b/c.txt", "/a") --> "b/c.txt"
+path.relative_to("/a/b", "/a/c", true) --> "../b"
+path.relative_to("/a/b", "/a/x") --> nil, "'/a/b' is not in the subpath of '/a/x'"
+```
+
+
+
+#### `with_name(path, name)`
+
+Return a path with the final filename replaced.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `name` (`string`): Replacement filename.
+
+**Return**:
+
+- `updatedPath` (`string?`): Path with replaced filename, or `nil` on error.
+- `err` (`string?`): Error message when replacement fails.
+
+**Example**:
+
+```lua
+path.with_name("a/b", "c.txt") --> "a/c.txt"
+path.with_name("a/b.txt", "c.lua") --> "a/c.lua"
+path.with_name("a/b", "c/d") --> nil, "invalid name 'c/d'"
+path.with_name("/", "d.xml") --> nil, "'/' has an empty name"
+```
+
+
+
+#### `with_stem(path, stem)`
+
+Return a path with the final filename stem replaced.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `stem` (`string`): Replacement filename stem.
+
+**Return**:
+
+- `updatedPath` (`string?`): Path with replaced filename stem, or `nil` on
+ error.
+- `err` (`string?`): Error message when replacement fails.
+
+**Example**:
+
+```lua
+path.with_stem("a/b", "d") --> "/a/d"
+path.with_stem("a/b.lua", "d") --> "/a/d.lua"
+path.with_stem("/", "d") --> "'/' has an empty name"
+path.with_stem("a/b", "d") --> "invalid name ''."
+```
+
+
+
+#### `with_suffix(path, suffix)`
+
+Return a path with the final filename suffix replaced.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `suffix` (`string`): Replacement suffix.
+
+**Return**:
+
+- `updatedPath` (`string?`): Path with replaced suffix, or `nil` on error.
+- `err` (`string?`): Error message when replacement fails.
+
+**Example**:
+
+```lua
+path.with_suffix("a/b", ".gz") --> "a/b/.gz"
+path.with_suffix("a/b.gz", ".lua") --> "a/b/.lua"
+path.with_suffix("a/b", "gz") --> nil, "invalid suffix 'gz'"
+path.with_suffix("//a/b", "gz") --> nil, "'//a/b' has an empty name"
+```
diff --git a/docs/api/posixpath.md b/docs/api/posixpath.md
new file mode 100644
index 0000000..31d90cc
--- /dev/null
+++ b/docs/api/posixpath.md
@@ -0,0 +1,22 @@
+---
+description: "POSIX-style path operations."
+---
+
+# `posixpath`
+
+POSIX-style path operations.
+
+> 💡 Python `posixpath`-style behavior, ported to Lua.
+
+## Usage
+
+```lua
+posixpath = require "mods.posixpath"
+
+print(posixpath.join("/usr", "bin")) --> "/usr/bin"
+print(posixpath.normpath("/a//./b/..")) --> "/a"
+print(posixpath.splitext("archive.tar.gz")) --> "archive.tar", ".gz"
+print(posixpath.relpath("/usr/local/bin", "/usr")) --> "local/bin"
+```
+
+> ✨ Same API as `mods.path`, but with POSIX path semantics.
diff --git a/docs/api/runtime.md b/docs/api/runtime.md
new file mode 100644
index 0000000..805d576
--- /dev/null
+++ b/docs/api/runtime.md
@@ -0,0 +1,131 @@
+---
+description: "Lua runtime metadata and version compatibility flags."
+---
+
+# `runtime`
+
+Lua runtime metadata and version compatibility flags.
+
+## Usage
+
+```lua
+runtime = require "mods.runtime"
+
+print(runtime.version) --> 501 | 502 | 503 | 504 | 505
+print(runtime.is_lua55) --> true | false
+```
+
+## Fields
+
+| Field | Description |
+| --------------------------- | ------------------------------------------------- |
+| [`is_lua51`](#is-lua51) | True only on Lua 5.1 runtimes. |
+| [`is_lua52`](#is-lua52) | True only on Lua 5.2 runtimes. |
+| [`is_lua53`](#is-lua53) | True only on Lua 5.3 runtimes. |
+| [`is_lua54`](#is-lua54) | True only on Lua 5.4 runtimes. |
+| [`is_lua55`](#is-lua55) | True only on Lua 5.5 runtimes. |
+| [`is_luajit`](#is-luajit) | True when running under LuaJIT. |
+| [`is_windows`](#is-windows) | True when running on a Windows host. |
+| [`major`](#major) | Major version number parsed from `version`. |
+| [`minor`](#minor) | Minor version number parsed from `version`. |
+| [`version`](#version) | Numeric version encoded as `major * 100 + minor`. |
+
+
+
+### `is_lua51` (`boolean`)
+
+True only on Lua 5.1 runtimes.
+
+```lua
+print(runtime.is_lua51) --> true | false
+```
+
+
+
+### `is_lua52` (`boolean`)
+
+True only on Lua 5.2 runtimes.
+
+```lua
+print(runtime.is_lua52) --> true | false
+```
+
+
+
+### `is_lua53` (`boolean`)
+
+True only on Lua 5.3 runtimes.
+
+```lua
+print(runtime.is_lua53) --> true | false
+```
+
+
+
+### `is_lua54` (`boolean`)
+
+True only on Lua 5.4 runtimes.
+
+```lua
+print(runtime.is_lua54) --> true | false
+```
+
+
+
+### `is_lua55` (`boolean`)
+
+True only on Lua 5.5 runtimes.
+
+```lua
+print(runtime.is_lua55) --> true | false
+```
+
+
+
+### `is_luajit` (`boolean`)
+
+True when running under LuaJIT.
+
+```lua
+print(runtime.is_luajit) --> true | false
+```
+
+
+
+### `is_windows` (`boolean`)
+
+True when running on a Windows host.
+
+```lua
+print(runtime.is_windows) --> true | false
+```
+
+
+
+### `major` (`5`)
+
+Major version number parsed from `version`.
+
+```lua
+print(runtime.major) --> 5
+```
+
+
+
+### `minor` (`1|2|3|4|5`)
+
+Minor version number parsed from `version`.
+
+```lua
+print(runtime.minor) --> 1 | 2 | 3 | 4 | 5
+```
+
+
+
+### `version` (`501|502|503|504|505`)
+
+Numeric version encoded as `major * 100 + minor`.
+
+```lua
+print(runtime.version) --> 501 | 502 | 503 | 504 | 505
+```
diff --git a/docs/api/set.md b/docs/api/set.md
new file mode 100644
index 0000000..84124f5
--- /dev/null
+++ b/docs/api/set.md
@@ -0,0 +1,843 @@
+---
+description: "A set class for creating, combining, and querying unique values."
+---
+
+# `set`
+
+A set class for creating, combining, and querying unique values.
+
+## Usage
+
+```lua
+Set = require "mods.Set"
+
+s = Set({ "a" })
+print(s:contains("a")) --> true
+```
+
+## Functions
+
+**Mutation**:
+
+| Function | Description |
+| --------------------------------------------------------------------- | ----------------------------------------------------------- |
+| [`add(v)`](#fn-add) | Add an element to the set. |
+| [`clear()`](#fn-clear) | Remove all elements from the set. |
+| [`difference_update(set)`](#fn-difference-update) | Remove elements found in another set (in place). |
+| [`intersection_update(set)`](#fn-intersection-update) | Keep only elements common to both sets (in place). |
+| [`pop()`](#fn-pop) | Remove and return an arbitrary element. |
+| [`symmetric_difference_update(set)`](#fn-symmetric-difference-update) | Update the set with elements not shared by both (in place). |
+| [`update(set)`](#fn-update) | Add all elements from another set (in place). |
+
+**Predicates**:
+
+| Function | Description |
+| ----------------------------------- | ---------------------------------------------------------------- |
+| [`equals(t)`](#fn-equals) | Return true when both sets contain exactly the same members. |
+| [`isdisjoint(set)`](#fn-isdisjoint) | Return true if sets have no elements in common. |
+| [`isempty()`](#fn-isempty) | Return true if the set has no elements. |
+| [`issubset(t)`](#fn-issubset) | Return true if all elements of this set are also in another set. |
+| [`issuperset(t)`](#fn-issuperset) | Return true if this set contains all elements of another set. |
+
+**Queries**:
+
+| Function | Description |
+| ----------------------------- | ----------------------------------------- |
+| [`contains(v)`](#fn-contains) | Return true if the set contains `v`. |
+| [`len()`](#fn-len) | Return the number of elements in the set. |
+
+**Set Operations**:
+
+| Function | Description |
+| ----------------------------------------------------- | ---------------------------------------------------------------------------- |
+| [`copy()`](#fn-copy) | Return a shallow copy of the set. |
+| [`difference(t)`](#fn-difference) | Return elements in this set but not in another. |
+| [`has(v)`](#fn-has) | Check whether a value is present in the set without following the metatable. |
+| [`intersection(t)`](#fn-intersection) | Return elements common to both sets. |
+| [`remove(v)`](#fn-remove) | Remove an element if present, do nothing otherwise. |
+| [`symmetric_difference(t)`](#fn-symmetric-difference) | Return elements not shared by both sets. |
+| [`union(t)`](#fn-union) | Return a new set with all elements from both. |
+
+**Transforms**:
+
+| Function | Description |
+| --------------------------------- | ------------------------------------------------------- |
+| [`join(sep?, quoted?)`](#fn-join) | Join set values into a string. |
+| [`map(fn)`](#fn-map) | Return a new set by mapping each value. |
+| [`mirror()`](#fn-mirror) | Mirror values into a new table as both keys and values. |
+| [`tostring()`](#fn-tostring) | Render the set as a string. |
+| [`values()`](#fn-values) | Return a list of all values in the set. |
+
+**Metamethods**:
+
+| Function | Description |
+| ------------------------------ | -------------------------------------------------------------------------- |
+| [`__add(t)`](#fn-add) | Return the union of two sets using `+`. |
+| [`__band(t)`](#fn-band) | Return the intersection of two sets using `&`. |
+| [`__bor(t)`](#fn-bor) | Return the union of two sets using `\|`. |
+| [`__bxor(t)`](#fn-bxor) | Return elements present in exactly one set using `^`. |
+| [`__eq(t)`](#fn-eq) | Return true if both sets contain exactly the same members using `==`. |
+| [`__le(t)`](#fn-le) | Return true if the left set is a subset of the right set using `<=`. |
+| [`__lt(set)`](#fn-lt) | Return true if the left set is a proper subset of the right set using `<`. |
+| [`__sub(set)`](#fn-sub) | Return the difference of two sets using `-`. |
+| [`__tostring()`](#fn-tostring) | Render the set via `tostring(set)`. |
+
+### Mutation
+
+
+
+#### `add(v)`
+
+Add an element to the set.
+
+**Parameters**:
+
+- `v` (`any`): Value to add.
+
+**Return**:
+
+- `self` (`T`): Current set.
+
+**Example**:
+
+```lua
+s = Set({ "a" }):add("b") --> s contains "a", "b"
+```
+
+
+
+#### `clear()`
+
+Remove all elements from the set.
+
+**Return**:
+
+- `self` (`T`): Current set.
+
+**Example**:
+
+```lua
+s = Set({ "a", "b" }):clear() --> s is empty
+```
+
+
+
+#### `difference_update(set)`
+
+Remove elements found in another set (in place).
+
+**Parameters**:
+
+- `set` (`T|mods.List`): Other set/list.
+
+**Return**:
+
+- `self` (`T`): Current set.
+
+**Example**:
+
+```lua
+s = Set({ "a", "b" }):difference_update(Set({ "b" })) --> s contains "a"
+```
+
+
+
+#### `intersection_update(set)`
+
+Keep only elements common to both sets (in place).
+
+**Parameters**:
+
+- `set` (`T|mods.List`): Other set/list.
+
+**Return**:
+
+- `self` (`T`): Current set.
+
+**Example**:
+
+```lua
+s = Set({ "a", "b" }):intersection_update(Set({ "b", "c" }))
+--> s contains "b"
+```
+
+
+
+#### `pop()`
+
+Remove and return an arbitrary element.
+
+**Return**:
+
+- `removedValue` (`any`): Removed value, or `nil` when the set is empty.
+
+**Example**:
+
+```lua
+v = Set({ "a", "b" }):pop() --> v is either "a" or "b"
+```
+
+
+
+#### `symmetric_difference_update(set)`
+
+Update the set with elements not shared by both (in place).
+
+**Parameters**:
+
+- `set` (`T|mods.List`): Other set/list.
+
+**Return**:
+
+- `self` (`T`): Current set.
+
+**Example**:
+
+```lua
+s = Set({ "a", "b" }):symmetric_difference_update(Set({ "b", "c" }))
+--> s contains "a", "c"
+```
+
+
+
+#### `update(set)`
+
+Add all elements from another set (in place).
+
+**Parameters**:
+
+- `set` (`T|mods.List`): Other set/list.
+
+**Return**:
+
+- `self` (`T`): Current set.
+
+**Example**:
+
+```lua
+s = Set({ "a" }):update(Set({ "b" })) --> s contains "a", "b"
+```
+
+### Predicates
+
+
+
+#### `equals(t)`
+
+Return true when both sets contain exactly the same members.
+
+**Parameters**:
+
+- `t` (`mods.Set|mods.List|table`): Other set/list.
+
+**Return**:
+
+- `isEqual` (`boolean`): True when both sets contain the same members.
+
+**Example**:
+
+```lua
+a = Set({ "a", "b" })
+b = Set({ "b", "a" })
+ok = a:equals(b) --> true
+```
+
+> [!NOTE]
+>
+> `equals` is also available as the `__eq` (`==`) operator. `a:equals(b)` is
+> equivalent to `a == b`.
+
+
+
+#### `isdisjoint(set)`
+
+Return true if sets have no elements in common.
+
+**Parameters**:
+
+- `set` (`T|mods.List`): Other set/list.
+
+**Return**:
+
+- `isDisjoint` (`boolean`): True when sets have no elements in common.
+
+**Example**:
+
+```lua
+ok = Set({ "a" }):isdisjoint(Set({ "b" })) --> true
+```
+
+
+
+#### `isempty()`
+
+Return true if the set has no elements.
+
+**Return**:
+
+- `isEmpty` (`boolean`): True when the set has no elements.
+
+**Example**:
+
+```lua
+empty = Set({}):isempty() --> true
+```
+
+
+
+#### `issubset(t)`
+
+Return true if all elements of this set are also in another set.
+
+**Parameters**:
+
+- `t` (`mods.Set|mods.List|table`): Other set/list.
+
+**Return**:
+
+- `isSubset` (`boolean`): True when every element of `self` exists in `set`.
+
+**Example**:
+
+```lua
+ok = Set({ "a" }):issubset(Set({ "a", "b" })) --> true
+```
+
+> [!NOTE]
+>
+> `issubset` is also available as the `__le` (`<=`) operator. `a:issubset(b)` is
+> equivalent to `a <= b`.
+
+
+
+#### `issuperset(t)`
+
+Return true if this set contains all elements of another set.
+
+**Parameters**:
+
+- `t` (`mods.Set|mods.List|table`): Other set/list.
+
+**Return**:
+
+- `isSuperset` (`boolean`): True when `self` contains every element of `set`.
+
+**Example**:
+
+```lua
+ok = Set({ "a", "b" }):issuperset(Set({ "a" })) --> true
+```
+
+### Queries
+
+
+
+#### `contains(v)`
+
+Return true if the set contains `v`.
+
+**Parameters**:
+
+- `v` (`any`): Value to check.
+
+**Return**:
+
+- `isPresent` (`boolean`): True when `v` is present in the set.
+
+**Example**:
+
+```lua
+ok = Set({ "a", "b" }):contains("a") --> true
+ok = Set({ "a", "b" }):contains("z") --> false
+```
+
+
+
+#### `len()`
+
+Return the number of elements in the set.
+
+**Return**:
+
+- `count` (`integer`): Element count.
+
+**Example**:
+
+```lua
+n = Set({ "a", "b" }):len() --> 2
+```
+
+### Set Operations
+
+
+
+#### `copy()`
+
+Return a shallow copy of the set.
+
+**Return**:
+
+- `set` (`mods.Set`): New set.
+
+**Example**:
+
+```lua
+c = Set({ "a" }):copy() --> c is a new set with "a"
+```
+
+
+
+#### `difference(t)`
+
+Return elements in this set but not in another.
+
+**Parameters**:
+
+- `t` (`mods.Set|mods.List|table`): Other set/list.
+
+**Return**:
+
+- `set` (`mods.Set`): New set.
+
+**Example**:
+
+```lua
+d = Set({ "a", "b" }):difference(Set({ "b" })) --> d contains "a"
+```
+
+> [!NOTE]
+>
+> `difference` is also available as the `__sub` (`-`) operator.
+> `a:difference(b)` is equivalent to `a - b`.
+
+
+
+#### `has(v)`
+
+Check whether a value is present in the set without following the metatable.
+
+**Parameters**:
+
+- `v` (`any`): Value to look up.
+
+**Return**:
+
+- `present` (`boolean`): Whether the value is present.
+
+**Example**:
+
+```lua
+s = Set({ "a", "b", "c" })
+print(s:has("a")) --> true
+print(s:has("__index")) --> false
+```
+
+
+
+#### `intersection(t)`
+
+Return elements common to both sets.
+
+**Parameters**:
+
+- `t` (`mods.Set|mods.List|table`): Other set/list.
+
+**Return**:
+
+- `set` (`mods.Set`): New set.
+
+**Example**:
+
+```lua
+i = Set({ "a", "b" }):intersection(Set({ "b", "c" })) --> i contains "b"
+```
+
+> [!NOTE]
+>
+> `intersection` is also available as `__band` (`&`) on Lua 5.3+.
+
+
+
+#### `remove(v)`
+
+Remove an element if present, do nothing otherwise.
+
+**Parameters**:
+
+- `v` (`any`): Value to remove.
+
+**Return**:
+
+- `self` (`T`): Current set.
+
+**Example**:
+
+```lua
+s = Set({ "a", "b" }):remove("b") --> s contains "a"
+```
+
+
+
+#### `symmetric_difference(t)`
+
+Return elements not shared by both sets.
+
+**Parameters**:
+
+- `t` (`mods.Set|mods.List|table`): Other set/list.
+
+**Return**:
+
+- `set` (`mods.Set`): New set.
+
+**Example**:
+
+```lua
+d = Set({ "a", "b" }):symmetric_difference(Set({ "b", "c" }))
+--> d contains "a", "c"
+```
+
+> [!NOTE]
+>
+> `symmetric_difference` is also available as `__bxor` (`^`) on Lua 5.3+.
+
+
+
+#### `union(t)`
+
+Return a new set with all elements from both.
+
+**Parameters**:
+
+- `t` (`mods.Set|mods.List|table`): Other set/list.
+
+**Return**:
+
+- `set` (`mods.Set`): New set.
+
+**Example**:
+
+```lua
+s = Set({ "a" }):union(Set({ "b" })) --> s contains "a", "b"
+```
+
+> [!NOTE]
+>
+> `union` is available as `__add` (`+`) and `__bor` (`|`) on Lua 5.3+.
+> `a:union(b)` is equivalent to `a + b` and `a | b`.
+
+### Transforms
+
+
+
+#### `join(sep?, quoted?)`
+
+Join set values into a string.
+
+**Parameters**:
+
+- `sep?` (`string`): Optional separator value (defaults to `""`).
+- `quoted?` (`boolean`): Optional boolean flag (defaults to `false`).
+
+**Return**:
+
+- `joined` (`string`): Joined string.
+
+**Example**:
+
+```lua
+s = Set({ "b", "a" }):join(", ") --> "a, b"
+s = Set({ "b", "a" }):join(", ", true) --> '"a", "b"'
+```
+
+> [!NOTE]
+>
+> Join order is not guaranteed.
+
+
+
+#### `map(fn)`
+
+Return a new set by mapping each value.
+
+**Parameters**:
+
+- `fn` (`fun(v:any):any`): Mapping function.
+
+**Return**:
+
+- `set` (`mods.Set`): New set.
+
+**Example**:
+
+```lua
+s = Set({ 1, 2 }):map(function(v) return v * 10 end) --> s contains 10, 20
+```
+
+
+
+#### `mirror()`
+
+Mirror values into a new table as both keys and values.
+
+**Return**:
+
+- `mirroredValues` (`table`): Table mapping each value to itself.
+
+**Example**:
+
+```lua
+mirrored = Set({ "a", "b" }):mirror() --> { a = "a", b = "b" }
+```
+
+
+
+#### `tostring()`
+
+Render the set as a string.
+
+**Return**:
+
+- `renderedSet` (`string`): Rendered set string.
+
+**Example**:
+
+```lua
+s = Set({ "b", "a", 1 }):tostring() --> '{ 1, "a", "b" }'
+```
+
+
+
+#### `values()`
+
+Return a list of all values in the set.
+
+**Return**:
+
+- `values` (`mods.List`): List of set values.
+
+**Example**:
+
+```lua
+values = Set({ "a", "b" }):values() --> { "a", "b" }
+```
+
+### Metamethods
+
+
+
+#### `__add(t)`
+
+Return the union of two sets using `+`.
+
+**Parameters**:
+
+- `t` (`mods.Set|mods.List|table`): Other set/list.
+
+**Return**:
+
+- `set` (`mods.Set`): New set.
+
+**Example**:
+
+```lua
+a = Set({ "a", "b" })
+b = Set({ "b", "c" })
+u = a + b --> { a = true, b = true, c = true }
+```
+
+> [!NOTE]
+>
+> `__add` is the operator form of `:union(set)`.
+
+
+
+#### `__band(t)`
+
+Return the intersection of two sets using `&`.
+
+**Parameters**:
+
+- `t` (`mods.Set|mods.List|table`): Other set/list.
+
+**Return**:
+
+- `set` (`mods.Set`): New set.
+
+**Example**:
+
+```lua
+a = Set({ "a", "b" })
+b = Set({ "b", "c" })
+i = a & b --> { b = true }
+```
+
+> [!NOTE]
+>
+> `__band` is the operator form of `:intersection(set)` on Lua 5.3+.
+
+
+
+#### `__bor(t)`
+
+Return the union of two sets using `|`.
+
+**Parameters**:
+
+- `t` (`mods.Set|mods.List|table`): Other set/list.
+
+**Return**:
+
+- `set` (`mods.Set`): New set.
+
+**Example**:
+
+```lua
+a = Set({ "a", "b" })
+b = Set({ "b", "c" })
+u = a | b --> { a = true, b = true, c = true }
+```
+
+> [!NOTE]
+>
+> `__bor` is the operator form of `:union(set)` on Lua 5.3+.
+
+
+
+#### `__bxor(t)`
+
+Return elements present in exactly one set using `^`.
+
+**Parameters**:
+
+- `t` (`mods.Set|mods.List|table`): Other set/list.
+
+**Return**:
+
+- `set` (`mods.Set`): New set.
+
+**Example**:
+
+```lua
+a = Set({ "a", "b" })
+b = Set({ "b", "c" })
+d = a ^ b --> { a = true, c = true }
+```
+
+> [!NOTE]
+>
+> `__bxor` is the operator form of `:symmetric_difference(set)` on Lua 5.3+.
+
+
+
+#### `__eq(t)`
+
+Return true if both sets contain exactly the same members using `==`.
+
+**Parameters**:
+
+- `t` (`mods.Set|mods.List|table`): Other set/list.
+
+**Return**:
+
+- `isEqual` (`boolean`): True when both sets contain the same members.
+
+**Example**:
+
+```lua
+ok = Set({ "a", "b" }) == Set({ "b", "a" }) --> true
+```
+
+> [!NOTE]
+>
+> `__eq` is the operator form of `:equals(set)`.
+
+
+
+#### `__le(t)`
+
+Return true if the left set is a subset of the right set using `<=`.
+
+**Parameters**:
+
+- `t` (`mods.Set|mods.List|table`): Other set/list.
+
+**Return**:
+
+- `isSubset` (`boolean`): True when `self` is a subset of `set`.
+
+**Example**:
+
+```lua
+a = Set({ "a" })
+b = Set({ "a", "b" })
+ok = a <= b --> true
+```
+
+> [!NOTE]
+>
+> `__le` is the operator form of `:issubset(set)`.
+
+
+
+#### `__lt(set)`
+
+Return true if the left set is a proper subset of the right set using `<`.
+
+**Parameters**:
+
+- `set` (`mods.Set|table`): Other set.
+
+**Return**:
+
+- `isProperSubset` (`boolean`): True when `self` is a proper subset of `set`.
+
+**Example**:
+
+```lua
+a = Set({ "a" })
+b = Set({ "a", "b" })
+ok = a < b --> true
+```
+
+
+
+#### `__sub(set)`
+
+Return the difference of two sets using `-`.
+
+**Parameters**:
+
+- `set` (`mods.Set|table`): Other set.
+
+**Return**:
+
+- `set` (`mods.Set`): New set.
+
+**Example**:
+
+```lua
+a = Set({ "a", "b" })
+b = Set({ "b", "c" })
+d = a - b --> { a = true }
+```
+
+> [!NOTE]
+>
+> `__sub` is the operator form of `:difference(set)`.
+
+
+
+#### `__tostring()`
+
+Render the set via `tostring(set)`.
+
+**Return**:
+
+- `renderedSet` (`string`): Rendered set string.
+
+**Example**:
+
+```lua
+s = tostring(Set({ "b", "a", 1 })) --> '{ 1, "a", "b" }'
+```
diff --git a/docs/api/str.md b/docs/api/str.md
new file mode 100644
index 0000000..7a5091e
--- /dev/null
+++ b/docs/api/str.md
@@ -0,0 +1,968 @@
+---
+description:
+ "String operations for searching, splitting, trimming, and formatting text."
+---
+
+# `str`
+
+String operations for searching, splitting, trimming, and formatting text.
+
+## Usage
+
+```lua
+str = require "mods.str"
+
+print(str.capitalize("hello world")) --> "Hello world"
+```
+
+## Functions
+
+**Casing & Transform**:
+
+| Function | Description |
+| -------------------------------------------------------- | --------------------------------------------------------- |
+| [`startswith(s, prefix, start?, stop?)`](#fn-startswith) | Return true if string starts with prefix. |
+| [`swapcase(s)`](#fn-swapcase) | Return a copy with case of alphabetic characters swapped. |
+| [`title(s)`](#fn-title) | Return titlecased copy. |
+| [`translate(s, table_map)`](#fn-translate) | Translate characters using a mapping table. |
+| [`upper(s)`](#fn-upper) | Return uppercased copy. |
+| [`zfill(s, width)`](#fn-zfill) | Pad numeric string on the left with zeros. |
+
+**Formatting**:
+
+| Function | Description |
+| ---------------------------------------------------- | --------------------------------------------------------------------- |
+| [`capitalize(s)`](#fn-capitalize) | Return copy with first character capitalized and the rest lowercased. |
+| [`center(s, width, fillchar?)`](#fn-center) | Center string within width, padded with fill characters. |
+| [`count(s, sub, start?, stop?)`](#fn-count) | Count non-overlapping occurrences of a substring. |
+| [`endswith(s, suffix, start?, stop?)`](#fn-endswith) | Return true if string ends with suffix. |
+| [`expandtabs(s, tabsize?)`](#fn-expandtabs) | Expand tabs to spaces using given tabsize. |
+| [`find(s, sub, start?, stop?)`](#fn-find) | Return lowest index of substring or nil if not found. |
+| [`format_map(s, mapping)`](#fn-format-map) | Format string with mapping (key-based) replacement. |
+
+**Layout**:
+
+| Function | Description |
+| ----------------------------------------- | ------------------------------------------------------------------- |
+| [`join(sep, ls)`](#fn-join) | Join an array-like table of strings using this string as separator. |
+| [`ljust(s, width, fillchar?)`](#fn-ljust) | Left-justify string in a field of given width. |
+| [`lower(s)`](#fn-lower) | Return lowercased copy. |
+| [`lstrip(s, chars?)`](#fn-lstrip) | Remove leading characters (default: whitespace). |
+| [`rstrip(s, chars?)`](#fn-rstrip) | Remove trailing characters (default: whitespace). |
+| [`strip(s, chars?)`](#fn-strip) | Remove leading and trailing characters (default: whitespace). |
+
+**Predicates**:
+
+| Function | Description |
+| ------------------------------------- | -------------------------------------------------------------------------------------------- |
+| [`isalnum(s)`](#fn-isalnum) | Return true if all characters are alphanumeric and string is non-empty. |
+| [`isalpha(s)`](#fn-isalpha) | Return true if all characters are alphabetic and string is non-empty. |
+| [`isascii(s)`](#fn-isascii) | Return true if all characters are ASCII. |
+| [`isdecimal(s)`](#fn-isdecimal) | Return true if all characters are decimal characters and string is non-empty. |
+| [`isidentifier(s)`](#fn-isidentifier) | Return true if string is a valid identifier and not a reserved keyword. |
+| [`islower(s)`](#fn-islower) | Return true if all cased characters are lowercase and there is at least one cased character. |
+| [`isprintable(s)`](#fn-isprintable) | Return true if all characters are printable. |
+| [`isspace(s)`](#fn-isspace) | Return true if all characters are whitespace and string is non-empty. |
+| [`istitle(s)`](#fn-istitle) | Return true if string is titlecased. |
+| [`isupper(s)`](#fn-isupper) | Return true if all cased characters are uppercase and there is at least one cased character. |
+
+**Split & Replace**:
+
+| Function | Description |
+| --------------------------------------------- | ------------------------------------------------------------------------- |
+| [`partition(s, sep)`](#fn-partition) | Partition string into head, sep, tail from left. |
+| [`removeprefix(s, prefix)`](#fn-removeprefix) | Remove prefix if present. |
+| [`removesuffix(s, suffix)`](#fn-removesuffix) | Remove suffix if present. |
+| [`replace(s, old, new, count?)`](#fn-replace) | Return a copy of the string with all occurrences of a substring replaced. |
+| [`rfind(s, sub, start?, stop?)`](#fn-rfind) | Return highest index of substring or nil if not found. |
+| [`rjust(s, width, fillchar?)`](#fn-rjust) | Right-justify string in a field of given width. |
+| [`rpartition(s, sep)`](#fn-rpartition) | Partition string into head, sep, tail from right. |
+| [`rsplit(s, sep?, maxsplit?)`](#fn-rsplit) | Split from the right by separator, up to maxsplit. |
+| [`split(s, sep?, maxsplit?)`](#fn-split) | Split by separator (or whitespace) up to maxsplit. |
+| [`splitlines(s, keepends?)`](#fn-splitlines) | Split on line boundaries. |
+
+### Casing & Transform
+
+
+
+#### `startswith(s, prefix, start?, stop?)`
+
+Return true if string starts with prefix.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `prefix` (`string|string[]`): Prefix string.
+- `start?` (`integer`): Optional start index (defaults to `1`).
+- `stop?` (`integer`): Optional exclusive end index (defaults to `#s + 1`).
+
+**Return**:
+
+- `hasPrefix` (`boolean`): True when `s` starts with `prefix`.
+
+**Example**:
+
+```lua
+ok = startswith("hello.lua", "he") --> true
+```
+
+> [!NOTE]
+>
+> If prefix is a list, returns `true` when any prefix matches.
+
+
+
+#### `swapcase(s)`
+
+Return a copy with case of alphabetic characters swapped.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `swappedCase` (`string`): String with alphabetic case swapped.
+
+**Example**:
+
+```lua
+s = swapcase("AbC") --> "aBc"
+```
+
+
+
+#### `title(s)`
+
+Return titlecased copy.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `titlecased` (`string`): Titlecased string.
+
+**Example**:
+
+```lua
+s = title("hello world") --> "Hello World"
+```
+
+
+
+#### `translate(s, table_map)`
+
+Translate characters using a mapping table.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `table_map` (`table`): Character translation map.
+
+**Return**:
+
+- `translated` (`string`): Translated string.
+
+**Example**:
+
+```lua
+map = { [string.byte("a")] = "b", ["c"] = false }
+s = translate("abc", map) --> "bb"
+```
+
+
+
+#### `upper(s)`
+
+Return uppercased copy.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `uppercased` (`string`): Uppercased string.
+
+**Example**:
+
+```lua
+s = upper("Hello") --> "HELLO"
+```
+
+
+
+#### `zfill(s, width)`
+
+Pad numeric string on the left with zeros.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `width` (`integer`): Target width.
+
+**Return**:
+
+- `zeroFilled` (`string`): Zero-padded string.
+
+**Example**:
+
+```lua
+s = zfill("42", 5) --> "00042"
+```
+
+### Formatting
+
+
+
+#### `capitalize(s)`
+
+Return copy with first character capitalized and the rest lowercased.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `capitalized` (`string`): Capitalized string.
+
+**Example**:
+
+```lua
+s = capitalize("hello WORLD") --> "Hello world"
+```
+
+
+
+#### `center(s, width, fillchar?)`
+
+Center string within width, padded with fill characters.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `width` (`integer`): Target width.
+- `fillchar?` (`string`): Optional fill character.
+
+**Return**:
+
+- `centered` (`string`): Centered string.
+
+**Example**:
+
+```lua
+s = center("hi", 6, "-") --> "--hi--"
+```
+
+
+
+#### `count(s, sub, start?, stop?)`
+
+Count non-overlapping occurrences of a substring.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `sub` (`string`): Substring to search.
+- `start?` (`integer`): Optional start index (defaults to `1`).
+- `stop?` (`integer`): Optional exclusive end index (defaults to `#s + 1`).
+
+**Return**:
+
+- `count` (`integer`): Number of non-overlapping matches.
+
+**Example**:
+
+```lua
+n = count("aaaa", "aa") --> 2
+n = count("aaaa", "a", 2, -1) --> 2
+n = count("abcd", "") --> 5
+```
+
+
+
+#### `endswith(s, suffix, start?, stop?)`
+
+Return true if string ends with suffix.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `suffix` (`string|string[]`): Suffix string.
+- `start?` (`integer`): Optional start index (defaults to `1`).
+- `stop?` (`integer`): Optional exclusive end index (defaults to `#s + 1`).
+
+**Return**:
+
+- `hasSuffix` (`boolean`): True when `s` ends with `suffix`.
+
+**Example**:
+
+```lua
+ok = endswith("hello.lua", ".lua") --> true
+```
+
+> [!NOTE]
+>
+> If suffix is a list, returns `true` when any suffix matches.
+
+
+
+#### `expandtabs(s, tabsize?)`
+
+Expand tabs to spaces using given tabsize.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `tabsize?` (`integer`): Optional tab width (defaults to `8`).
+
+**Return**:
+
+- `expanded` (`string`): String with tabs expanded.
+
+**Example**:
+
+```lua
+s = expandtabs("a\tb", 4) --> "a b"
+```
+
+
+
+#### `find(s, sub, start?, stop?)`
+
+Return lowest index of substring or nil if not found.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `sub` (`string`): Substring to search.
+- `start?` (`integer`): Optional start index (defaults to `1`).
+- `stop?` (`integer`): Optional exclusive end index (defaults to `#s + 1`).
+
+**Return**:
+
+- `index` (`integer?`): First match index, or `nil` when not found.
+
+**Example**:
+
+```lua
+i = find("hello", "ll") --> 3
+```
+
+
+
+#### `format_map(s, mapping)`
+
+Format string with mapping (key-based) replacement.
+
+**Parameters**:
+
+- `s` (`string`): Template string with `{key}` placeholders.
+- `mapping` (`table`): Values used to replace placeholder keys.
+
+**Return**:
+
+- `formatted` (`string`): Formatted string with placeholders replaced.
+
+**Example**:
+
+```lua
+s = format_map("hi {name}", { name = "bob" }) --> "hi bob"
+```
+
+> [!NOTE]
+>
+> `format_map` is a lightweight `{key}` replacement helper. For richer
+> templating, use `mods.template`.
+
+### Layout
+
+
+
+#### `join(sep, ls)`
+
+Join an array-like table of strings using this string as separator.
+
+**Parameters**:
+
+- `sep` (`string`): Separator value.
+- `ls` (`string[]`): Table value.
+
+**Return**:
+
+- `joined` (`string`): Joined string.
+
+**Example**:
+
+```lua
+s = join(",", { "a", "b", "c" }) --> "a,b,c"
+```
+
+
+
+#### `ljust(s, width, fillchar?)`
+
+Left-justify string in a field of given width.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `width` (`integer`): Target width.
+- `fillchar?` (`string`): Optional fill character.
+
+**Return**:
+
+- `leftJustified` (`string`): Left-justified string.
+
+**Example**:
+
+```lua
+s = ljust("hi", 5, ".") --> "hi..."
+```
+
+
+
+#### `lower(s)`
+
+Return lowercased copy.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `lowercased` (`string`): Lowercased string.
+
+**Example**:
+
+```lua
+s = lower("HeLLo") --> "hello"
+```
+
+
+
+#### `lstrip(s, chars?)`
+
+Remove leading characters (default: whitespace).
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `chars?` (`string`): Optional character set.
+
+**Return**:
+
+- `leadingStripped` (`string`): String with leading characters removed.
+
+**Example**:
+
+```lua
+s = lstrip(" hello") --> "hello"
+```
+
+
+
+#### `rstrip(s, chars?)`
+
+Remove trailing characters (default: whitespace).
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `chars?` (`string`): Optional character set.
+
+**Return**:
+
+- `trailingStripped` (`string`): String with trailing characters removed.
+
+**Example**:
+
+```lua
+s = rstrip("hello ") --> "hello"
+```
+
+
+
+#### `strip(s, chars?)`
+
+Remove leading and trailing characters (default: whitespace).
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `chars?` (`string`): Optional character set.
+
+**Return**:
+
+- `stripped` (`string`): String with leading and trailing characters removed.
+
+**Example**:
+
+```lua
+s = strip(" hello ") --> "hello"
+```
+
+### Predicates
+
+
+
+#### `isalnum(s)`
+
+Return true if all characters are alphanumeric and string is non-empty.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `isAlnum` (`boolean`): True when `s` is non-empty and all characters are
+ alphanumeric.
+
+**Example**:
+
+```lua
+ok = isalnum("abc123") --> true
+```
+
+> [!NOTE]
+>
+> Lua letters are ASCII by default, so non-ASCII letters are not alphanumeric.
+>
+> ```lua
+> isalnum("á1") --> false
+> ```
+
+
+
+#### `isalpha(s)`
+
+Return true if all characters are alphabetic and string is non-empty.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `isAlpha` (`boolean`): True when `s` is non-empty and all characters are
+ alphabetic.
+
+**Example**:
+
+```lua
+ok = isalpha("abc") --> true
+```
+
+> [!NOTE]
+>
+> Lua letters are ASCII by default, so non-ASCII letters are not alphabetic.
+>
+> ```lua
+> isalpha("á") --> false
+> ```
+
+
+
+#### `isascii(s)`
+
+Return true if all characters are ASCII.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `isAscii` (`boolean`): True when all bytes in `s` are ASCII.
+
+**Example**:
+
+```lua
+ok = isascii("hello") --> true
+```
+
+> [!NOTE]
+>
+> The empty string returns `true`.
+
+
+
+#### `isdecimal(s)`
+
+Return true if all characters are decimal characters and string is non-empty.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `isDecimal` (`boolean`): True when `s` is non-empty and all characters are
+ decimal digits.
+
+**Example**:
+
+```lua
+ok = isdecimal("123") --> true
+```
+
+
+
+#### `isidentifier(s)`
+
+Return true if string is a valid identifier and not a reserved keyword.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `isIdentifier` (`boolean`): True when `s` is a valid identifier and not a
+ keyword.
+
+**Example**:
+
+```lua
+ok = isidentifier("foo_bar") --> true
+ok = isidentifier("2var") --> false
+ok = isidentifier("end") --> false (keyword)
+```
+
+
+
+#### `islower(s)`
+
+Return true if all cased characters are lowercase and there is at least one
+cased character.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `isLower` (`boolean`): True when `s` has at least one cased character and all
+ are lowercase.
+
+**Example**:
+
+```lua
+ok = islower("hello") --> true
+```
+
+
+
+#### `isprintable(s)`
+
+Return true if all characters are printable.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `isPrintable` (`boolean`): True when all bytes in `s` are printable ASCII.
+
+**Example**:
+
+```lua
+ok = isprintable("abc!") --> true
+```
+
+> [!NOTE]
+>
+> The empty string returns `true`.
+
+
+
+#### `isspace(s)`
+
+Return true if all characters are whitespace and string is non-empty.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `isSpace` (`boolean`): True when `s` is non-empty and all characters are
+ whitespace.
+
+**Example**:
+
+```lua
+ok = isspace(" \t") --> true
+```
+
+
+
+#### `istitle(s)`
+
+Return true if string is titlecased.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `isTitle` (`boolean`): True when `s` is titlecased.
+
+**Example**:
+
+```lua
+ok = istitle("Hello World") --> true
+```
+
+
+
+#### `isupper(s)`
+
+Return true if all cased characters are uppercase and there is at least one
+cased character.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `isUpper` (`boolean`): True when `s` has at least one cased character and all
+ are uppercase.
+
+**Example**:
+
+```lua
+ok = isupper("HELLO") --> true
+```
+
+### Split & Replace
+
+
+
+#### `partition(s, sep)`
+
+Partition string into head, sep, tail from left.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `sep` (`string`): Separator value.
+
+**Return**:
+
+- `head` (`string`): Part before the separator.
+- `separator` (`string`): Matched separator, or empty string when not found.
+- `tail` (`string`): Part after the separator.
+
+**Example**:
+
+```lua
+a, b, c = partition("a-b-c", "-") --> "a", "-", "b-c"
+```
+
+
+
+#### `removeprefix(s, prefix)`
+
+Remove prefix if present.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `prefix` (`string`): Prefix string.
+
+**Return**:
+
+- `prefixRemoved` (`string`): String with prefix removed when present.
+
+**Example**:
+
+```lua
+s = removeprefix("foobar", "foo") --> "bar"
+```
+
+
+
+#### `removesuffix(s, suffix)`
+
+Remove suffix if present.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `suffix` (`string`): Suffix string.
+
+**Return**:
+
+- `suffixRemoved` (`string`): String with suffix removed when present.
+
+**Example**:
+
+```lua
+s = removesuffix("foobar", "bar") --> "foo"
+```
+
+
+
+#### `replace(s, old, new, count?)`
+
+Return a copy of the string with all occurrences of a substring replaced.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `old` (`string`): Substring to replace.
+- `new` (`string`): Replacement string.
+- `count?` (`integer`): Optional maximum replacement count.
+
+**Return**:
+
+- `replaced` (`string`): String with replacements applied.
+
+**Example**:
+
+```lua
+s = replace("a-b-c", "-", "_", 1) --> "a_b-c"
+```
+
+
+
+#### `rfind(s, sub, start?, stop?)`
+
+Return highest index of substring or nil if not found.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `sub` (`string`): Substring to search.
+- `start?` (`integer`): Optional start index (defaults to `1`).
+- `stop?` (`integer`): Optional inclusive end index (defaults to `#s`).
+
+**Return**:
+
+- `index` (`integer?`): Last match index, or `nil` when not found.
+
+**Example**:
+
+```lua
+i = rfind("ababa", "ba") --> 4
+```
+
+
+
+#### `rjust(s, width, fillchar?)`
+
+Right-justify string in a field of given width.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `width` (`integer`): Target width.
+- `fillchar?` (`string`): Optional fill character.
+
+**Return**:
+
+- `rightJustified` (`string`): Right-justified string.
+
+**Example**:
+
+```lua
+s = rjust("hi", 5, ".") --> "...hi"
+```
+
+
+
+#### `rpartition(s, sep)`
+
+Partition string into head, sep, tail from right.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `sep` (`string`): Separator value.
+
+**Return**:
+
+- `head` (`string`): Part before the separator.
+- `separator` (`string`): Matched separator, or empty string when not found.
+- `tail` (`string`): Part after the separator.
+
+**Example**:
+
+```lua
+a, b, c = rpartition("a-b-c", "-") --> "a-b", "-", "c"
+```
+
+
+
+#### `rsplit(s, sep?, maxsplit?)`
+
+Split from the right by separator, up to maxsplit.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `sep?` (`string`): Optional separator value.
+- `maxsplit?` (`integer`): Optional maximum number of splits.
+
+**Return**:
+
+- `parts` (`mods.List`): Split parts.
+
+**Example**:
+
+```lua
+parts = rsplit("a,b,c", ",", 1) --> { "a,b", "c" }
+```
+
+
+
+#### `split(s, sep?, maxsplit?)`
+
+Split by separator (or whitespace) up to maxsplit.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `sep?` (`string`): Optional separator value.
+- `maxsplit?` (`integer`): Optional maximum number of splits.
+
+**Return**:
+
+- `parts` (`mods.List`): Split parts.
+
+**Example**:
+
+```lua
+parts = split("a,b,c", ",") --> { "a", "b", "c" }
+```
+
+
+
+#### `splitlines(s, keepends?)`
+
+Split on line boundaries.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `keepends?` (`boolean`): Optional whether to keep line endings.
+
+**Return**:
+
+- `lines` (`mods.List`): Split lines.
+
+**Example**:
+
+```lua
+lines = splitlines("a\nb\r\nc") --> { "a", "b", "c" }
+```
diff --git a/docs/api/stringcase.md b/docs/api/stringcase.md
new file mode 100644
index 0000000..977d9c8
--- /dev/null
+++ b/docs/api/stringcase.md
@@ -0,0 +1,391 @@
+---
+description: "String case conversion and word splitting."
+---
+
+# `stringcase`
+
+String case conversion and word splitting.
+
+## Usage
+
+```lua
+stringcase = require "mods.stringcase"
+
+print(stringcase.snake("FooBar")) --> "foo_bar"
+```
+
+## Functions
+
+**Basic**:
+
+| Function | Description |
+| ----------------------- | -------------------------------- |
+| [`lower(s)`](#fn-lower) | Convert string to all lowercase. |
+| [`upper(s)`](#fn-upper) | Convert string to all uppercase. |
+
+**Letter Case**:
+
+| Function | Description |
+| --------------------------------- | ------------------------------------------------------------------------- |
+| [`capitalize(s)`](#fn-capitalize) | Capitalize the first letter and lowercase the rest. |
+| [`sentence(s)`](#fn-sentence) | Convert string to sentence case (first letter uppercase, rest unchanged). |
+| [`swapcase(s)`](#fn-swapcase) | Swap case of each letter. |
+
+**Word Case**:
+
+| Function | Description |
+| --------------------------------- | --------------------------------------------------------------------- |
+| [`acronym(s)`](#fn-acronym) | Get acronym of words in string (first letters only). |
+| [`camel(s)`](#fn-camel) | Convert string to camelCase. |
+| [`constant(s)`](#fn-constant) | Convert string to CONSTANT_CASE (uppercase snake_case). |
+| [`delimit(s, sep?)`](#fn-delimit) | Normalize to snake_case, then delimit words with a separator. |
+| [`dot(s)`](#fn-dot) | Convert string to dot.case. |
+| [`kebab(s)`](#fn-kebab) | Convert string to kebab-case. |
+| [`pascal(s)`](#fn-pascal) | Convert string to PascalCase. |
+| [`path(s)`](#fn-path) | Convert string to path/case (slashes between words). |
+| [`snake(s)`](#fn-snake) | Convert string to snake_case. |
+| [`space(s)`](#fn-space) | Convert string to space case (spaces between words). |
+| [`title(s)`](#fn-title) | Convert string to Title Case (first letter of each word capitalized). |
+
+### Basic
+
+
+
+#### `lower(s)`
+
+Convert string to all lowercase.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `lowercased` (`string`): Lowercased string.
+
+**Example**:
+
+```lua
+lower("foo_bar-baz") --> "foo_bar-baz"
+lower("FooBar baz") --> "foobar baz"
+```
+
+
+
+#### `upper(s)`
+
+Convert string to all uppercase.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `uppercased` (`string`): Uppercased string.
+
+**Example**:
+
+```lua
+upper("foo_bar-baz") --> "FOO_BAR-BAZ"
+upper("FooBar baz") --> "FOOBAR BAZ"
+```
+
+### Letter Case
+
+
+
+#### `capitalize(s)`
+
+Capitalize the first letter and lowercase the rest.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `capitalized` (`string`): Capitalized string.
+
+**Example**:
+
+```lua
+capitalize("foo_bar-baz") --> "Foo_bar-baz"
+capitalize("FooBar baz") --> "Foobar baz"
+```
+
+
+
+#### `sentence(s)`
+
+Convert string to sentence case (first letter uppercase, rest unchanged).
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `sentenceCased` (`string`): Sentence-cased string.
+
+**Example**:
+
+```lua
+sentence("foo_bar-baz") --> "Foo_bar-baz"
+sentence("FooBar baz") --> "FooBar baz"
+```
+
+
+
+#### `swapcase(s)`
+
+Swap case of each letter.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `swapCased` (`string`): Swap-cased string.
+
+**Example**:
+
+```lua
+swapcase("foo_bar-baz") --> "FOO_BAR-BAZ"
+swapcase("FooBar baz") --> "fOObAR BAZ"
+```
+
+### Word Case
+
+
+
+#### `acronym(s)`
+
+Get acronym of words in string (first letters only).
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `acronym` (`string`): Acronym string.
+
+**Example**:
+
+```lua
+acronym("foo_bar-baz") --> "FBB"
+acronym("FooBar baz") --> "FBB"
+```
+
+
+
+#### `camel(s)`
+
+Convert string to camelCase.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `camelCased` (`string`): Camel-cased string.
+
+**Example**:
+
+```lua
+camel("foo_bar-baz") --> "fooBarBaz"
+camel("FooBar baz") --> "fooBarBaz"
+```
+
+
+
+#### `constant(s)`
+
+Convert string to CONSTANT_CASE (uppercase snake_case).
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `constantCased` (`string`): Constant-cased string.
+
+**Example**:
+
+```lua
+constant("foo_bar-baz") --> "FOO_BAR_BAZ"
+constant("FooBar baz") --> "FOO_BAR_BAZ"
+```
+
+
+
+#### `delimit(s, sep?)`
+
+Normalize to snake_case, then delimit words with a separator.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `sep?` (`string`): Optional separator value (defaults to `""`).
+
+**Return**:
+
+- `delimited` (`string`): String with normalized words separated by `sep`.
+
+**Example**:
+
+```lua
+delimit("foo_bar-baz", "-") --> "foo-bar-baz"
+delimit("FooBar baz", "-") --> "foo-bar-baz"
+```
+
+
+
+#### `dot(s)`
+
+Convert string to dot.case.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `dotCased` (`string`): Dot-cased string.
+
+**Example**:
+
+```lua
+dot("foo_bar-baz") --> "foo.bar.baz"
+dot("FooBar baz") --> "foo.bar.baz"
+```
+
+
+
+#### `kebab(s)`
+
+Convert string to kebab-case.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `kebabCased` (`string`): Kebab-cased string.
+
+**Example**:
+
+```lua
+kebab("foo_bar-baz") --> "foo-bar-baz"
+kebab("FooBar baz") --> "foo-bar-baz"
+```
+
+
+
+#### `pascal(s)`
+
+Convert string to PascalCase.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `pascalCased` (`string`): Pascal-cased string.
+
+**Example**:
+
+```lua
+pascal("foo_bar-baz") --> "FooBarBaz"
+pascal("FooBar baz") --> "FooBarBaz"
+```
+
+
+
+#### `path(s)`
+
+Convert string to path/case (slashes between words).
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `pathCased` (`string`): Path-cased string.
+
+**Example**:
+
+```lua
+path("foo_bar-baz") --> "foo/bar/baz"
+path("FooBar baz") --> "foo/bar/baz"
+```
+
+
+
+#### `snake(s)`
+
+Convert string to snake_case.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `snakeCased` (`string`): Snake-cased string.
+
+**Example**:
+
+```lua
+snake("foo_bar-baz") --> "foo_bar_baz"
+snake("FooBar baz") --> "foo_bar_baz"
+```
+
+
+
+#### `space(s)`
+
+Convert string to space case (spaces between words).
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `spaceCased` (`string`): Space-cased string.
+
+**Example**:
+
+```lua
+space("foo_bar-baz") --> "foo bar baz"
+space("FooBar baz") --> "foo bar baz"
+```
+
+
+
+#### `title(s)`
+
+Convert string to Title Case (first letter of each word capitalized).
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `titleCased` (`string`): Title-cased string.
+
+**Example**:
+
+```lua
+title("foo_bar-baz") --> "Foo Bar Baz"
+title("FooBar baz") --> "Foo Bar Baz"
+```
diff --git a/docs/api/stringify.md b/docs/api/stringify.md
new file mode 100644
index 0000000..7d6a3b2
--- /dev/null
+++ b/docs/api/stringify.md
@@ -0,0 +1,144 @@
+---
+description: "Render Lua values as readable source-like text."
+---
+
+# `stringify`
+
+Render Lua values as readable source-like text.
+
+## Usage
+
+```lua
+local stringify = require("mods").stringify
+
+local t = { "first", name = "Ada"}
+
+print(stringify(t))
+--> {
+--> "first",
+--> name = "Ada"
+--> }
+```
+
+
+
+## `stringify(value, opts?)`
+
+Render Lua values as readable source-like text.
+
+**Parameters**:
+
+- `value` (`any`): Lua value to render.
+- `opts?`
+ (`{omit_array_keys?:boolean, indent?:string, newline?:string, replacer?:fun(k,v):(result:any)}`):
+ Rendering options.
+
+**Return**:
+
+- `out` (`string`): Readable string representation.
+
+**Example**:
+
+```lua
+local stringify = require("mods").stringify
+
+local t = {
+ "first",
+ "second",
+ name = "Ada",
+ ok = true,
+ nested = { value = 42 },
+}
+
+print(stringify(t))
+--> {
+--> "first",
+--> "second",
+--> name = "Ada",
+--> nested = {
+--> value = 42
+--> },
+--> ok = true
+--> }
+```
+
+### Key formatting
+
+Strings are quoted and reserved keys use bracket notation.
+
+```lua
+print(stringify({ name = "Ada", ["with space"] = true }))
+--> {
+--> ["with space"] = true,
+--> name = "Ada"
+--> }
+```
+
+### Cycle rendering
+
+Circular references render as ``.
+
+```lua
+local t = {}
+t.self = t
+
+print(stringify(t))
+--> {
+--> self =
+--> }
+```
+
+## Options
+
+### `omit_array_keys`
+
+Contiguous positive integer keys render as implicit array entries by default.
+Set `omit_array_keys = false` to keep them explicit.
+
+```lua
+print(stringify({ "first", nil, "third" }, { omit_array_keys = false }))
+--> {
+--> [1] = "first",
+--> [3] = "third"
+--> }
+```
+
+### `indent`
+
+`indent` controls the repeated indentation prefix. It defaults to two spaces.
+
+```lua
+print(stringify({ a = { b = true } }, { indent = ".." }))
+--> {
+--> a = {
+--> b = true
+--> }
+--> }
+```
+
+### `newline`
+
+`newline` controls the line separator. Use `newline = ""` for inline output.
+
+```lua
+print(stringify({ a = 1, b = 2 }, { newline = "" }))
+--> {a=1,b=2}
+```
+
+### `replacer`
+
+`replacer` can transform or remove values before they are rendered.
+
+```lua
+local function replacer(k, v)
+ if k == "secret" then
+ return nil
+ end
+ return v
+end
+
+print(stringify({ name = "Ada", secret = "hidden" }, { replacer = replacer }))
+--> {
+--> name = "Ada"
+--> }
+```
diff --git a/docs/api/tbl.md b/docs/api/tbl.md
new file mode 100644
index 0000000..28c8f22
--- /dev/null
+++ b/docs/api/tbl.md
@@ -0,0 +1,473 @@
+---
+description:
+ "Table operations for querying, copying, merging, and transforming tables."
+---
+
+# `tbl`
+
+Table operations for querying, copying, merging, and transforming tables.
+
+## Usage
+
+```lua
+tbl = require "mods.tbl"
+
+print(tbl.count({ a = 1, b = 2 })) --> 2
+```
+
+## Functions
+
+**Copies**:
+
+| Function | Description |
+| ----------------------------- | ----------------------------------- |
+| [`copy(t)`](#fn-copy) | Create a shallow copy of the table. |
+| [`deepcopy(v)`](#fn-deepcopy) | Create a deep copy of a value. |
+
+**Core Utilities**:
+
+| Function | Description |
+| ----------------------- | --------------------------------------- |
+| [`clear(t)`](#fn-clear) | Remove all entries from the table. |
+| [`count(t)`](#fn-count) | Return the number of keys in the table. |
+
+**Iterators**:
+
+| Function | Description |
+| ------------------------------- | -------------------------------------------- |
+| [`foreach(t, fn)`](#fn-foreach) | Call a function for each value in the table. |
+| [`spairs(t)`](#fn-spairs) | Iterate key-value pairs in sorted key order. |
+
+**Queries**:
+
+| Function | Description |
+| ------------------------------------ | ---------------------------------------------------------------- |
+| [`deep_equal(a, b)`](#fn-deep-equal) | Return `true` if two tables are deeply equal. |
+| [`filter(t, pred)`](#fn-filter) | Filter entries by a value predicate. |
+| [`find(t, v)`](#fn-find) | Find the first key whose value equals the given value. |
+| [`find_if(t, pred)`](#fn-find-if) | Find first value and key matching predicate. |
+| [`get(t, ...)`](#fn-get) | Safely get nested value by keys. |
+| [`is_same(a, b)`](#fn-is-same) | Return `true` if two tables have the same keys and equal values. |
+
+**Transforms**:
+
+| Function | Description |
+| ------------------------------ | -------------------------------------------------- |
+| [`invert(t)`](#fn-invert) | Invert keys/values into new table. |
+| [`isempty(t)`](#fn-isempty) | Return true if table has no entries. |
+| [`keys(t)`](#fn-keys) | Return a list of all keys in the table. |
+| [`map(t, fn)`](#fn-map) | Return a new table by mapping each key-value pair. |
+| [`update(t1, t2)`](#fn-update) | Merge entries from `t2` into `t1` and return `t1`. |
+| [`values(t)`](#fn-values) | Return a list of all values in the table. |
+
+### Copies
+
+
+
+#### `copy(t)`
+
+Create a shallow copy of the table.
+
+**Parameters**:
+
+- `t` (`T`): Source table.
+
+**Return**:
+
+- `copy` (`T`): Shallow-copied table.
+
+**Example**:
+
+```lua
+t = copy({ a = 1, b = 2 }) --> { a = 1, b = 2 }
+```
+
+
+
+#### `deepcopy(v)`
+
+Create a deep copy of a value.
+
+**Parameters**:
+
+- `v` (`T`): Input value.
+
+**Return**:
+
+- `copiedValue` (`T`): Deep-copied value.
+
+**Example**:
+
+```lua
+t = deepcopy({ a = { b = 1 } }) --> { a = { b = 1 } }
+n = deepcopy(42) --> 42
+```
+
+> [!NOTE]
+>
+> If `v` is a table, all nested tables are copied recursively; other types are
+> returned as-is.
+
+### Core Utilities
+
+
+
+#### `clear(t)`
+
+Remove all entries from the table.
+
+**Parameters**:
+
+- `t` (`table`): Target table.
+
+**Return**:
+
+- `none` (`nil`)
+
+**Example**:
+
+```lua
+t = { a = 1, b = 2 }
+clear(t) --> t = {}
+```
+
+
+
+#### `count(t)`
+
+Return the number of keys in the table.
+
+**Parameters**:
+
+- `t` (`table`): Input table.
+
+**Return**:
+
+- `count` (`integer`): Number of keys in `t`.
+
+**Example**:
+
+```lua
+n = count({ a = 1, b = 2 }) --> 2
+```
+
+### Iterators
+
+
+
+#### `foreach(t, fn)`
+
+Call a function for each value in the table.
+
+**Parameters**:
+
+- `t` (`table`): Input table.
+- `fn` (`fun(value:V, key:K)`): Function invoked for each entry.
+
+**Return**:
+
+- `none` (`nil`)
+
+**Example**:
+
+```lua
+foreach({ a = 1, b = 2 }, function(v, k)
+ print(k, v)
+end)
+```
+
+
+
+#### `spairs(t)`
+
+Iterate key-value pairs in sorted key order.
+
+**Parameters**:
+
+- `t` (`T`): Input table.
+
+**Return**:
+
+- `iterator` (`fun(table: table, index?: K):(K, V)`): Sorted pairs
+ iterator.
+- **value** (`T`)
+
+**Example**:
+
+```lua
+for k, v in spairs({ b = 2, a = 1 }) do
+ print(k, v)
+end
+```
+
+### Queries
+
+
+
+#### `deep_equal(a, b)`
+
+Return `true` if two tables are deeply equal.
+
+**Parameters**:
+
+- `a` (`table`): Left table.
+- `b` (`table`): Right table.
+
+**Return**:
+
+- `isDeepEqual` (`boolean`): True when both tables are recursively equal.
+
+**Example**:
+
+```lua
+ok = deep_equal({ a = { b = 1 } }, { a = { b = 1 } }) --> true
+ok = deep_equal({ a = { b = 1 } }, { a = { b = 2 } }) --> false
+```
+
+
+
+#### `filter(t, pred)`
+
+Filter entries by a value predicate.
+
+**Parameters**:
+
+- `t` (`table`): Input table.
+- `pred` (`fun(value:V):boolean`): Value predicate.
+
+**Return**:
+
+- `filtered` (`table`): Table containing entries where `pred(v)` is true.
+
+**Example**:
+
+```lua
+even = filter({ a = 1, b = 2, c = 3 }, function(v)
+ return v % 2 == 0
+end) --> { b = 2 }
+```
+
+
+
+#### `find(t, v)`
+
+Find the first key whose value equals the given value.
+
+**Parameters**:
+
+- `t` (`table`): Input table.
+- `v` (`V`): Value to find.
+
+**Return**:
+
+- `key` (`K?`): First matching key, or `nil` when not found.
+
+**Example**:
+
+```lua
+key = find({ a = 1, b = 2, c = 2 }, 2) --> "b" or "c"
+```
+
+
+
+#### `find_if(t, pred)`
+
+Find first value and key matching predicate.
+
+**Parameters**:
+
+- `t` (`table`): Input table.
+- `pred` (`fun(key:K,value:V):boolean`): Predicate function.
+
+**Return**:
+
+- `value` (`V?`): First matching value, or `nil` when not found.
+- `key` (`K?`): Key for the first matching value, or `nil` when not found.
+
+**Example**:
+
+```lua
+v, k = find_if({ a = 1, b = 2 }, function(v, k)
+ return k == "b" and v == 2
+end) --> 2, "b"
+```
+
+
+
+#### `get(t, ...)`
+
+Safely get nested value by keys.
+
+**Parameters**:
+
+- `t` (`table`): Root table.
+- `...` (`any`): Additional arguments.
+
+**Return**:
+
+- `nestedValue` (`any`): Nested value, or `nil` when any key is missing.
+
+**Example**:
+
+```lua
+t = { a = { b = { c = 1 } } }
+v1 = get(t, "a", "b", "c") --> 1
+v2 = get(t) --> { a = { b = { c = 1 } } }
+```
+
+> [!NOTE]
+>
+> If no keys are provided, returns the input table.
+
+
+
+#### `is_same(a, b)`
+
+Return `true` if two tables have the same keys and equal values.
+
+**Parameters**:
+
+- `a` (`table`): Left table.
+- `b` (`table`): Right table.
+
+**Return**:
+
+- `isSame` (`boolean`): True when both tables have the same keys and values.
+
+**Example**:
+
+```lua
+ok = is_same({ a = 1, b = 2 }, { b = 2, a = 1 }) --> true
+ok = is_same({ a = {} }, { a = {} }) --> false
+```
+
+### Transforms
+
+
+
+#### `invert(t)`
+
+Invert keys/values into new table.
+
+**Parameters**:
+
+- `t` (`table`): Input table.
+
+**Return**:
+
+- `inverted` (`table`): Inverted table (`value -> key`).
+
+**Example**:
+
+```lua
+t = invert({ a = 1, b = 2 }) --> { [1] = "a", [2] = "b" }
+```
+
+
+
+#### `isempty(t)`
+
+Return true if table has no entries.
+
+**Parameters**:
+
+- `t` (`table`): Input table.
+
+**Return**:
+
+- `isEmpty` (`boolean`): True when `t` has no entries.
+
+**Example**:
+
+```lua
+empty = isempty({}) --> true
+```
+
+
+
+#### `keys(t)`
+
+Return a list of all keys in the table.
+
+**Parameters**:
+
+- `t` (`table`): Input table.
+
+**Return**:
+
+- `keys` (`mods.List`): List of keys in `t`.
+
+**Example**:
+
+```lua
+keys = keys({ a = 1, b = 2 }) --> { "a", "b" }
+```
+
+
+
+#### `map(t, fn)`
+
+Return a new table by mapping each key-value pair.
+
+**Parameters**:
+
+- `t` (`table`): Input table.
+- `fn` (`fun(key:K, value:V):T`): Key-value mapping function.
+
+**Return**:
+
+- `mapped` (`table`): New table with mapped values.
+
+**Example**:
+
+```lua
+t = map({ a = 1, b = 2 }, function(k, v)
+ return k .. v
+end) --> { a = "a1", b = "b2" }
+```
+
+> [!NOTE]
+>
+> Output keeps original keys; only values are transformed by `fn`.
+
+
+
+#### `update(t1, t2)`
+
+Merge entries from `t2` into `t1` and return `t1`.
+
+**Parameters**:
+
+- `t1` (`T`): Target table.
+- `t2` (`table`): Source table.
+
+**Return**:
+
+- `table` (`T`): Updated `t1` table.
+
+**Example**:
+
+```lua
+t1 = { a = 1, b = 2 }
+update(t1, { b = 3, c = 4 }) --> t1 is { a = 1, b = 3, c = 4 }
+```
+
+
+
+#### `values(t)`
+
+Return a list of all values in the table.
+
+**Parameters**:
+
+- `t` (`table`): Input table.
+
+**Return**:
+
+- `values` (`mods.List`): List of values in `t`.
+
+**Example**:
+
+```lua
+vals = values({ a = 1, b = 2 }) --> { 1, 2 }
+```
diff --git a/docs/api/template.md b/docs/api/template.md
new file mode 100644
index 0000000..0d5bdc2
--- /dev/null
+++ b/docs/api/template.md
@@ -0,0 +1,123 @@
+---
+description: "String template rendering with {{."
+---
+
+# `template`
+
+String template rendering with {{...}} placeholders.
+
+## Usage
+
+```lua
+template = require "mods.template"
+
+view = {
+ user = { name = "Ada" },
+}
+
+out = template("Hello {{user.name}}!", view) --> "Hello Ada!"
+```
+
+
+
+## `template(tmpl, view)`
+
+Render string templates with {{...}} placeholders.
+
+**Parameters**:
+
+- `tmpl` (`string`): Template string with placeholders.
+- `view` (`table`): Input data used to resolve placeholders.
+
+**Return**:
+
+- `out` (`string`): Rendered output string.
+
+**Example**:
+
+```lua
+view = { subject = "World" }
+template("Hello {{subject}}", view) --> "Hello World"
+```
+
+> [!NOTE]
+>
+> Whitespace inside placeholders is ignored.
+>
+> ```lua
+> template("Hi {{ name }}", { name = "Ada" }) --> "Hi Ada"
+> ```
+
+## Dot Paths
+
+Use dot notation to access nested values in `view`.
+
+```lua
+view = { user = { meta = { role = "Engineer" } } }
+template("Role: {{user.meta.role}}", view) --> "Role: Engineer"
+```
+
+> [!NOTE]
+>
+> {{.}} renders the entire root `view` table, not a nested
+> field.
+>
+> ```lua
+> template("View: {{.}}", { value = 123 })
+> --> View: {
+> -- value = 123
+> -- }
+> ```
+
+## Function Values
+
+If a placeholder resolves to a function, that function is called and its result
+is rendered.
+
+```lua
+view = { name_func = function() return "Ada" end }
+template("Hi {{name_func}}", view) --> "Hi Ada"
+```
+
+> [!NOTE]
+>
+> If the function returns `nil`, the placeholder renders as an empty string.
+
+## Table Values
+
+Table placeholders are rendered using `mods.stringify`.
+
+```lua
+view = { data = { a = 1, b = true } }
+template("Data: {{data}}", view)
+--> Data: {
+-- a = 1,
+-- b = true
+-- }
+```
+
+## Missing and Invalid Placeholders
+
+Missing keys render as an empty string.
+
+```lua
+view = {}
+template("Missing: {{unknown}}", view) --> "Missing: "
+```
+
+Invalid placeholder names render as an empty string (for example:
+{{..}}, {{.name}},
+{{user.}}, {{user..name}}).
+
+```lua
+view = { user = { name = "Ada" } }
+template("Bad: {{user..name}}", view) --> "Bad: "
+```
+
+If a placeholder is not closed ({{unclosed), it is emitted
+as-is.
+
+```lua
+view = { name = "Ada" }
+template("Hi {{name", view) --> "Hi {{name"
+```
diff --git a/docs/api/utils.md b/docs/api/utils.md
new file mode 100644
index 0000000..1309acd
--- /dev/null
+++ b/docs/api/utils.md
@@ -0,0 +1,232 @@
+---
+description: "Shared utility helpers used across the Mods library."
+---
+
+# `utils`
+
+Shared utility helpers used across the Mods library.
+
+## Usage
+
+```lua
+utils = require "mods.utils"
+
+print(utils.quote('hello "world"')) --> 'hello "world"'
+```
+
+## Functions
+
+**Formatting**:
+
+| Function | Description |
+| ------------------------------- | -------------------------------------------------------------- |
+| [`args_repr(v)`](#fn-args-repr) | Format a list-like table as a comma-separated argument string. |
+| [`keypath(...)`](#fn-keypath) | Format a key chain as a Lua-like table access path. |
+| [`quote(v)`](#fn-quote) | Smart-quote a string for readable Lua-like output. |
+
+**Lazy Loading**:
+
+| Function | Description |
+| -------------------------------------------- | --------------------------------- |
+| [`lazy_module(name, err?)`](#fn-lazy-module) | Return a lazy proxy for a module. |
+
+**Validation**:
+
+| Function | Description |
+| ------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
+| [`assert_arg(argn, v, validator?, optional?, lv?)`](#fn-assert-arg) | Assert argument value using `mods.validate` and raise a Lua error on failure. |
+| [`validate(name, v, validator?, optional?, msg?)`](#fn-validate) | Validate a value using `mods.validate` and raise a Lua error on failure. |
+| [`validate(path, v, validator?, optional?, msg?)`](#fn-validate) | Validate a value using `mods.validate` and raise a Lua error on failure. |
+
+### Formatting
+
+
+
+#### `args_repr(v)`
+
+Format a list-like table as a comma-separated argument string.
+
+**Parameters**:
+
+- `v` (`any`): Value to format. `nil` returns an empty string.
+
+**Return**:
+
+- `out` (`string`): Argument list string.
+
+**Example**:
+
+```lua
+utils.args_repr({ "a", 1, true }) --> '"a", 1, true'
+```
+
+
+
+#### `keypath(...)`
+
+Format a key chain as a Lua-like table access path.
+
+**Parameters**:
+
+- `...` (`any`): Additional arguments.
+
+**Return**:
+
+- `path` (`string`): Rendered key path.
+
+**Example**:
+
+```lua
+p1 = utils.keypath("t", "a", "b", "c") --> "t.a.b.c"
+p2 = utils.keypath("ctx", "users", 1, "name") --> "ctx.users[1].name"
+p3 = utils.keypath("ctx", "invalid-key") --> 'ctx["invalid-key"]'
+p4 = utils.keypath() --> ""
+```
+
+
+
+#### `quote(v)`
+
+Smart-quote a string for readable Lua-like output.
+
+**Parameters**:
+
+- `v` (`string`): String to quote.
+
+**Return**:
+
+- `out` (`string`): Quoted string.
+
+**Example**:
+
+```lua
+print(utils.quote('He said "hi"')) -- 'He said "hi"'
+print(utils.quote('say "hi" and \\'bye\\'')) -- "say \"hi\" and 'bye'"
+```
+
+### Lazy Loading
+
+
+
+#### `lazy_module(name, err?)`
+
+Return a lazy proxy for a module.
+
+The proxy rewrites its metamethods after first access while keeping the proxy
+table itself free of cached fields.
+
+**Parameters**:
+
+- `name` (`string`): Module name passed to `require`.
+- `err?` (`string`): Optional error message raised when loading fails.
+
+**Return**:
+
+- `module` (`{}`): Lazy proxy for the loaded module.
+
+**Example**:
+
+```lua
+local fs = utils.lazy_module("mods.fs")
+print(fs.exists("README.md"))
+
+local stringify = utils.lazy_module("mods.stringify")
+print(stringify({ a = 1 }))
+```
+
+> [!NOTE]
+>
+> Supports both table-returning modules and function-returning modules.
+
+### Validation
+
+
+
+#### `assert_arg(argn, v, validator?, optional?, lv?)`
+
+Assert argument value using `mods.validate` and raise a Lua error on failure.
+
+**Parameters**:
+
+- `argn` (`integer`): Argument index for error context.
+- `v` (`T`): Value to check.
+- `validator?` (`modsValidatorName`): Validator name (defaults to `"truthy"`).
+- `optional?` (`boolean`): Skip errors when `v` is `nil` (defaults to `false`).
+- `lv?` (`integer`): Error level passed to `error` (defaults to `3`).
+
+**Return**:
+
+- `validatedValue` (`T`): Same input value on success, or `nil` when optional.
+
+**Example**:
+
+```lua
+utils.assert_arg(1, "ok", "string") --> "ok"
+utils.assert_arg(2, nil, "string", true) --> nil
+utils.assert_arg(2, 123, "string")
+--> raises: bad argument #2 (expected string, got number)
+utils.assert_arg(3, "x", "number", false, "need {{expected}}, got {{got}}")
+--> raises: bad argument #3 (need number, got string)
+```
+
+> [!NOTE]
+>
+> When the caller function name is available, error text includes
+> `to ''` (Lua-style bad argument context).
+
+
+
+#### `validate(name, v, validator?, optional?, msg?)`
+
+Validate a value using `mods.validate` and raise a Lua error on failure.
+
+**Parameters**:
+
+- `name` (`string`): Name for the error prefix.
+- `v` (`any`): Value to validate.
+- `validator?` (`modsValidatorName`): Validator name (defaults to `"truthy"`).
+- `optional?` (`boolean`): Skip errors when `v` is `nil` (defaults to `false`).
+- `msg?` (`string`): Optional override template passed to `mods.validate`.
+
+**Return**:
+
+- `none` (`nil`)
+
+**Example**:
+
+```lua
+utils.validate("path", "ok", "string")
+utils.validate("name", nil, "string", true)
+utils.validate("count", "x", "number")
+--> raises: count: expected number, got string
+```
+
+
+
+#### `validate(path, v, validator?, optional?, msg?)`
+
+Validate a value using `mods.validate` and raise a Lua error on failure.
+
+**Parameters**:
+
+- `path` (`table`): Path parts for the error name.
+- `v` (`any`): Value to validate.
+- `validator?` (`modsValidatorName`): Validator name (defaults to `"truthy"`).
+- `optional?` (`boolean`): Skip errors when `v` is `nil` (defaults to `false`).
+- `msg?` (`string`): Optional override template passed to `mods.validate`.
+
+**Return**:
+
+- `none` (`nil`)
+
+**Example**:
+
+```lua
+utils.validate({ "ctx", "users", 1, "name" }, nil, "string", true)
+utils.validate({ "ctx", "users", 1, "name" }, 123, "string")
+--> raises: ctx.users[1].name: expected string, got number
+```
+
+> [!NOTE]
+>
+> On failure, `path` is rendered with `mods.utils.keypath`.
diff --git a/docs/api/validate.md b/docs/api/validate.md
new file mode 100644
index 0000000..196af83
--- /dev/null
+++ b/docs/api/validate.md
@@ -0,0 +1,745 @@
+---
+description: "Validation helpers for Lua values and filesystem path types."
+---
+
+# `validate`
+
+Validation helpers for Lua values and filesystem path types.
+
+## Usage
+
+```lua
+local validate = require "mods.validate"
+
+ok, err = validate.number("nope") --> false, "number expected, got string"
+ok, err = validate(123, "number") --> true, nil
+```
+
+## `validate()`
+
+`validate(v, validator)` dispatches to the registered validator. If `validator`
+is omitted, it defaults to `"truthy"`.
+
+```lua
+validate() --> false, "truthy value expected, got no value"
+validate(1) --> true, nil
+validate(1, "nil") --> false, "nil expected, got number"
+```
+
+> [!IMPORTANT]
+>
+> Path checks require **LuaFileSystem**
+> ([`lfs`](https://github.com/lunarmodules/luafilesystem)) and raise an error if
+> it is not installed.
+
+## Validator Names
+
+Validator names are case-insensitive for field access.
+
+```lua
+validate.number(1) --> true, nil
+validate.NumBer(1) --> true, nil
+```
+
+`validator` in `validate(v, validator)` is matched as-is (case-sensitive):
+
+```lua
+validate(1, "number") --> true, nil
+validate(1, "NuMbEr") --> false, "NuMbEr expected, got number"
+```
+
+## Custom Messages
+
+Validator functions accept an optional template override as the second argument:
+validate.number(v, "need {{expected}}, got {{got}}")`.
+
+You can also set `validate.messages.` to define default templates per
+validator.
+
+```lua
+validate.string(123, "want {{expected}}, got {{got}}")
+--> false, "want string, got number"
+```
+
+## Functions
+
+**Path Checks**:
+
+| Function | Description |
+| ------------------------------- | ------------------------------------------------------------------------------------------------------- |
+| [`block(v, msg?)`](#fn-block) | Returns `true` when `v` is a block device path. Otherwise returns `false` and an error message. |
+| [`char(v, msg?)`](#fn-char) | Returns `true` when `v` is a char device path. Otherwise returns `false` and an error message. |
+| [`device(v, msg?)`](#fn-device) | Returns `true` when `v` is a block or char device path. Otherwise returns `false` and an error message. |
+| [`dir(v, msg?)`](#fn-dir) | Returns `true` when `v` is a directory path. Otherwise returns `false` and an error message. |
+| [`fifo(v, msg?)`](#fn-fifo) | Returns `true` when `v` is a FIFO path. Otherwise returns `false` and an error message. |
+| [`file(v, msg?)`](#fn-file) | Returns `true` when `v` is a file path. Otherwise returns `false` and an error message. |
+| [`link(v, msg?)`](#fn-link) | Returns `true` when `v` is a symlink path. Otherwise returns `false` and an error message. |
+| [`path(v, msg?)`](#fn-path) | Returns `true` when `v` is a valid filesystem path. Otherwise returns `false` and an error message. |
+| [`socket(v, msg?)`](#fn-socket) | Returns `true` when `v` is a socket path. Otherwise returns `false` and an error message. |
+
+**Registration**:
+
+| Function | Description |
+| ------------------------------------------------------ | -------------------------------------------------- |
+| [`register(name, validator, template?)`](#fn-register) | Register or override a validator function by name. |
+
+**Type Checks**:
+
+| Function | Description |
+| ----------------------------------- | -------------------------------------------------------------------------------------------- |
+| [`boolean(v, msg?)`](#fn-boolean) | Returns `true` when `v` is a boolean. Otherwise returns `false` and an error message. |
+| [`function(v, msg?)`](#fn-function) | Returns `true` when `v` is a function. Otherwise returns `false` and an error message. |
+| [`nil(v, msg?)`](#fn-nil) | Returns `true` when `v` is `nil`. Otherwise returns `false` and an error message. |
+| [`number(v, msg?)`](#fn-number) | Returns `true` when `v` is a number. Otherwise returns `false` and an error message. |
+| [`string(v, msg?)`](#fn-string) | Returns `true` when `v` is a string. Otherwise returns `false` and an error message. |
+| [`table(v, msg?)`](#fn-table) | Returns `true` when `v` is a table. Otherwise returns `false` and an error message. |
+| [`thread(v, msg?)`](#fn-thread) | Returns `true` when `v` is a thread. Otherwise returns `false` and an error message. |
+| [`userdata(v, msg?)`](#fn-userdata) | Returns `true` when `v` is a userdata value. Otherwise returns `false` and an error message. |
+
+**Value Checks**:
+
+| Function | Description |
+| ----------------------------------- | ------------------------------------------------------------------------------------------- |
+| [`callable(v, msg?)`](#fn-callable) | Returns `true` when `v` is callable. Otherwise returns `false` and an error message. |
+| [`false(v, msg?)`](#fn-false) | Returns `true` when `v` is exactly `false`. Otherwise returns `false` and an error message. |
+| [`falsy(v, msg?)`](#fn-falsy) | Returns `true` when `v` is falsy. Otherwise returns `false` and an error message. |
+| [`integer(v, msg?)`](#fn-integer) | Returns `true` when `v` is an integer. Otherwise returns `false` and an error message. |
+| [`true(v, msg?)`](#fn-true) | Returns `true` when `v` is exactly `true`. Otherwise returns `false` and an error message. |
+| [`truthy(v, msg?)`](#fn-truthy) | Returns `true` when `v` is truthy. Otherwise returns `false` and an error message. |
+
+### Path Checks
+
+
+
+#### `block(v, msg?)`
+
+Returns `true` when `v` is a block device path. Otherwise returns `false` and an
+error message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.block(".")
+```
+
+
+
+#### `char(v, msg?)`
+
+Returns `true` when `v` is a char device path. Otherwise returns `false` and an
+error message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.char(".")
+```
+
+
+
+#### `device(v, msg?)`
+
+Returns `true` when `v` is a block or char device path. Otherwise returns
+`false` and an error message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.device(".")
+```
+
+
+
+#### `dir(v, msg?)`
+
+Returns `true` when `v` is a directory path. Otherwise returns `false` and an
+error message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.dir(".")
+```
+
+
+
+#### `fifo(v, msg?)`
+
+Returns `true` when `v` is a FIFO path. Otherwise returns `false` and an error
+message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.fifo(".")
+```
+
+
+
+#### `file(v, msg?)`
+
+Returns `true` when `v` is a file path. Otherwise returns `false` and an error
+message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.file(".")
+```
+
+
+
+#### `link(v, msg?)`
+
+Returns `true` when `v` is a symlink path. Otherwise returns `false` and an
+error message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.link(".")
+```
+
+
+
+#### `path(v, msg?)`
+
+Returns `true` when `v` is a valid filesystem path. Otherwise returns `false`
+and an error message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.path("README.md")
+```
+
+
+
+#### `socket(v, msg?)`
+
+Returns `true` when `v` is a socket path. Otherwise returns `false` and an error
+message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.socket(".")
+```
+
+### Registration
+
+
+
+#### `register(name, validator, template?)`
+
+Register or override a validator function by name.
+
+**Parameters**:
+
+- `name` (`string`): Validator name.
+- `validator` (`fun(v:any):(ok:boolean)`): Validator function.
+- `template?` (`string`): Optional default message template.
+
+**Return**:
+
+- `none` (`nil`)
+
+**Example**:
+
+```lua
+validate.register("odd", function(v)
+ return type(v) == "number" and v % 2 == 1
+end, "{{value}} does not satisfy {{expected}}")
+
+ok, err = validate.odd(3) --> true, nil
+ok, err = validate.odd("x") --> false, '"x" does not satisfy odd'
+ok, err = validate(2, "odd") --> false, "2 does not satisfy odd"
+```
+
+> [!NOTE]
+>
+> - If `template` is provided, it becomes the default message template for that
+> validator.
+> - If `template` is omitted, failures use:
+> `{{expected}} expected, got {{got}}`.
+
+### Type Checks
+
+
+
+#### `boolean(v, msg?)`
+
+Returns `true` when `v` is a boolean. Otherwise returns `false` and an error
+message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.boolean(true) --> true, nil
+ok, err = validate.boolean(1) --> false, "boolean expected, got number"
+```
+
+
+
+#### `function(v, msg?)`
+
+Returns `true` when `v` is a function. Otherwise returns `false` and an error
+message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.Function(function() end) --> true, nil
+ok, err = validate.Function(1)
+--> false, "function expected, got number"
+```
+
+
+
+#### `nil(v, msg?)`
+
+Returns `true` when `v` is `nil`. Otherwise returns `false` and an error
+message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.Nil(nil) --> true, nil
+ok, err = validate.Nil(0) --> false, "nil expected, got number"
+```
+
+
+
+#### `number(v, msg?)`
+
+Returns `true` when `v` is a number. Otherwise returns `false` and an error
+message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.number(42) --> true, nil
+ok, err = validate.number("x") --> false, "number expected, got string"
+```
+
+
+
+#### `string(v, msg?)`
+
+Returns `true` when `v` is a string. Otherwise returns `false` and an error
+message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.string("hello") --> true, nil
+ok, err = validate.string(1) --> false, "string expected, got number"
+```
+
+
+
+#### `table(v, msg?)`
+
+Returns `true` when `v` is a table. Otherwise returns `false` and an error
+message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.table({}) --> true, nil
+ok, err = validate.table(1) --> false, "table expected, got number"
+```
+
+
+
+#### `thread(v, msg?)`
+
+Returns `true` when `v` is a thread. Otherwise returns `false` and an error
+message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+co = coroutine.create(function() end)
+ok, err = validate.thread(co) --> true, nil
+ok, err = validate.thread(1) --> false, "thread expected, got number"
+```
+
+
+
+#### `userdata(v, msg?)`
+
+Returns `true` when `v` is a userdata value. Otherwise returns `false` and an
+error message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.userdata(io.stdout) --> true, nil
+ok, err = validate.userdata(1) --> false, "userdata expected, got number"
+```
+
+### Value Checks
+
+
+
+#### `callable(v, msg?)`
+
+Returns `true` when `v` is callable. Otherwise returns `false` and an error
+message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.callable(type) --> true, nil
+ok, err = validate.callable(1) --> false, "callable value expected, got 1"
+```
+
+
+
+#### `false(v, msg?)`
+
+Returns `true` when `v` is exactly `false`. Otherwise returns `false` and an
+error message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.False(false) --> true, nil
+ok, err = validate.False(true) --> false, "false value expected, got true"
+```
+
+
+
+#### `falsy(v, msg?)`
+
+Returns `true` when `v` is falsy. Otherwise returns `false` and an error
+message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.falsy(false) --> true, nil
+ok, err = validate.falsy(1) --> false, "falsy value expected, got 1"
+```
+
+
+
+#### `integer(v, msg?)`
+
+Returns `true` when `v` is an integer. Otherwise returns `false` and an error
+message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.integer(1) --> true, nil
+ok, err = validate.integer(1.5) --> false, "integer value expected, got 1.5"
+```
+
+
+
+#### `true(v, msg?)`
+
+Returns `true` when `v` is exactly `true`. Otherwise returns `false` and an
+error message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.True(true) --> true, nil
+ok, err = validate.True(false) --> false, "true value expected, got false"
+```
+
+
+
+#### `truthy(v, msg?)`
+
+Returns `true` when `v` is truthy. Otherwise returns `false` and an error
+message.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
+
+**Return**:
+
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
+
+**Example**:
+
+```lua
+ok, err = validate.truthy(1) --> true, nil
+ok, err = validate.truthy(false) --> false, "truthy value expected, got false"
+```
+
+## Fields
+
+
+
+### `messages` (`modsValidatorMessages`)
+
+Custom error-message templates for validator failures.
+
+Set `validate.messages.`, where `` is a validator name (for example:
+`number`, `truthy`, `file`).
+
+The error-message template is used only when validation fails and an error
+message is returned.
+
+```lua
+validate.messages.number = "need {{expected}}, got {{got}}"
+ok, err = validate.number("x") --> false, "need number, got string"
+```
+
+**Placeholders**:
+
+- {{expected}}: The check target (for example `number`,
+ `string`, `truthy`).
+- {{got}}: The detected failure kind (usually a Lua type;
+ path validators use `invalid path`).
+- {{value}}: The passed value, formatted for display (strings
+ are quoted).
+
+> [!NOTE]
+>
+> When the passed value is `nil`, rendered value text uses `no value`.
+>
+> ```lua
+> validate.messages.truthy = "{{expected}} value expected, got {{value}}"
+> validate.truthy(nil) --> false, "truthy value expected, got no value"
+> ```
+
+**Default Messages**:
+
+- Type checks: {{expected}} expected, got {{got}}
+- Value checks: {{expected}} value expected, got {{value}}
+- Path checks: {{value}} is not a valid {{expected}} path
+ (for `path`: {{value}} is not a valid path)
+
+> [!NOTE]
+>
+> For path checks, if the value is not a `string`, the message falls back to
+> `messages.string` (as if `validate.string` was called).