288 lines
23 KiB
Markdown
288 lines
23 KiB
Markdown
# UI Phase 12 — Copy, motion & final polish
|
||
|
||
> **Mission:** the closing sweep of the UI chain — make the product read and move as one: a checked-in
|
||
> Persian style guide enforced across both catalogs, the verified copy defects on trust-critical strings
|
||
> fixed, a restrained motion language behind a single reduced-motion gate, an accessibility pass, and a final
|
||
> four-axes QA of everything the chain touched. Copy is the cheapest trust lever Balinyaar has — and today
|
||
> the brand name is spelled two ways, a nurse-facing EVV error reads as "there is no game-entrance", and one
|
||
> BNPL sentence states the inverse of the intended risk allocation.
|
||
>
|
||
> **Track:** frontend · **Depends on:** all prior phases — [0](ui-phase-0-design-language.md) →
|
||
> [11](ui-phase-11-admin-and-partner-console.md) (run this last) · **Unlocks:** ships the chain — one voice, one motion language, verified on all axes.
|
||
> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) and invoke the frontend-designer skill — both are mandatory.**
|
||
|
||
## 1. Context — where this sits
|
||
|
||
Balinyaar is a trust-first product in a market where written register carries weight: families decide whether
|
||
to let a stranger into a parent's home partly on whether the product *sounds* careful. The catalogs are
|
||
already hand-written — the weaknesses are craft, not tone. Verified in `client/messages/fa.json` / `en.json`
|
||
(line numbers spot-checked 2026-07-16; earlier phases may shift them — grep the key, not the line):
|
||
|
||
- **The brand name is spelled two ways:** «بالین یار» (plain space) in 5 keys — `common.brand` (53),
|
||
`auth.customer_title` (625), `auth.select_role_title` (645), `auth.account_error_body` (654),
|
||
`verification.start_body` (663) — vs «بالینیار» (ZWNJ) in 17: login and escrow copy disagree on the name.
|
||
- **تأیید — the most frequent word in a verification product — appears 38× without hamza and 26× with**, no
|
||
pattern (`bank.status_verified_title` vs `admin.ver_pass`); ~64 occurrences to sweep.
|
||
- **Two grammar bugs read as nonsense:** `booking.evv_no_open_check_in` (542) «ورود بازی برای ثبت خروج وجود
|
||
ندارد» and `admin.alert_empty` (1360) «هشدار بازی وجود ندارد» — the indefinite ی attached to the wrong word.
|
||
And a typo sits on a trust chip: `bank.status_verified_chip` (180) reads «تاییدشد» (missing final ه) — the
|
||
chip a nurse stares at while IBAN ownership verification runs.
|
||
- **One en arrow points the wrong way:** `booking.continue_payment` (en 445) is "Continue to payment ←" while
|
||
its sibling `payment.cta_pay` (577) is "…→". Five keys per catalog embed ←/→ literally (445, 577, 632, 633,
|
||
1337) — directional glyphs inside translatable strings are how this bug happens.
|
||
- **fa lacks ICU plurals where en has them:** `search.cta_view_results` (fa 350) is «مشاهده {count} پرستار» —
|
||
the primary search CTA can render «مشاهده ۰ پرستار»; en has ICU but its `=0` case renders "View no nurses".
|
||
- **BNPL copy is banker's language with an ambiguous pronoun:** `bnpl.ownership_note` (857) «…ریسک نکول مشتری
|
||
کاملاً با اوست…» — «نکول» is credit-desk jargon and «با اوست» leaves the reader unsure whether the
|
||
*customer* carries the default risk (the intended meaning: the provider does).
|
||
- **Policy numbers are hard-coded into legally-sensitive copy** the admin config panel can change: the ۷۲-hour
|
||
dispute window (`payouts.explainer_point_2`, 958), the ۲۴-hour cancellation tiers (`refunds.lead_gt_24h/
|
||
lead_lt_24h`, 789–790), the ۷–۱۰-day refund ETA (`refunds.eta_business_days`, 850). One config edit makes
|
||
the UI lie — and **no public config read exists** (`platform_config/*` in `services/admin/` is admin-only).
|
||
- **Motion and live-region a11y are absent, app-wide:** zero `prefers-reduced-motion` handling and zero
|
||
`aria-live` anywhere in `client/src`; `AppIconButton` names icon-only actions solely via Tooltip `title`,
|
||
dropped entirely when disabled (`AppIconButton.tsx:89–95`).
|
||
|
||
**What already exists (do not rebuild):**
|
||
|
||
- Phase 0's token extension (motion/focus/elevation tokens in `theme/tokens.css`) and single-weight icon
|
||
registry with a mirrored chevron — this phase *applies* motion tokens, it does not invent them.
|
||
- Phase 1's shared primitives: `utils/number.ts` `formatNumber` (replacing the 25+ inline `fa-IR` ternaries),
|
||
the state-view kit, `CountdownTimer` v2, route-level `loading.tsx`/`error.tsx`.
|
||
- Full key parity between `fa.json` and `en.json` (28 namespaces) with complete loading/empty/error/confirm/
|
||
toast coverage per namespace, and the trust copy that is already right (keep-list, §5):
|
||
`payment.escrow_notice`, `payouts.explainer_point_1–3`, calming refund-status vocabulary, two-stage
|
||
disclosure copy, the specific verification-failure reasons.
|
||
- Phases 4/6/8/9 may already have fixed items this phase lists conditionally (3.2 address hint, 3.5 pipeline
|
||
naming, 3.7 cities, 3.10 desktop layouts) — **read their reports first** and skip what is done.
|
||
|
||
## 2. Required reading (do this first)
|
||
|
||
- [audit/microcopy.md](audit/microcopy.md) — the 16 copy problems + 7 opportunities this phase executes; its
|
||
keep-list is binding. Then [audit/cross-cutting-ux.md](audit/cross-cutting-ux.md) — motion/a11y/responsive
|
||
findings (AppIconButton, motion absence, desktop phone-column) and its keep-list.
|
||
- Prior phase reports in `../../shared-working-context/reports/` (`ui-phase-0` … `ui-phase-11`) — to learn
|
||
which conditional items are already done and what naming phase 8 chose for the verification journey.
|
||
- `client/messages/fa.json` + `en.json` — read the namespaces you will sweep (booking, verification, payment,
|
||
auth, common, bnpl, search, refunds, payouts, admin, shell, tickets, bank, address).
|
||
- Code (under `client/src/`): `components/common/AppIconButton/AppIconButton.tsx`,
|
||
`components/CountdownTimer/`, `components/EscrowNotice/EscrowNotice.tsx`, `theme/tokens.css` (phase-0
|
||
motion/focus tokens), `utils/number.ts` (phase-1 `formatNumber`), and for 3.10
|
||
`app/[locale]/(private-routes)/(customer)/bookings/checkout/` + `…/(customer)/search/results/page.tsx`.
|
||
- Product: [../../../product/overview/platform-summary.md](../../../product/overview/platform-summary.md)
|
||
§"Glossary" — consult it **before** deciding پرستار/مددجو/بیمار; the cancellation/refund docs under
|
||
[../../../product/business/](../../../product/business/index.md) before touching policy-number copy.
|
||
- The design contract: [../../../.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md)
|
||
(§7 non-negotiables) and `client/CLAUDE.md` "Golden rules". REQ tracker:
|
||
[../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
|
||
— find the tail (REQ-038 at authoring time; earlier UI phases may have appended) and number onward.
|
||
|
||
## 3. Scope — build this
|
||
|
||
### 3.1 The Persian style guide — `client/messages/STYLE.md` + enforcement
|
||
|
||
Write a one-page style guide the catalogs are then swept against, fixing these decisions:
|
||
|
||
- **Brand = «بالینیار» (ZWNJ), always.** Sweep the 5 plain-space keys.
|
||
- **Hamza form «تأیید»** everywhere (~64 occurrences to normalize — the 38 hamza-less ones change).
|
||
- **One form of جستجو.** Standardize on «جستجو» (the 9-key majority; fold in the 2 «جستوجو» keys).
|
||
- **ZWNJ rules:** می + verb, plural ها, compound adjectives («تأییدشده»); codify with examples.
|
||
- **Punctuation & quotes:** Persian «،» / «؛» / guillemets; en uses straight apostrophes (the admin
|
||
namespace's curly 'don’t' normalizes to the file's straight-quote majority).
|
||
- **Domain glossary:** پرستار / بیمار vs مددجو — decide against the product glossary; «مددجو» appears exactly
|
||
once (`booking.patient_label`), so adopt it consistently or drop the one-off. One status vocabulary shared
|
||
by nurse-facing and admin-facing keys (see 3.5).
|
||
- **Shell naming system:** one metaphor per audience class instead of today's four (`shell.*`, fa 56–61:
|
||
اپلیکیشن/نما/کنسول/پرتال). Recommended: end-user shells = «اپلیکیشن» (خانواده، پرستار), back-office
|
||
shells = «کنسول» (مدیریت، همکار).
|
||
- **Register:** formal شما with polite imperatives (کنید) — already consistent; codify so it cannot drift.
|
||
- **Digits policy:** Persian digits in fa for both literals and interpolations (mechanics in 3.3).
|
||
|
||
**Enforcement:** add a ~30-line Node script (`client/scripts/check-copy.mjs`) that greps `fa.json` for the
|
||
banned variants (space-brand, hamza-less تایید, the «بازی» word-boundary trap, جستوجو, میگردد) and exits
|
||
non-zero; wire it as `npm run lint:copy` and into `npm run check`. 60+ scattered findings become a one-time
|
||
fix that cannot regress.
|
||
|
||
### 3.2 Verified copy-bug fixes (each independently shippable)
|
||
|
||
1. `booking.evv_no_open_check_in` → «ورودِ ثبتشدهای برای این ویزیت وجود ندارد؛ ابتدا ورود را ثبت کنید.»
|
||
and `admin.alert_empty` → «هشداری برای رسیدگی نیست.»
|
||
2. `address.line_hint` «…برای یافتن در نیاز دارد» → «هر جزئیاتی که پرستار برای پیدا کردن منزل شما لازم
|
||
دارد.» — **only if phase 9 didn't**. And `bank.status_verified_chip` «تاییدشد» → «تأییدشده».
|
||
3. Archaic passive «میگردد» → «میشود»: `refunds.confirm_restate` (814), `admin.mod_confirm_publish` (1314),
|
||
`admin.cfg_save_confirm_body` (1333) — three occurrences, not one as the audit counted.
|
||
4. `tickets.thread_empty_body` comma splice → «هنوز پیامی نیست. هماهنگی را شروع کنید.»
|
||
5. en normalization: `auth.nurse_subtitle` "licence" → "license" (American throughout — the file already uses
|
||
"center" 25×); straighten the admin namespace's curly apostrophes.
|
||
6. Introduce EVV in Persian once, then abbreviate: a first-occurrence key «ثبت حضور الکترونیکی (EVV) — ورود و
|
||
خروج شما ثبت میشود تا ویزیت بدون اختلاف تأیید شود» on the nurse's visits surface (`booking.evv_*`, fa
|
||
528–535), short chips («ثبت ورود») thereafter — EVV is why the nurse gets paid without arguments; sell it.
|
||
|
||
### 3.3 ICU plurals + designed zero cases
|
||
|
||
Every `{count}` key gets an ICU plural form in **both** catalogs with a designed `=0` case: fa
|
||
`search.cta_view_results` (350), `results_count` (352), `reviews_count` (364), `booking.session_count` (515)
|
||
plus a grep sweep for the rest. Zero states are copy, not numerals — «پرستاری یافت نشد» / "No nurses found";
|
||
also fix en's awkward `=0` label ("View no nurses"). Digits policy: fa interpolations render Persian digits —
|
||
use `{count, number}` / ICU `#` (locale-formatted by next-intl) in message keys, and phase 1's `formatNumber`
|
||
where a raw value is interpolated at a call site; verify one of each renders «۳» not "3".
|
||
|
||
### 3.4 Arrows out of strings
|
||
|
||
Remove the five literal ←/→ glyphs per catalog (`booking.continue_payment`, `payment.cta_pay`,
|
||
`auth.nurse_switch`, `auth.customer_switch`, `admin.cfg_history_change` — fa/en lines 445/577/632/633/1337)
|
||
and fix en 445's wrong-direction arrow by construction: direction moves into auto-mirroring `endIcon` slots
|
||
on the buttons involved (phase 0's chevron via the `AppIcon` registry; the RTL Emotion cache mirrors it).
|
||
`admin.cfg_history_change` «{old} ← {new}» becomes two interpolations joined by a component-rendered mirrored
|
||
arrow icon. Touch each call site; the strings become text-only.
|
||
|
||
### 3.5 Trust-moments copy pass
|
||
|
||
- **OTP screen reassurance:** `auth.customer_subtitle` (626) is just «با شماره موبایل خود وارد شوید» — add one
|
||
warm trust line (verified nurses + escrow, e.g. «پرستاران تأییدشده، پرداخت امن نزد بالینیار»).
|
||
- **Checkout «چرا امن است»:** one line near the pay CTA linking escrow to the dispute process —
|
||
*supplementing*, never replacing, `payment.escrow_notice` (§5). And **post-payment what-happens-next:** the
|
||
confirmation states the next steps in order (nurse notified → coordination ticket → visit-day EVV).
|
||
- **BNPL de-jargon:** rewrite `bnpl.ownership_note` (857) reader-first, plainly allocating risk to the
|
||
provider — e.g. «قسطها را مستقیماً به {provider} میپردازید؛ بالینیار مبلغ کامل را همان ابتدا دریافت
|
||
میکند و اگر قسطی پرداخت نشود، مسئولیت آن با ارائهدهنده است، نه شما و نه پرستار.» «نکول» disappears from
|
||
the catalog.
|
||
- **Disambiguate the verification names:** «احراز هویت» names both the 7-step pipeline (`nav.verification`
|
||
13, `verification.title` 658) *and* the KYC step inside it (`step_identity_kyc` 676, admin 1470). Rename
|
||
the pipeline «تأیید صلاحیت»; the KYC step keeps «احراز هویت». **Coordinate with phase 8's journey naming**
|
||
— if its report already renamed, adopt its term and only sweep stragglers.
|
||
- **One rejected-status vocabulary:** unify رد شد / ناموفق / ردشده into one nurse-facing and one
|
||
admin-facing form (decide in STYLE.md; sweep `verification.*` and `admin.*` together — they cross-reference).
|
||
|
||
### 3.6 Config-served policy numbers (REQ + client single-sourcing)
|
||
|
||
Parameterize the hard-coded policy numbers: `payouts.explainer_point_2` takes `{hours}` (72),
|
||
`refunds.lead_gt_24h`/`lead_lt_24h` take `{hours}` (24), `refunds.eta_business_days` takes `{minDays}`/
|
||
`{maxDays}` (7–10). File a REQ (next free number) for a **public/authenticated policy-config read**
|
||
(dispute-window hours, cancellation tiers + percentages, refund ETA) — verify first that no existing endpoint
|
||
serves it (at authoring time only admin-scoped `platform_config/*` exists). Until served, single-source the
|
||
constants in `client/src/constants/policy.ts` with a comment linking the REQ, and feed the interpolations
|
||
from there. One config edit must never again silently make copy lie.
|
||
|
||
### 3.7 Honest search empty state
|
||
|
||
Replace `search.empty_suggest_city` (361, both catalogs) — «شهر نزدیک دیگری مانند مشهد، اصفهان یا شیراز را
|
||
امتحان کنید» is nonsense advice for a Tehran-launch user — with honest relaxations already next to it (clear
|
||
the district, widen filters) or interpolated actually-covered cities. **Only if phase 4 didn't.**
|
||
|
||
### 3.8 Motion pass — one restrained language
|
||
|
||
Apply phase 0's motion tokens app-wide; introduce nothing springy — this is a calm clinical product:
|
||
|
||
- Page/content transitions: 150–200ms fade + small slide on route-group content; skeleton → content
|
||
crossfade (the phase-1 skeleton twins).
|
||
- Dialog/bottom-sheet enter/exit via `theme.components` defaults (one place, not per-dialog); list-entrance
|
||
restraint — at most a subtle stagger on first paint, never re-animating on refetch.
|
||
- **`prefers-reduced-motion` is respected via a single media-query gate in one place** (a token/theme-level
|
||
switch that zeroes durations) — today the app has zero handling; do not scatter per-component checks.
|
||
|
||
### 3.9 A11y sweep
|
||
|
||
- `AppIconButton`: add an `aria-label` passthrough that defaults from `title` and survives `disabled` (today
|
||
the Tooltip — the only name source — is dropped when disabled; `AppIconButton.tsx:89–95`). Update its test.
|
||
- `aria-live` on the moments that change without focus: `CountdownTimer` (polite), async error/retry regions,
|
||
payment-status polling. Today `aria-live` appears **nowhere** in `client/src`.
|
||
- Disclosure semantics (`aria-expanded` + `aria-controls`) on collapsibles — the earnings `ExplainerCard`
|
||
pattern and any phase-built equivalents.
|
||
- Form label association audit (MUI does this when `label` is used — audit the hand-rolled ones); focus-visible
|
||
coverage check (phase 0 tokenized the ring — verify interactive elements show it); contrast re-check on
|
||
remaining terracotta *text* usages in both schemes.
|
||
|
||
### 3.10 Desktop-aware layouts — the two worst phone-column offenders (scope-boxed)
|
||
|
||
**Only if phases 4/6 didn't already:** above ~900px, (a) checkout (`(customer)/bookings/checkout/`) becomes
|
||
two-column with a sticky order summary; (b) search results (`(customer)/search/results/page.tsx`) uses the
|
||
width (wider cards or list + detail). Nothing else — a full responsive pass is (DEFERRED → post-chain).
|
||
|
||
### 3.11 Final QA — the closing walkthrough
|
||
|
||
Write a per-shell walkthrough checklist (customer funnel, nurse day-of + verification, admin desks, partner
|
||
portal, auth/first-run) × `/fa`+`/en` × light+dark × mobile+desktop. Run it end-to-end; fix what it finds
|
||
(anything non-trivial becomes a report note, not silent scope expansion). Attach the completed checklist to
|
||
the phase report — it is the chain's shipping evidence.
|
||
|
||
## 4. Mocks & seams in this phase
|
||
|
||
None introduced. UI stays mock-tolerant behind the existing `services/{domain}` seams. The one backend gap is
|
||
3.6's policy-config read: a REQ appended to
|
||
[for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) (REQ-001…038 taken at
|
||
authoring time — check the tail for numbers earlier UI phases claimed, and number onward). Until delivered,
|
||
the client single-sources the numbers in `constants/policy.ts`; no mock flag changes.
|
||
|
||
## 5. Critical rules you must not get wrong
|
||
|
||
- **`payment.escrow_notice` and product-mandated strings change only with a flagged product note.** The
|
||
escrow sentence, `payouts.explainer_point_1–3` (especially point 3's BNPL-fee-never-deducted guarantee),
|
||
the two-stage disclosure copy (`booking.notes_hint` / `booking.disclosure_note`), and the verification-failure
|
||
reasons are model trust writing — sweeps may normalize their spelling, never their meaning. If a rewrite
|
||
seems needed, flag it in the report instead.
|
||
- **Don't regress the keep-lists** (both audit files): calming money-status vocabulary («در راه»، «نیازمند
|
||
بررسی»), culturally-tuned gender copy, formal شما, complete per-namespace state strings, admin
|
||
confirm-dialog operational candor, token discipline, RTL `dir="ltr"` islands, Shamsi-first formatting.
|
||
- **en stays hand-written-idiomatic** — no machine-translation tone ("Queue clear — nothing to review" is the
|
||
bar). **Key parity fa/en stays absolute:** every add/rename lands in both catalogs in the same change.
|
||
- **Don't rename stable message keys casually.** A key rename must grep and update every `t('…')` usage; if a
|
||
key's meaning is unchanged, keep its name and change only the value.
|
||
- **Design-contract non-negotiables** that bite here: tokens not hexes (motion durations/easings are tokens,
|
||
not inline magic numbers); RTL logical props (arrows mirror by construction); dark mode on every touched
|
||
surface; MUI v9 API only; co-located tests for touched shared components (`AppIconButton`,
|
||
`CountdownTimer`, `EscrowNotice` all have `.test.tsx`); fetch/cookies rules untouched — no data-layer changes.
|
||
- **Reduced motion is a hard gate, not a nice-to-have** — every animation collapses to no-motion under
|
||
`prefers-reduced-motion: reduce` via the single gate (3.8). **Frontend lane only:** the policy-config gap
|
||
is a REQ, never a `server/` edit.
|
||
|
||
## 6. Definition of Done
|
||
|
||
On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
|
||
|
||
- [ ] `npm run check` green (now including `lint:copy`); `npm run test:ci` green for touched shared
|
||
components (`AppIconButton`, `CountdownTimer`, any collapsible/state component touched).
|
||
- [ ] `client/messages/STYLE.md` exists; `scripts/check-copy.mjs` passes with **zero** banned variants —
|
||
no plain-space brand, no hamza-less تایید, no «بازی» trap, no میگردد, one جستجو form.
|
||
- [ ] `en.json`/`fa.json` in key parity; every `{count}` key has ICU plural + designed `=0` in both catalogs;
|
||
no literal ←/→ remains in either catalog (grep proves it).
|
||
- [ ] `bnpl.ownership_note` plainly allocates default risk to the provider; «نکول» absent from the catalog.
|
||
- [ ] Policy numbers interpolate from `constants/policy.ts`; the policy-config REQ is filed with a number.
|
||
- [ ] `prefers-reduced-motion: reduce` disables all chain-added motion; icon-only buttons have accessible
|
||
names including when disabled; countdown/async-status regions have `aria-live`; touched collapsibles
|
||
expose `aria-expanded`.
|
||
- [ ] Visual verification on the four axes — `/fa` + `/en` × light + dark — and mobile + desktop for 3.8/3.10
|
||
surfaces; the completed 3.11 walkthrough checklist is attached to the phase report.
|
||
|
||
## 7. How to test (what a human can verify after this phase)
|
||
|
||
1. `cd client && npm run check` — passes, including the new copy lint. Temporarily add «بالین یار» to any fa
|
||
value → `lint:copy` fails; revert.
|
||
2. Log in as a customer (`/fa`): login, checkout escrow copy, and refund screens all spell «بالینیار»
|
||
identically; the OTP screen shows the new reassurance line.
|
||
3. Search with filters that match nothing → «پرستاری یافت نشد» (not «۰ پرستار»; not "View no nurses" on
|
||
`/en`); counts render Persian digits on `/fa`.
|
||
4. As a nurse, attempt EVV check-out without a check-in → the corrected sentence (no «ورود بازی»); the bank
|
||
screen's verified chip reads «تأییدشده»; the visits screen introduces «ثبت حضور الکترونیکی (EVV)» once.
|
||
5. On `/en` checkout, "Continue to payment" carries a mirrored end-icon pointing forward (→ in LTR, mirrored
|
||
on `/fa`); grep both catalogs for `←|→` → zero hits.
|
||
6. Open the BNPL comparison → the ownership note reads plainly, no «نکول», risk clearly on the provider.
|
||
Grep the catalogs for «۷۲ ساعته» → none; the keys take `{hours}` from `constants/policy.ts`; the REQ
|
||
exists in the tracker.
|
||
7. Navigate between routes → 150–200ms calm fade/slide, skeletons crossfade into content; enable reduced
|
||
motion in DevTools → everything appears instantly.
|
||
8. Inspect the a11y tree on a screen with icon-only buttons: each has a name, including a disabled one; a
|
||
running countdown announces politely (`aria-live`).
|
||
9. Run the 3.11 checklist across all shells on the four axes + both widths; every row checked or fixed.
|
||
|
||
## 8. Hand off & document (close the phase)
|
||
|
||
- Update `client/CLAUDE.md`: mention `messages/STYLE.md` + `lint:copy` in the i18n/Golden-rules sections and
|
||
the reduced-motion gate location; update "Project Structure" if `scripts/` or `constants/policy.ts` are new.
|
||
- Write the frontend report at `../../shared-working-context/reports/ui-phase-12-report.md`: the style-guide
|
||
decisions taken (hamza form, جستجو form, shell naming, status vocabulary, digits policy), swept-key counts,
|
||
conditional items skipped because an earlier phase did them, the completed QA checklist, and anything the
|
||
walkthrough found but deferred. List the REQ(s) filed with their numbers.
|
||
- Save a memory note per operating-rules §8: the chain is complete — style guide + copy lint now enforce
|
||
Persian orthography; motion and a11y gates exist; record the STYLE.md decisions so future copy follows them.
|