=== EffortLess Simple Reviews Editor ===
Contributors: domclic
Tags: reviews, ratings, star rating, testimonials, infinite scroll
Requires at least: 6.2
Tested up to: 7.0
Requires PHP: 8.0
Stable tag: 2.2.3
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

A simple reviews system with front-end submission, admin approval, and responsive card grid display with infinite scroll.

== Description ==

EffortLess Simple Reviews Editor lets visitors submit reviews (pending admin approval) and displays published reviews in a responsive card grid with infinite scroll.

**Features:**

* Front-end review submission form with star rating, name, title, and review text fields
* Admin approval workflow — all submissions are set to "Pending"
* Responsive card grid with configurable columns
* Infinite scroll (IntersectionObserver) for seamless loading
* Spam protection: honeypot field + rate limiting (1 submission per 60 s)
* Accessible star rating with keyboard navigation
* One-time-use review links — send unique links to clients
* Programmatic link generation via `elsre_create_review_link()`
* No external dependencies — pure CSS stars, vanilla JavaScript
* i18n ready — `.pot` translation template included

== Shortcodes ==

= [elsre_reviews] — Display Reviews Grid =

Place this shortcode on any page or post to display published reviews.

Attributes:

* `per_page` (default: `6`) — Reviews per load batch
* `columns` (default: `3`) — Grid columns on desktop (1–6)
* `ids` (default: empty) — Comma-separated review IDs; disables pagination
* `order` (default: `desc`) — Sort order: `desc` for newest first, `asc` for oldest first
* `star_size` (default: `18px`) — Star icon size (any CSS unit)
* `text_size` (default: `15px`) — Review text font size
* `name_size` (default: `14px`) — Reviewer name font size

Examples:

    [elsre_reviews]
    [elsre_reviews per_page="9" columns="3"]
    [elsre_reviews ids="12,45,78" columns="2"]
    [elsre_reviews star_size="28px" text_size="18px" name_size="16px"]

= [elsre_form] — Review Submission Form =

Place this shortcode on any page or post to display the submission form.

Attributes:

* `require_token` (default: `no`) — Set to `yes` to require a one-time-use link token
* `star_size` (default: `32px`) — Star icon size in the rating picker

Examples:

    [elsre_form]
    [elsre_form require_token="yes"]
    [elsre_form star_size="48px"]

== One-Time Review Links ==

1. Create a page with `[elsre_form require_token="yes"]`.
2. Go to **Reviews → Review Links** in the admin.
3. Click **Generate Link** (optionally add a label for your reference).
4. Copy the link and send it to your client.

Each link works only once. After the client submits their review, the link is marked as used.

== Developer Integration ==

Generate review links programmatically from your own plugin or theme:

    $link = elsre_create_review_link( 'client@example.com' );
    $link = elsre_create_review_link( 'client@example.com', 'John Doe', 42 );

* First parameter: client email (unique identifier — reuses existing unused token if found)
* Second parameter: optional label for admin reference
* Third parameter: optional page ID containing `[elsre_form]` (auto-detected if omitted)
* Returns the full URL with token, or empty string if no form page found

Check availability before calling: `function_exists( 'elsre_create_review_link' )`

== Installation ==

1. Upload the `effortless-simple-reviews-editor` folder to `/wp-content/plugins/`.
2. Activate the plugin through the **Plugins** menu in WordPress.
3. A new **Reviews** menu (star icon) appears in the admin sidebar.
4. Add `[elsre_reviews]` to a page to display reviews.
5. Add `[elsre_form]` to a page to allow visitors to submit reviews.
6. For one-time links: use `[elsre_form require_token="yes"]`, then generate links via **Reviews → Review Links**.

== Frequently Asked Questions ==

= Where do submitted reviews go? =

All submissions are saved with "Pending" status. Go to **Reviews** in the admin to approve them.

= Can I display specific reviews only? =

Yes, use the `ids` attribute: `[elsre_reviews ids="12,45,78"]`

= How do I translate the plugin? =

Copy `languages/elsre.pot` to `languages/elsre-{locale}.po` (e.g. `elsre-fr_FR.po`), translate, then compile to `.mo`.

== Screenshots ==

1. **Reviews list** — WordPress admin list with rating, reviewer name, and date columns.
2. **Review edit screen** — meta box with star picker, name, text, and date fields.
3. **Review Links page** — generate, copy, and manage one-time-use review links.
4. **Front-end reviews grid** — responsive card grid with infinite scroll.
5. **Front-end submission form** — star rating picker, name, and review text fields.

== Admin Pages ==

= Reviews List (Dashboard → Reviews) =

All submitted reviews are listed here as a standard WordPress post list.
Extra columns show the key review data at a glance:

    ┌──────────────┬────────┬──────────────┬─────────────┬────────────┐
    │ Title        │ Rating │ Reviewer     │ Date        │ Status     │
    ├──────────────┼────────┼──────────────┼─────────────┼────────────┤
    │ Great hotel! │ ★★★★★  │ Jane Doe     │ 2026-03-01  │ Pending    │
    │ Good service │ ★★★★☆  │ John Smith   │ 2026-02-28  │ Published  │
    └──────────────┴────────┴──────────────┴─────────────┴────────────┘

Publish a review to make it appear on the front end. Leave it as Pending to
keep it hidden until you have reviewed it.

= Review Edit Screen =

Each review has a **Review Details** meta box below the title field:

    ┌─────────────────────────────────────────┐
    │ REVIEW DETAILS                          │
    ├──────────────┬──────────────────────────┤
    │ Reviewer Name│ Jane Doe                 │
    │ Review Title │ Perfect weekend getaway! │
    │ Rating       │ ★ ★ ★ ★ ★               │
    │ Review Text  │ Fantastic experience...  │
    │ Review Date  │ 2026-03-01               │
    └──────────────┴──────────────────────────┘

The star picker is fully keyboard-accessible (arrow keys, Enter, Space).

= Review Links Page (Dashboard → Reviews → Review Links) =

Generate one-time-use links to send to specific clients:

    ┌──────────────────────────────────────────────────────────────┐
    │ GENERATE NEW LINK                                            │
    │  Client Email  [client@example.com          ]               │
    │  Label         [e.g. John's booking         ]  (optional)   │
    │  Form Page     [Leave a Review ▾            ]               │
    │                [ Generate Link ]                            │
    └──────────────────────────────────────────────────────────────┘

    ┌──────────────────────────────────────────────────────────────────────┐
    │ ALL LINKS                                                            │
    ├──────────────┬───────────┬────────┬────────────┬────────┬───────────┤
    │ Email        │ Label     │ Status │ Created    │ Used   │ Actions   │
    ├──────────────┼───────────┼────────┼────────────┼────────┼───────────┤
    │ jane@...     │ Jane Doe  │ Active │ 2026-03-01 │ —      │ Copy Link │
    │ john@...     │ John Smith│ Used   │ 2026-02-28 │ Mar 01 │ Expired   │
    └──────────────┴───────────┴────────┴────────────┴────────┴───────────┘

After generating, a success banner shows the link ready to copy:

    ┌─────────────────────────────────────────────────────────────┐
    │ ✓  Link generated! Send this link to your client:           │
    │   https://example.com/review/?elsre_token=abc123  [ Copy ] │
    └─────────────────────────────────────────────────────────────┘

== Changelog ==

= 2.2.3 =
* Security: added the `elsre_require_token` filter so sites can enforce invitation-only reviews server-side — when it returns true, tokenless submissions to `admin-ajax.php` are rejected (the `require_token="yes"` shortcode attribute only hid the form in the browser).
* Fix: `elsre_create_review_link()` now keeps a reused token's stored `page_id` in sync with the link it builds, matching the admin Generate Link flow.
* Code: token rollback after a failed insert is now guarded so it only runs for token-based submissions; shared review-query and ID-parsing logic extracted to `ELSRE_Shortcodes` helpers; flash-notice transients read via a single `consume_transient()` helper.

= 2.2.2 =
* Code: removed `plugins_api` filter and associated `plugin_row_meta` "View details" link — WordPress.org hosting serves plugin information natively; the local interceptor was flagged as an unauthorized update/phone-home mechanism.

= 2.2.1 =
* Security: all table-name SQL references migrated from `esc_sql()` + backtick interpolation to the `%i` identifier placeholder (WordPress 6.2+).
* Security: `count_tokens()` now wraps `$wpdb->get_var()` in `prepare()` instead of passing a raw SQL string.
* UX: invalid email on the Generate Link form now shows an admin error notice instead of silently ignoring the input.
* Fix: `uninstall.php` now also removes `elsre_error_*` transients created by the email validation notice.

= 2.2.0 =
* Fix: token is now restored (un-consumed) when `wp_insert_post()` fails after atomic consume, so the review link remains valid and the client can retry.
* Fix: `uninstall.php` now also removes `_transient_timeout_` records for `elsre_generated_`, `elsre_deleted_`, and `elsre_form_pages` transients, preventing orphan rows in `wp_options`.
* Performance: `get_form_pages()` result is now cached for one hour via transient (invalidated on `save_post` / `delete_post`).
* Performance: Review Links admin page now paginates token rows (50 per page) instead of loading all rows at once.
* Code: `render_admin_columns()` switch now has an explicit `default` branch.
* Code: `render_form()` passes the template variable via `extract()` to match the `load_card_template()` pattern.
* Security: removed the descriptive `<!-- Honeypot field -->` HTML comment from the front-end form.

= 2.1.9 =
* Fix: `post_title` auto-generation now only runs for new posts or posts with an empty/Auto Draft title — existing reviews with a manually set title are never overwritten.

= 2.1.8 =
* UX: removed the native "Add title" input from the review edit screen — `post_title` is now auto-generated from "Reviewer Name – Review Title" and remains visible in the admin list table.

= 2.1.7 =
* UX: admin "Review Details" meta box fields reordered to match the front-end form: Rating → Reviewer Name → Review Title → Review Text → Review Date.
* Fix: `maxlength="100"` added to Reviewer Name input in admin meta box.
* Fix: `maxlength="1000"` added to Review Text textarea in admin meta box.
* Security: server-side `mb_strlen` / `mb_substr` guards added for reviewer name (100 chars) and review text (1000 chars) in `save_meta()`.

= 2.1.6 =
* Code: removed `load_plugin_textdomain()` call — WordPress 4.6+ loads translations automatically; the explicit call triggered a PluginCheck warning.
* Code: `$token` variable in `review-form.php` template renamed to `$elsre_token` to satisfy WordPress global variable prefix requirement.

= 2.1.5 =
* Security: token now consumed **before** the review post is inserted — eliminates race window that allowed token reuse on server crash.
* Security: REST API read access restricted to users with `edit_posts` capability via `ELSRE_REST_Controller` — review content (including pending) is no longer publicly readable via the REST API.
* Security: token delete action changed from a GET link to a POST form, preventing accidental deletion by browser prefetch or link scanners.
* Security: `handle_admin_actions()` now checks `current_user_can()` before reading any request parameters.
* Fix: admin meta box now reads `post_content` first (authoritative) and falls back to `_elsre_review_text` meta, matching the documented architecture and preventing translated content from being overwritten on save.
* Fix: `load_plugin_textdomain()` added to `elsre_init()` so translations load correctly on self-hosted installs.
* Fix: `response.data` null guard added on JS success path to prevent TypeError when server returns `{success:true, data:null}`.
* Fix: success message receives focus after form submission so keyboard and screen-reader users are not left on the hidden form.
* Fix: `submit_button()` no longer receives a pre-escaped label — prevented double-escaping of special characters.
* Code: `ms_gpt_translatable_post_types` filter converted to a named function so third parties can call `remove_filter()`.
* Code: `uninstall.php` `DROP TABLE` uses `%i` identifier placeholder (WordPress 6.2+) instead of `esc_sql()` workaround.
* CSS: `prefers-reduced-motion` media query added — disables spinner animation and CSS transitions for users who prefer reduced motion.

= 2.1.4 =
* Feature: live character counter on Name, Title, and Review fields — green while typing, orange within 5% of the limit, red at the limit. Counter is hidden until the user starts typing. Accessible via `aria-live` and `aria-describedby`.

= 2.1.3 =
* Fix: `uninstall.php` — `DROP TABLE` query now uses `esc_sql()` inline instead of a `$table` variable, resolving PluginCheck `DirectDB.UnescapedDBParameter` warning.

= 2.1.2 =
* Fix: `uninstall.php` — tokens table `DROP` query now guarded by a `preg_match()` pattern check on the table name, resolving PluginCheck `DirectDB.UnescapedDBParameter` warning.

= 2.1.1 =
* Fix: assets and script data (`elsreData`) now enqueued during `wp_enqueue_scripts` instead of inside the shortcode render — prevents "elsreData is not defined" JS errors caused by caching and script-optimisation plugins (LiteSpeed, WP Rocket, Autoptimize, etc.).
* Fix: form hidden on successful submission instead of calling `form.reset()` — resolves permanently disabled submit button on Safari/WebKit after token-based submissions.
* Fix: XHR handlers now cover all failure paths (onerror, ontimeout, onabort) with a 30-second timeout, so the submit button is always re-enabled even if the server does not respond.

= 2.1.0 =
* Feature: review title field added to the submission form, review cards, and admin meta box.
* Change: review text maximum length reduced from 2000 to 1000 characters.
* Change: submission form field order is now Rating → Name → Title → Review.

= 2.0.1 =
* Code: PHPCS/WPCS zero errors, zero warnings. Alignment fixes auto-corrected by phpcbf. Rephrased doc comment to satisfy capitalisation rule. Removed unused `$blog_id` parameter from `elsre_uninstall_site()`. Added `PreparedSQL` ignore annotation for the dynamic-placeholder query in `get_form_pages()`.

= 2.0.0 =
* Security: REST API meta fields now require `edit_posts` capability via `auth_callback` — previously any authenticated user could read review meta via the REST API.
* Security: `get_client_ip()` now defaults to `REMOTE_ADDR` only. Proxy header trust (`X-Forwarded-For` / `X-Real-IP`) requires explicit opt-in via the `elsre_trust_proxy_headers` filter to prevent rate-limit bypass on sites not behind a reverse proxy.
* Security: `inject_post_content_before_save()` now checks `current_user_can()` in addition to nonce verification before modifying `post_content`.
* Fix: double-escaping of translatable submit button label — `esc_attr__()` replaced with `__()` to prevent corrupted output for translations containing apostrophes (e.g. French).
* Fix: `uninstall.php` now loops over all sites on multisite network installs, cleaning up posts, transients, tokens table, and options for each site.
* Fix: token reuse now updates the stored `page_id` when the admin selects a different form page, keeping the token record consistent with the generated URL.
* Fix: `wp_create_nonce()` is now called only when a shortcode is actually rendered on a page, not on every front-end page load.
* Fix: copy-to-clipboard button now only shows "Copied!" when the clipboard write actually succeeded.
* Code: `load_card_template()` uses `extract()` with an explicit named array instead of relying on PHP variable scope inheritance across `include`.
* Code: `render_admin_columns()` now applies `esc_attr()` to inline star color values (WPCS compliance).
* New: `elsre_form_page_post_types` filter — allows custom post types to appear in the form page dropdown (useful for page-builder custom post types).
* New: `elsre_trust_proxy_headers` filter — opt-in to reading `X-Forwarded-For` / `X-Real-IP` on sites behind a trusted reverse proxy.

= 1.9.5 =
* Fix: esc_sql() applied to table variable before interpolation in consume_token() query.

= 1.9.4 =
* Fix: JS — response.data null-guard added before accessing response.data.html in infinite scroll callback.
* Fix: JS — IntersectionObserver callback now checks entries.length before accessing entries[0].
* Fix: JS — hardcoded 'Submit Review' fallback replaced with translatable elsreData.i18n.submit (added to wp_localize_script).
* Fix: Accessibility — individual star spans in review-card.php now have aria-hidden="true"; the parent div already carries the full aria-label.
* Fix: readme.txt — stale version number 1.4.4 updated in plugin ASCII art example.

= 1.9.3 =
* Code: PHPCS/WPCS — zero errors, zero warnings. Fixed short ternary operator (replaced ?: with explicit if/else), added missing docblock short description, fixed indentation in review-card.php template. Added extensions and exclude-pattern to phpcs.xml to scope checks to PHP only.

= 1.9.2 =
* Fix: SQL table names now consistently wrapped in backticks across all queries in class-elsre-tokens.php and uninstall.php.
* Fix: strtotime() return value now validated before passing to wp_date() — prevents current date/time showing for reviews with missing or malformed date meta.
* Fix: missing esc_attr() on CSS class output in review-card.php (WPCS compliance).
* Fix: replaced printf(esc_html__(...), '<code>') pattern with echo wp_kses_post(sprintf(__(...))) — correct approach for substituting HTML into translatable strings.
* Fix: added PHP 8.0 union type hint false|object on the $api parameter of elsre_plugins_api_info().

= 1.9.1 =
* Fix: blank "Add New Review" screen in WordPress 6.9+ — `replace_editor` filter returning `true` caused WP 6.9 to skip the entire edit form, showing only the admin footer. Replaced with CSS-based editor suppression (`.post-type-elsre_review #postdivrich` etc.).
* Fix: added `use_block_editor_for_post` filter alongside `use_block_editor_for_post_type` for belt-and-suspenders block editor disabling.
* New: plugin details modal — "View details" link on the Plugins page showing description, shortcode docs, installation guide, and changelog.

= 1.9.0 =
* Fix: editor suppression switched from `user_can_richedit` filter to `replace_editor` filter for WordPress 6.x compatibility.

= 1.8.3 =
* Fix: removed `postcustom` meta box (Custom Fields) from review edit screen to keep the admin UI clean.

= 1.8.2 =
* Fix: translation reverting to source — meta fallback in inject_post_content_before_save now only fires when post_content is genuinely empty, never when translated content is already present.
* Fix: TinyMCE no longer appears on review edit screen (user_can_richedit filter).

= 1.8.1 =
* Fix: restored plain-textarea UI for review text (no TinyMCE, no image uploads). Native content editor hidden via remove_meta_box() while editor stays in CPT supports.
* Fix: removed sync_content_from_meta — its wp_update_post() fired a second save_post which triggered the translator twice, causing random translation results.
* inject_post_content_before_save is the single reliable sync point.

= 1.8.0 =
* Fix: `editor` added to CPT supports — translation plugins check post_type_supports before including post_content in jobs. Without it, post_content was silently skipped. Review text now translates correctly.
* Block editor disabled for review posts; classic editor used. The content area holds the review text directly, like a standard post.
* Removed "Review Text" textarea from the structured meta box — it is now the native content field. Reviewer name, rating, and date remain in the meta box.
* Removed inject_post_content_before_save and sync_content_from_meta hooks — no longer needed with native editor support.
* Backward compat: card template still falls back to _elsre_review_text meta for reviews created before 1.8.0.

= 1.7.1 =
* Security: token consumption is now atomic (UPDATE WHERE is_used = 0) — prevents race condition where two simultaneous requests with the same token could both succeed.
* Security: REST API requests excluded from post_content restore hooks — translation plugins updating via REST no longer risk source-language overwrite.
* Security: server-side max-length validation added for reviewer name (100 chars) and review text (2000 chars).
* Security: REMOTE_ADDR validated as IP before use in rate limiting.
* Fix: uninstall.php now cleans up admin-notice transients.

= 1.7.0 =
* Requires PHP 8.0+ and WordPress 6.9+.
* PHP 8.0 type hints added to all methods and global functions.
* `match` expressions replace if/else chains in token validation, AJAX order parsing, and error messages.
* `wp_date()` replaces `date_i18n()` throughout.
* Removed all `phpcs:ignore NonceVerification` suppressions: nonce verified directly in `inject_post_content_before_save()`; programmatic saves detected via `$_SERVER['REQUEST_METHOD']`; admin notices use per-user transients instead of unverified `$_GET` params.
* Template variables renamed with `elsre_` prefix — removes all `phpcs:ignore NonPrefixedVariableFound` suppressions.

= 1.6.3 =
* Fix: Plugin URI updated from example.com to domclic.com.
* Fix: removed deprecated load_plugin_textdomain() call.
* Fix: esc_sql() applied to table name in get_all_tokens().
* Fix: phpcs ignore on loop variable $i and $submit_label in templates.

= 1.6.2 =
* i18n: text domain changed from `elsre` to `effortless-simple-reviews-editor` across all PHP files to match WordPress.org convention.

= 1.6.1 =
* Fix: infinite re-translation loop with multisite translation plugins — `inject_post_content_before_save` and `sync_content_from_meta` now skip programmatic saves (no `$_POST`) so translated `post_content` on destination sites is never overwritten with source-language meta.

= 1.6.0 =
* Code: full PHPCS/WPCS compliance — zero errors, zero warnings.
* Fix: Yoda conditions in `validate_token()` and token list rendering.
* Fix: renamed `$posts`/`$post_id` in `uninstall.php` to avoid overriding WordPress globals.
* Fix: suppressed false-positive nonce warnings on read-only redirect params.
* Fix: alignment and whitespace issues auto-corrected by phpcbf.

= 1.5.9 =
* Fix: `$id_array` undefined variable notice when `[elsre_reviews]` is used without the `ids` attribute.
* Fix: replaced `gmdate()`+`strtotime()` with `date_i18n()` — dates now respect WordPress timezone and locale.
* Fix: rate limiting skips gracefully when client IP cannot be determined instead of hashing an empty string.
* Fix: AJAX `per_page` capped at 100; `page` capped at 1000 to prevent DB abuse.
* Fix: deprecated `bigint(20)` syntax replaced with `BIGINT UNSIGNED` in token table (DB version 1.2).
* Fix: front-end form now has `method="post"` for valid HTML.

= 1.5.8 =
* Compatibility: hooks `ms_gpt_translatable_post_types` filter so EffortLess Multisite Auto Translate picks up the non-public `elsre_review` CPT and syncs reviews across sites.

= 1.5.7 =
* Fix: PHP Notice "post type elsre_review is not registered" on REST API capability checks — `register_meta_fields()` now hooks to `init` at priority 11, after `register_post_type()` at priority 10.

= 1.5.6 =
* REST API support enabled on the `elsre_review` post type.
* All custom meta fields registered with `show_in_rest: true` for REST-based translation plugins.
* Added `custom-fields` to CPT supports.

= 1.5.5 =
* Fix: multisite translation plugins were receiving empty `post_content` because our sync hook ran after the translation plugin's `save_post` hook.
* New `wp_insert_post_data` filter injects review text into `post_content` before the database write, so translation plugins always sync the correct content.

= 1.5.4 =
* Fix: `$tokens_handler` variable scope bug in `submit_review()` — was potentially undefined at token consumption.
* Fix: `wp_insert_post()` zero-return now treated as failure (not just `WP_Error`).
* Fix: removed `$wpdb->prepare()` with no placeholders in `get_all_tokens()`.
* Security: CSS size attributes on shortcodes now validated against a unit whitelist.

= 1.5.3 =
* Fix: infinite recursion — removed `wp_update_post()` from `save_meta()`.
* Fix: WordPress was clearing `post_content` on every admin save (CPT without editor). `sync_content_from_meta()` now correctly restores it.
* Replaced remove/re-add hook pattern with a static `$syncing` flag.

= 1.5.2 =
* Fix: quick edit and bulk edit now correctly sync `post_content` from `_elsre_review_text` meta.
* Infinite-loop guard on the sync hook.

= 1.5.1 =
* Translation compatibility: review text now stored in `post_content` so multisite translators and translation plugins handle it natively.
* Card display reads from `post_content` first, falls back to meta for reviews created before this version.
* Admin meta box keeps `post_content` in sync when review text is edited.

= 1.5.0 =
* New `order` attribute on `[elsre_reviews]`: `order="desc"` (newest first, default) or `order="asc"` (oldest first).
* Sort order preserved across infinite scroll loads.

= 1.4.4 =
* Security: replaced MD5 with SHA-256 for rate-limit IP hashing.
* Security: proxy-aware IP detection — reads `X-Forwarded-For` / `X-Real-IP` headers with validation before falling back to `REMOTE_ADDR`.
* Security: moved inline `onclick` confirm on delete links to `addEventListener` in JS.
* Security: nonce verified before processing delete action; added `token_id` guard.
* Security: validate generated page ID exists before issuing a token.
* Code: `get_all_tokens()` SQL query now uses `$wpdb->prepare()`.
* Code: date validation uses `DateTime::createFromFormat()` for strict round-trip check.
* Code: `ELSRE_Shortcodes::load_card_template()` is now static — no extra class instantiation in AJAX handler.

= 1.4.3 =
* Plugin renamed to EffortLess Simple Reviews Editor to match the `ELSRE_` prefix convention.

= 1.4.2 =
* i18n ready: `load_plugin_textdomain()` and `.pot` template file for translations.

= 1.4.1 =
* Review title now displayed in the card (between stars and review text).

= 1.4.0 =
* Customizable sizes: `star_size`, `text_size`, `name_size` attributes on `[elsre_reviews]`.
* Customizable star size on `[elsre_form]` via `star_size` attribute.
* Sizes powered by CSS custom properties for easy theming.

= 1.3.0 =
* Client email is now the unique identifier for review links.
* If an unused link exists for the same email, it is reused (no duplicates).
* Email column added to the tokens table and admin Review Links page.
* `elsre_create_review_link()` first parameter is now `$email`.

= 1.2.0 =
* New `elsre_create_review_link()` PHP function for programmatic link generation.
* Use it from your own plugin to automatically include a review link in emails.

= 1.1.0 =
* One-time-use review links: generate unique links for clients via Reviews → Review Links.
* New `require_token` attribute on `[elsre_form]` shortcode.
* Admin page to generate, copy, and manage review links.
* Token consumed after submission — link can only be used once.

= 1.0.0 =
* Initial release.
* Custom post type for reviews with admin meta boxes.
* `[elsre_reviews]` shortcode with infinite scroll and responsive grid.
* `[elsre_form]` shortcode with AJAX submission, honeypot, and rate limiting.
