---
"@ux-team/tvu-design-system": minor
---

Add the ESLint half of §R20 / §R21 — two new rules, `require-hash-n-semantic` and
`no-hardcoded-overlay`, wired into `incremental` (warn) and `recommended` / `strict`
(error). Until now these two rules existed only as CLI probes in
`audit-product-code.mjs`, which many consumers never run.

- **`require-hash-n-semantic` (R20)** — a `#N` list-item label rendered *from an index*
  (`` `#${i + 1}` `` / `#{{ index + 1 }}` / `'#' + (idx + 1)`) must carry an adjacent
  `#N semantic: sequence` or `#N semantic: name` declaration. The question to answer is
  "is this number generated by position, or does the data carry it?" — generated ⇒
  `sequence` (deleting renumbers); typed in ⇒ `name` (deleting does not). Values are a
  closed set; URL hashes (`` `#${section.id}` ``) and hex colors are not flagged.
- **`no-hardcoded-overlay` (R21)** — an overlay/scrim/mask `background` written as a raw
  `rgba(...)`, or a `backdrop-filter` written as a raw `blur(<N>px)`, is flagged: use
  `var(--mask-overlay)` / `blur(var(--mask-overlay-blur))`, the one sanctioned mask in the
  system. Only fires inside overlay-semantics context, so thumbnail `filter: blur(2px)`
  and `box-shadow … rgba()` are left alone. It deliberately does **not** judge the mask's
  numeric value.

Each rule imports its criterion from the CLI probe rather than reimplementing it, so the
ESLint and CLI halves cannot drift — including the message text, which is the probe's own
hint verbatim.

⚠️ **Two things to know if you use them.** These two rules scan the whole file (their
criteria need cross-line context — R20's declaration may sit on an adjacent line, R21's
overlay selector may sit several lines above the declaration), unlike the other six rules
which scan AST nodes. So in a Vue SFC they also see `<style>` blocks — and inside
`<style>` an `eslint-disable` comment has **no effect**, because `vue-eslint-parser` emits
no comment tokens there. Escape those two with `/* AUDIT-IGNORE-R20: <reason> */` /
`/* AUDIT-IGNORE-R21: <reason> */`, which the shared criterion reads off the line itself.
Standalone `.css` / `.scss` files are not on the ESLint scan face at all; those stay with
the CLI probe.
