Stedica Surveys — full changelog
================================
wp.org truncates a readme changelog past ~4,500 characters, so readme.txt
carries only the current release and the full history lives here.

== Changelog ==

= 5.16.5 =

Sanitization fix required by the WordPress.org plugin review. No database
change, no new setting.

`Stedica_Frontend::decode_json_input()` decoded a request value and returned the
tree untouched. It is reached from `ajax_save_answer()` with `$_POST['value']`
and `$_POST['analysis']`, so an autosaved structured answer landed in
`value_json` with no sanitization at all — while the identical answer arriving
through `ajax_submit_survey()` was run through
`stedica_surveys_sanitize_decoded_json()` first. Two paths, one destination,
different treatment; the autosave path was the one missed when that sanitizer
was introduced in 5.16.1, because the sanitize call was added at each call site
and this path decodes inside a helper.

Every return path of that helper now sanitizes:

* A decoded array or object goes through `stedica_surveys_sanitize_decoded_json()`.
* A value that is not a string at all — `value[]=x` arrives as an array and
  never reaches `json_decode()` — is sanitized as a tree instead of passing
  straight through.
* Plain text, and text that looked like JSON but failed to parse, go through
  `sanitize_textarea_field()`, matching what the tree sanitizer applies to
  string leaves. A value is now cleaned the same way whichever branch it takes.

**Lesson worth keeping: putting the sanitize call at each call site left a hole
the moment one path decoded inside a helper.** The guarantee belongs with the
decode, not next to it.

= 5.16.4 =

Two fixes required by the WordPress.org plugin review, plus the family version
match. No database change.

**The `wp-auth-check` deregistration is gone.** An `init` hook ran
`wp_deregister_script( 'wp-auth-check' )` on every admin request. It was added
to silence WP_Scripts dependency notices raised on stacks that enqueue that
script before heartbeat is registered — but `wp-auth-check` is what shows the
"session expired, log in again" modal, so the workaround disabled WordPress's
session-expiry check across the entire dashboard, for every screen and every
other plugin, on any site where this plugin was active. A cosmetic notice
raised outside this plugin is never a reason to switch off a core security
affordance site-wide. Do not re-add it.

**Printed survey markup goes through KSES.** The block's `render_callback` and
the Elementor widget both handed `do_shortcode( '[stedica_survey]' )` output
straight to WordPress to print. Both now filter it through
`Stedica_Frontend::kses_survey_markup()`, which applies `wp_kses()` with an
explicit element allowlist, extensible by add-ons through the
`stedica_surveys_allowed_survey_html` filter.

No `style` attribute is allowed, and none is needed: the login-required gate was
the only markup here carrying inline styles, and they moved into
`assets/css/stedica-survey.css`. KSES filters style attributes through
`safecss_filter_attr()`, which drops properties outside its own allowlist
without complaining — `object-fit` among them — so leaving them inline would
have been a silent layout bug. One of the new rules uses an element selector
(`a.stedica-btn`) on purpose: the inline style it replaced outranked theme link
styling, and a plain class selector would only tie with it.

The previous code comment defended returning it unfiltered on the grounds that
the survey is a form and escaping would strip its controls. **That was wrong
about this render path.** `render_survey()` returns a wrapper, a skip link, a
heading, the spam honeypot and an empty mount node; `stedica-survey.js` builds
every question client-side. There were no form controls in that string to
protect. `wp_kses_post()` is still not usable here — it drops the
`<link rel="manifest">` that offline surveys need — which is why the allowlist
is explicit rather than borrowed.

Version-matched with Stedica Surveys Premium 5.16.4, which puts the Stedica icon
on the one premium screen that was still drawing a plain heading.

= 5.16.3 =

Maintenance release. No functional change to this plugin, and no database
change.

Version-matched with Stedica Surveys Premium 5.16.3, which annotates three
Plugin Check warnings on code its own 5.16.2 introduced. Nothing in this package
was involved, so nothing here changed.

= 5.16.2 =

Maintenance release. No functional change to this plugin, and no database
change.

Version-matched with Stedica Surveys Premium 5.16.2, which is where the work in
this release actually landed: that package cleared its first Plugin Check run,
moved off heredoc syntax, and gained translation loading of its own. Nothing in
this package needed to change for any of it, so nothing did.

One copy fix does ride along: the plugin header Description and the readme
summary line both said "16 question types" while the registry has always had
seventeen and the readme's own feature list already said 17. `signature` was
missed when the count was last written by hand. No code path changes.

= 5.16.1 =

Resubmission to WordPress.org after the 5.16.0 review. Every item the reviewer
raised is addressed: `Tested up to` declared in readme.txt only; all JavaScript
enqueued with no inline `<script>` tags; request JSON sanitized after decoding
at every read point, including the builder autosave path that had stored the raw
request string; no hardcoded `/wp-content/...` paths; the shortcode session
output buffer paired and closed on `shutdown`; and survey backups moved under
the uploads directory, with existing backups copied forward.

Note for anyone reading this history later: no version in the 5.16 line has been
published on WordPress.org. 5.16.0 was reviewed and rejected, and 5.16.1 through
5.16.4 are resubmission builds.

= 5.10.11 =

Security fix, made before the plugin was ever published. No release carrying
this flaw was distributed to anyone — the affected builds only ever ran on
stedica.com for testing.

* Session cookies are now authenticated by their 128-bit token only. A legacy
  code path also accepted a bare integer cookie and matched it against
  session_id. Because session ids are sequential and an integer carries no
  secret, anyone could set that cookie to another respondent's session and be
  treated as its owner - reading the answers already entered, writing new ones,
  and submitting the survey on their behalf. The same path existed in the photo
  upload handler, allowing files to be attached to someone else's session.
* Sessions created before tokens existed are already backfilled on activation.
  A respondent still holding an old integer cookie simply starts a new session;
  the stale cookie is now cleared rather than trusted.

= 5.10.10 =

Clears the last three Plugin Check findings.

* "Tested up to" is now 7.1 in the plugin header as well as the readme. The
  plugin header overrides the readme, so the two must agree.
* Removed a stray phpcs:enable in the privacy exporter that switched the
  interpolated-SQL sniff back on partway through the file, and two ignore
  comments beside it that never applied — they sat on the line above the
  query, while the sniff reports on the line holding the SQL string.

= 5.10.9 =

Resolves every error and warning reported by the WordPress Plugin Check plugin
(658 findings across 28 files).

Fixed in code:

* Output escaping now happens at the point of output in seven places, rather
  than at assignment where a static analyser cannot follow it.
* Exception messages raised by the schema processor are escaped before display.
* unlink() replaced with wp_delete_file(); rand() replaced with wp_rand().
* error_log() calls are gated behind WP_DEBUG.
* Cookie, $_SERVER and honeypot reads are unslashed and sanitised at the point
  of read.
* Translator comments added to the two strings that carry placeholders, and
  the Survey Editor role label is now a literal so gettext can extract it.
* The global bootstrap function is prefixed: run_stedica_surveys() is now
  stedica_surveys_run().
* "Tested up to" raised to 7.1.

Documented as intentional, with the reason recorded in the file:

* Direct database queries and the absence of an object cache, which are
  inherent to owning custom tables and to showing a respondent their own
  submission without staleness.
* SQL identifiers built from $wpdb->prefix, which cannot be bound as query
  parameters.
* Nonce checks that happen inside a shared helper the analyser cannot follow,
  and anonymous front-end session resolution where the session token is the
  credential.
* JSON request payloads, which sanitize_text_field() would corrupt; these are
  decoded and structurally validated instead.

= 5.10.8 =

* The buttons on the add-on cards now sit in a flex row with an explicit 6px
  gap. They were previously separated only by a whitespace text node, which
  collapsed to a hairline and left the two buttons looking joined.

= 5.10.7 =

* The "Learn more" links on the add-on cards now point at the current product
  pages, `wordpress.stedica.com/data-sources/` and
  `wordpress.stedica.com/ai-images/`. They previously pointed at
  `stedica.com/add-ons/...`.

= 5.10.6 =

* The add-on cards on the Stedica Surveys dashboard now link to a free trial
  for the Data Sources and AI Images add-ons.
* The trial link appears only when the add-on is **not installed**. Somebody
  already running it is offered "Activate" or "Manage plugin" instead — being
  sold a trial for something you already have is the kind of upsell that makes
  a plugin feel like it is selling at you.
* Fixed: on that card, "Install Add-on" and "Learn More" pointed at the same
  URL, so the two buttons did the same thing. They are now "Start free trial"
  and "Learn more", with different destinations.

= 5.10.5 =

* The plugin's output buffer now starts only on front-end page requests. It
  previously started on every request — admin screens, AJAX, REST, WP-CLI and
  cron included — where nothing renders a survey and nothing needs it, while
  leaving an extra output-buffer level for other components to work around.
  Page builders nest output buffers heavily when composing a layout, so this
  removes a needless interaction with them.
* The buffer itself is unchanged on the front end, where it is required: a
  survey renders from a shortcode, mid-page, and its session cookie cannot be
  sent once output has reached the browser.

= 5.10.4 =

**Fixed: every survey displayed its title twice.** Once above the survey card
in the theme's heading style, and again inside it in the plugin's own — with
the shortcode, the block and the Elementor widget alike. Two separate pieces of
code were each rendering the survey name: the server, and the survey script.

* The script no longer renders a title. The server-rendered one survives
  because it appears before the script runs and it is what names the survey
  region for screen readers.
* That heading had no styling of its own, so it inherited whatever the theme
  gave an `h2` and looked like a stray heading rather than part of the survey.
  It now carries the plugin's title styling, including an explicit font so a
  theme's heading font cannot leave the title looking unrelated to the card
  beneath it.
* A survey with no description no longer renders an empty paragraph above the
  progress bar. The gap was invisible while the duplicate title sat over it.

= 5.10.3 =

* The "Start new survey" button on the thank-you card now matches the offline
  Sync and Reset buttons — red with white text, light grey with black text on
  hover.
* It was carrying a class name (`stedica-button`) that nothing in the plugin
  ever defined, so it had been rendering with the browser's default button
  styling rather than the plugin's. The correct class is `stedica-btn`.
* The three buttons now share one style rule rather than three copies of it.
  They are the same kind of action — discarding or moving on from what is on
  the device — and separate copies would drift apart the first time the palette
  changed.

= 5.10.2 =

**Fixes the root cause behind 5.10.1.** On any survey with offline capture
switched off, the runtime never established whether the server was reachable,
so it behaved as though permanently offline.

`initOffline()` returned immediately when offline capture was off, which meant
the connectivity probe never ran and the internal "is online" flag stayed at
its starting value of false for the whole session. Three things branch on that
flag, so with offline capture off:

* **Submitting** took the offline path — which is what produced 5.10.1's
  "Saved offline" message, and then 5.10.1's "You appear to be offline" one.
* **Answers were never sent to the server as they were given**, so resuming a
  part-finished response had nothing to resume from.
* **Photo upload failed outright** with "Offline storage unavailable", because
  it was routed to storage that is switched off.

Connectivity is now established regardless of the setting — seeded from the
browser and confirmed by the same lightweight probe — while everything else
about offline capture stays off. Submitting no longer consults the flag at all
when there is no queue to fall back on: it simply attempts the submit and lets
the network answer.

= 5.10.1 =

Three bug fixes on the submit path, all found from one report: a survey with
offline capture switched off still reported "Saved offline" after submitting,
on a machine that was online the whole time.

* **Fixed: a failed submit was saved to the device even when the survey has
  offline capture switched off.** The fallback that queues a submission never
  checked the setting, so a survey with no offline path told the respondent
  their answers were safely queued when nothing would ever sync them.
* **Fixed: a server rejection was reported as "Connection dropped".** If the
  server answered and refused the submission — a validation failure, a
  duplicate, a closed survey, a spam check — that error was discarded and
  replaced with a connectivity message that was not true, which made the real
  cause impossible to see. The server's own message is now shown, and the
  submission is not queued: it would only be rejected again on sync.
* **Fixed: the spam timing check compared two different clocks.** Session start
  times are stored in site-local time but were read as UTC, so on any site not
  set to UTC the check measured against a baseline hours out. It now converts
  properly, and skips rather than rejects if the clocks still disagree —
  turning away a real respondent is far worse than letting one fast bot
  through.
* With offline capture off, submitting while genuinely offline now says a
  connection is needed and keeps the answers on screen, instead of silently
  queueing them.

= 5.10.0 =

**Option randomisation.** A question's answer options can be shuffled so each
respondent sees a different order, cancelling the bias towards whatever sits at
the top of the list.

* **The order is fixed for a respondent once they start.** It is derived from
  their session rather than drawn at random, so nothing moves while they are
  choosing — a list that reshuffled on every click would put their next click
  on whatever slid into that spot, corrupting the data while looking like they
  changed their mind. It also survives a reload and an offline resume.
* Options can be pinned with "Keep in place", so "Other" and "None of the
  above" stay at the end where respondents expect them.
* Not offered for Likert scales, yes/no, ranking or matrix questions, where the
  order carries the meaning. Shuffling a Likert scale is not a de-biased
  question, it is a broken one — so the setting is hidden rather than shown and
  ignored.
* Answers store option values, never positions, so display order has no effect
  on stored responses, exports or results.

= 5.9.0 =

**Answer piping.** A question can now quote an earlier answer in its own
wording — "How was your visit to {answer:q1}?" — and it updates the moment the
respondent answers the question being quoted.

* The same `{answer:CODE}` tag syntax email notifications already use, so it is
  learned once.
* Optional fallback for when the source question has not been answered:
  `{answer:q1|the store}`. Without one, nothing is substituted rather than a
  raw tag being shown to a respondent.
* A multi-select reads as a sentence — "Coke, Pepsi and Fanta", not a
  comma-jammed list — because it lands mid-sentence.
* A question cannot quote itself, which would otherwise show a respondent their
  own half-typed answer inside the question asking for it.
* Piped values are inserted as **text, never markup**, so a respondent cannot
  put script into a question that a later respondent then reads.
* The builder shows the syntax under every question's label field, using a real
  earlier question from the same survey as the example.

= 5.8.0 =

**Starter templates.** Add New now offers six ready-made surveys instead of
only an empty canvas: Net Promoter Score, customer satisfaction, employee
engagement, event feedback, product feedback, and a store/field audit with
photo evidence.

* Picking one creates the survey with its questions already in place. Nothing
  is linked afterwards — editing a starter never changes a survey somebody
  already made from it, and the new survey is fully editable.
* The NPS starter arrives already marked as NPS, so the results screen scores
  it without a second setting to find.
* Starters run through the same schema validator as any other survey, so one
  that fell out of step would fail at creation rather than producing a survey
  the builder could not open.

= 5.7.0 =

**Net Promoter Score.** The builder has shipped an NPS 0–10 answer preset for
some time, but nothing calculated the score — offering the labels without the
metric looked like the feature was there.

* Mark a question as NPS (applying the 0–10 preset does it for you) and the
  results screen reports the score with its promoter, passive and detractor
  breakdown.
* NPS is presented as a **score from −100 to +100, never with a per-cent
  sign** — the usual way it gets misreported.
* Non-answers are excluded from the base rather than counted as zeros. Treating
  a skipped question as a detractor would drag the score down by an amount that
  depends purely on how many people skipped it.
* The number of responses is always shown next to the score, and below ten the
  card says outright that the base is too small to read as a score.
* Whether a question is NPS is an **explicit setting, never inferred** from the
  answers happening to run 0–10. "How many staff were on the floor?" is an
  ordinary 0–10 question, and reporting a Net Promoter Score for it would
  produce an authoritative-looking number that means nothing.

= 5.6.0 =

**Gutenberg block.** Version 5.1.2 removed a readme claim that the plugin had
a block, because it did not. It does now.

* Pick a survey from the block sidebar; the front end renders it through the
  same path as the shortcode, so access control, scheduling, spam protection
  and the offline setting all behave identically. There is one render path, not
  two to keep in step.
* The block stores **only the survey id**, never a copy of the survey. Editing
  a survey updates it everywhere it is embedded, immediately — a copy in post
  content would silently drift from the real thing.
* The editor shows which survey is selected rather than running the live
  survey. A survey loads a large runtime, registers a service worker and can
  open a response session; doing that in the editor canvas would be slow and
  could record a response every time someone opened the page to edit it.
* If an embedded survey is later deleted, the editor says so instead of
  quietly rendering nothing on the published page.
* No build step was added. The block is plain JavaScript, so the file that
  ships is the file you can read and fix.

= 5.5.0 =

**Cross-tabulation.** Break one question down by another — satisfaction by
region, brand seen by store format. Uses the same filters as the results table.

* Row, column and total percentages, each **named on screen with what it
  means**. They answer three different questions and quoting one while meaning
  another is the usual way a cross-tab gets misread.
* Respondents missing one of the two answers are counted in a "(no answer)"
  category rather than dropped. Excluding them silently shrinks the base, which
  inflates every percentage in the table with nothing on screen saying so.
* Multi-select questions count once per selected value, so a respondent can
  appear in several cells. The table says so and reports the number of
  selections separately from the number of respondents.
* Where a question has more categories than the table shows, the omitted count
  is stated and the totals cover only what is displayed — a table whose rows do
  not add up to their own total looks broken.
* Zero cells are dimmed, never blank: an empty cell reads as missing data
  rather than as nobody giving that combination.

= 5.4.0 =

**Individual response viewer.** You could export every response to CSV but not
open a single one and read it. Now there is a View button on each row of the
Session Explorer.

* Each response is rendered against **the survey version it was answered
  against**, not the current one. Sessions have always recorded their version
  id; this is the first screen to use it. Read the current schema instead and a
  response collected before someone reworded a question is shown under the new
  wording — the answer silently becomes an answer to a question that was never
  asked.
* Where that exact version is no longer stored, the viewer says so rather than
  presenting current wording as if it were what the respondent read.
* Unanswered questions are listed as unanswered rather than omitted. A skipped
  question is a finding; a gap in a list looks like a bug.
* "Not answered" and "answered, but left blank" are shown as different things,
  because they are.
* Answers are formatted for reading: a GPS reading appears as coordinates
  rather than JSON, a multi-select as a list, a matrix as row-and-value pairs.
* Photos attached to a response are shown inline.
* Answers whose question is no longer in the schema — a deleted question, or an
  add-on that is no longer active — are listed in their own section instead of
  being silently hidden.

= 5.3.0 =

**Response integrity.** The plugin previously accepted any submission that
carried a valid nonce, which is fine for a survey sent to people you know and
inadequate for one on a public page.

* **Spam protection**, on by default: a hidden decoy field, a check that the
  submission did not arrive faster than a person could answer, and a rate limit
  per connection. Deliberately no CAPTCHA — a honeypot and a timing check stop
  the overwhelming majority of automated submissions without sending anything
  to a third party or asking the respondent to do anything.
* **One response per person**, per survey: by WordPress account, by browser
  cookie, or by network address. Each has a real limitation and the builder
  says which — an account limit does not reach anonymous visitors, a cookie is
  cleared by a private window, and a network address is shared by everyone in
  an office.
* **Response caps** — the survey closes itself at a set number.
* **Anonymous responses** — no account is recorded against the response.
* Network addresses are **never stored**. Duplicate detection uses a salted
  hash that matches a repeat visitor without being reversible into an address.
* Proxy headers such as `X-Forwarded-For` are ignored unless the site defines
  `STEDICA_TRUST_PROXY`. They are set by the client, so trusting them by
  default would hand anyone a way to defeat both the duplicate check and the
  rate limit.

= 5.2.0 =

**Charts.** The results screen previously drew one visual — a horizontal bar
list — for every question regardless of type.

* Chart type is now chosen to suit the question: a doughnut for single-choice
  with few options, bars for multi-select (a pie would be a lie there, since
  the slices sum to more than the number of respondents), stacked bars for
  Likert and matrix, and a line for responses over time.
* **Every chart ships with a data table.** A canvas is an opaque bitmap to a
  screen reader, so a chart on its own is a blank spot where the answer used to
  be. The table sits behind a "View as table" toggle, carries the same numbers,
  and is what gets read aloud.
* Per-question distributions are no longer capped at the top five answers. That
  cap is fine for a "most common answers" list and wrong for a chart: a pie of
  the top five of eight options sums to less than the response count and
  misrepresents the data. Where a long tail is grouped, the chart says so and
  names how many answers were grouped.
* Chart.js 4.4.4 (MIT) is bundled locally, never loaded from a CDN.
* Charts respect `prefers-reduced-motion`.

= 5.1.2 =

* Readme correction. Two features were listed that have never existed in the
  plugin: **section randomisation** and a **Gutenberg block**. Both claims date
  from earlier releases and were carried forward without being checked against
  the code. Neither is implemented, so both have been removed from the feature
  list rather than left standing. Surveys are embedded with the
  `[stedica_survey]` shortcode or the Elementor widget.
* No functional change.

= 5.1.1 =

* Question fields, checkboxes and radio buttons now carry the same 6px radius
  as everything else, on both the published survey and the admin screens.
* Note that radio buttons are no longer circular. With square corners the only
  thing distinguishing a radio from a checkbox is its checked mark, so radios
  keep a dot and checkboxes keep a tick.
* On the published survey the checkbox and radio are now drawn by the plugin
  rather than by the browser, which is what allows them to take a radius at
  all. Their checked, hover, focus, disabled and disabled-checked states are
  all styled explicitly, and forced-colours / high-contrast mode is handled so
  the checked state stays visible.

= 5.1.0 =

Visual pass. No functional change.

* The offline Sync and "Reset local state" buttons are now red with white text,
  turning light grey with black text on hover. Both are red, not just Sync: a
  red Sync beside a neutral Reset would imply Reset is the safer of the two
  when it is the destructive one. Their disabled state is explicitly styled —
  Sync is disabled whenever the queue is empty, which is most of the time, and
  it should not look like a live red button.
* Every box, field, button and section border is now a consistent 6px radius,
  driven by a single `--stedica-radius` custom property rather than the nine
  different values that had accumulated. This extends to standard WordPress
  admin buttons and inputs, scoped strictly to this plugin's own screens.
* Progress bars, KPI gauges and avatars keep their pill and circle radii. A
  progress bar squared off at 6px reads as broken rather than restyled.
* The "Clear Sync Monitor" button lost its inline colour overrides in favour of
  a class, so it matches the other offline actions.

= 5.0.0 =

Stedica Surveys is now a complete, unrestricted survey plugin. The features
that used to require a licence have moved into a separate Stedica Surveys
Premium add-on, and nothing in this package is gated any more.

* Removed every licence check. GPS capture and photo upload used to be marked
  "locked" in the builder unless a licence was active; photo upload is now a
  normal question type here, and GPS capture lives in the Premium add-on.
* Removed the bundled licensing SDK. This plugin now contacts no external
  service at all.
* Leaflet is no longer loaded from a third-party CDN. The map that did so has
  moved to the Premium add-on, which bundles Leaflet locally.
* Every script and style is now enqueued properly instead of being written
  into the page from PHP.
* Added documented extension points so add-ons contribute question types,
  admin pages, dashboard cards, export columns and validation rules.
* Backup, restore, clear-data and GDPR erasure now discover the tables they
  should sweep instead of using a fixed list, so data added by an add-on is
  included in all four.
* The results screen no longer reports an "Avg Compliance" figure when no
  scoring engine is installed — it previously showed 0%, which read as a
  measurement rather than an absent one.
* **Added email notifications.** The readme has advertised "email notifications
  on new responses" since 4.x and the feature did not exist — there was no
  `wp_mail()` call anywhere in the plugin. There is now: an admin notification,
  a respondent confirmation, merge tags, per-survey overrides, and a duplicate
  guard so a response synced twice from an offline device does not email twice.
  Mail is sent after the response is returned to the browser, so a slow mail
  server no longer means a slow-looking survey.
* **Offline capture is now a per-survey setting**, in the builder under Survey
  Settings. It was previously always on for every survey. Existing surveys keep
  it on — a survey with no explicit setting is treated as enabled, so nothing
  changes on upgrade. With it off, the respondent gets no connectivity banner
  and nothing is written to their device.
* The builder no longer shows the live-scorecard toggle unless the Premium
  add-on is installed. It previously stored a value that nothing in this
  package reads.
* Fixed a stale fallback path in the offline service worker that pointed at the
  pre-5.0 plugin folder.
* Full history: see changelog.txt.

= 4.13.0 =
**Security release — upgrade immediately if you use the Data Sources add-on.**

* Security: a survey whose access was set to "open" baked the entire WordPress
  account list — including e-mail addresses, up to 200 records — into the public
  page HTML for anonymous visitors. The guard that scopes the WordPress Users
  data source to the current user was conditioned on the survey's access setting
  instead of on the visitor, so it never engaged on public surveys. The render
  path now enforces exactly the rule the Data Sources REST/AJAX endpoints already
  applied: anonymous visitors get nothing, a signed-in user without `list_users`
  gets only their own account, and only users who can `list_users` see the full
  directory. Imported (file-based) data sources are unaffected.
* Fix: CSV and Excel exports were misaligned by three columns. The header
  declared twelve fixed columns ending in five KPI names, but each row emitted
  only nine values (`total_score` plus a JSON blob of the survey's own KPIs), so
  every question's answer landed three columns to the left of its own heading.
  KPI keys are defined per survey and cannot be mapped onto five fixed names, so
  the header now describes what is actually written: `total_score` and
  `kpi_scores_json`. **Exports taken before this release have shifted columns —
  re-export any file you still rely on.**
* Fix: the connectivity probe called an AJAX action (`stedica_ping`) that was
  never registered. It worked only because admin-ajax answers an unknown action
  with `0` and HTTP 400, which the client counts as reachable — at the cost of a
  400 in the log every 30 seconds for every open survey. The action is now
  registered and returns a cheap 200.


= 4.12.1 =
* Compatibility: both add-ons were renamed in their 4.0.0 releases
  (`stedica-picture-ai-analysis/` -> `stedica-surveys-ai-images-addon/`,
  `stedica-data-sources/` -> `stedica-surveys-data-sources-addon/`). The admin
  add-on cards and the Plugin Health "Add-on folder slug" check now resolve
  BOTH the new and the legacy folder slug, preferring whichever is actually
  active, so detection stays accurate before, during and after the migration.
* No change to how core talks to the add-ons at runtime — that has always been
  by class name (`Stedica_DS_*`, `Stedica_Picture_AI*`), never by folder path,
  so the integration itself is unaffected by the rename.
* Freemius SDK updated 2.13.1 -> 2.13.4 (vendored copy replaced wholesale from the
  official Freemius/wordpress-sdk release; the previous copy was verified byte-identical
  to pristine 2.13.1, so no local patches were lost). Drop-in update — the only
  configuration-visible change is a new WP_FS__API_TIMEOUT default of 30 seconds.

= 4.12.0 =
* Licensing: Stedica Surveys is now offered as a single Business plan with a 14-day free trial. The trial includes every feature, and no credit card is required to start it.
* Removed the old free-tier restrictions, including the three-survey limit, which no longer apply.
* Picture AI Analysis and Data Sources are now available as licensed add-ons.
* Deleting the plugin now removes everything it created: all survey tables, the Survey Editor role, saved settings, cached data and the private backup folder. Previously some of these were left behind.

= 4.11.0 =
* New: you can now place more than one survey on the same page. Each survey keeps its own answers, progress and saved drafts, and they no longer interfere with each other. Pages with a single survey are unaffected.
* Fix: if part of a survey fails to display, the rest of the survey now still works and a short notice appears in its place. Previously a single unexpected problem in the survey header or scorecard could leave the whole survey blank with nothing to explain why.

= 4.10.4 =
* Fix: surveys showed their title with no questions beneath it. Every question hit the same internal error while being drawn, caused by a typo in the survey script that referred to a name which did not exist. It affected all question types on every survey, which is why nothing appeared at all. The typo is corrected and questions now display normally. If you were seeing an empty survey, this is the fix — no changes to your survey content are needed.
* Fix: on surveys with a "surveyor" question, the logged-in user's name no longer prefills as blank. The server was sending that identity in a different shape than the survey page expected, so the field fell back to an empty box. Both sides now agree, and older saved data is still accepted.
* Improved: if the survey ever fails to draw for some other reason, the page now shows a short explanation in place of the survey and writes the details to the browser console, instead of leaving a blank space. Previously several stages of the drawing process could fail silently, which is what made the problem above so hard to spot.

= 4.10.3 =
* Fix: a single question that failed to display could leave the whole survey blank. The survey drew its questions in an unguarded loop, so one unexpected error stopped every remaining question from appearing — visitors saw the survey title above an empty space, with nothing to indicate what had gone wrong. Each question is now drawn independently: if one fails, it is replaced by a short notice naming that question, the rest of the survey still works, and the technical details are written to the browser console for the site administrator.
* This is a resilience fix. Surveys that already displayed correctly are unaffected.

= 4.10.2 =
* Fix: on pages built with Elementor (or any page builder, or with a JavaScript optimizer active), a survey could show its title but none of its questions. The survey's script expected the survey container to already exist the moment it ran, and gave up silently if it did not — which is exactly what happens when a page builder moves, defers, or combines scripts, or injects the survey markup after the page has loaded. The script now waits for the page to be ready, retries if the survey container appears later, and writes a clear message to the browser console if the container genuinely never turns up, instead of failing invisibly.
* This affects only how the survey starts up. Surveys that already rendered correctly behave exactly as before.

= 4.10.1 =
* Fix: a renamed survey kept showing its OLD name on the public page. The survey heading was read from a copy of the name stored inside the version's schema JSON, and that copy took precedence over the survey's actual name — but the rename feature only updates the survey record, so the public heading stayed frozen at the pre-rename name while every admin screen showed the new one. The survey's own name is now authoritative for the public heading, which repairs every already-renamed survey immediately on upgrade with no migration and no changes to stored survey content.
* Fix: renaming a survey now also updates the name copy inside the latest saved version, so the builder's preview modal reflects the new name too. The sync is best-effort and can never cause a rename to fail; a version whose stored JSON cannot be parsed is left untouched rather than overwritten.

= 4.10.0 =
* Feature: native Elementor widget. "Stedica Survey" appears in the Elementor panel under a new "Stedica" category — drag it onto any page and pick a survey from a dropdown instead of typing a shortcode. An Advanced section exposes a slug override for cases where you want to target a survey by slug rather than ID.
* The widget renders a placeholder card inside the Elementor editor rather than a live survey. This is deliberate: rendering live would create a survey session row and cookie on every editor repaint. The placeholder shows the selected survey name and the exact shortcode that will run on the published page; the real survey renders normally for visitors.
* The widget is entirely optional. When Elementor is not installed the integration class is constructed but its hooks never fire, so there is no overhead and no change to the existing [stedica_survey] shortcode behaviour.

= 4.9.5 =
* Feature: rename a saved survey. Two entry points: (a) inline pencil-edit in the builder header — click the pencil (or double-click the name) to edit; Enter or blur saves, Esc cancels; (b) Rename button on the All Surveys admin page, opens a prompt with the current name pre-filled. Both routes hit a new stedica_rename_survey AJAX endpoint that updates stedica_templates.name with the same cap check (stedica_edit_surveys) + nonce as save/clone/autosave, and emits a survey_renamed audit event with old + new name.
* Security: name is trimmed, sanitized via sanitize_text_field, capped at 255 characters; wpdb->update prepares the query; unchanged from name = current name is a no-op (early return before the network call).

= 4.9.4 =
* Polish: matrix questions now render as a real preview table (rows x columns with column headers, row labels, and visual radio dots) instead of the "Matrix grid — N row(s) x M column(s)" placeholder line. The same buildPreviewQuestion function powers both the canvas card thumbnail AND the full Preview modal, so the table renders in both surfaces. Honors the v4.9.1 "Numbered rows" flag (prepends 1., 2., …). Unconfigured matrices (empty rows OR columns) still fall back to the count-line so the empty state is visible at a glance.

= 4.9.3 =
* Fix: Image Choice and Ranking question types now have a working options editor in the builder. Both types were registered as has_options=true in the question registry but were missing from the builder sidebar's options-editor switch, so admins had no UI to enter their options and saves failed server-side with "Question X requires options". The two types are now in the same switch as multiple_choice/dropdown/likert/closed_ended/dichotomous and share the same options editor + presets.
* Feature: Image Choice options now carry a per-option image. The sidebar shows an "Image URL" text input and a "Pick image" button per option; the button opens the WordPress Media Library picker so admins can attach images from the existing media library instead of pasting URLs. A 32x32 thumbnail preview renders next to the URL when set. The schema validator now preserves option.image through normalization (esc_url_raw sanitized, blocks javascript:/data: schemes).
* Fix: matrix question card preview in the builder canvas now reports the actual row and column counts ("Matrix grid - N row(s) x M column(s)") instead of always reading "0 row(s)" from the (always-empty) q.options array.
* Internal: wp_enqueue_media() is wired into the builder admin page enqueue so wp.media is available for the Image Choice picker.

= 4.9.2 =
* Fix: matrix questions could not be saved even with rows and columns properly populated. The Question Types registry incorrectly declared has_options=true for matrix, so the schema validator demanded a non-empty q.options array before reaching the matrix-specific rows/columns check. Matrix uses q.rows + q.columns, not q.options — the validator was rejecting every matrix save with "Question X requires options". Flipped matrix has_options to false in the registry and added a defensive explicit skip in the validator's options-required block.
* Audit: every other question type whose registry has_options flag is true was cross-checked against actual builder + renderer behavior to confirm no other type is similarly mis-classified.

= 4.9.1 =
* Fix: builder save error message now surfaces the actual schema validation reasons (e.g. "Matrix question X requires rows and columns") instead of the generic "Schema validation failed". The first three errors are shown inline in the save status pill (with a console log of the full list); the pill holds the message for 6 seconds so users can read it.
* Feature: matrix question type now has a full sidebar editor. Add/remove/reorder rows and columns. "Bulk paste rows" textarea lets you populate large matrices (e.g. 21 brands) from a newline-separated paste. Five column presets are one-click (Yes/No, Yes/No/Don't know, Likert 1-5, Strongly disagree → Strongly agree, Never → Always). New "Numbered rows" checkbox prepends 1., 2., … to each row label at render time. Schema additions are additive: q.numbered_rows defaults to false; q.rows and q.columns continue to use the existing whitelist.
* Fix: matrix questions with empty rows or columns now show a "Setup needed" badge on the canvas card so admins see the problem before saving (previously the card showed the green "Ready" badge even though server-side validation would reject it).

= 4.9.0 =
* Feature: per-question canonical Excel/CSV header. New "Export header" text input in the question sidebar lets admins set the exact column name used in CSV and XLSX exports. Blank falls back to the question label (existing behavior). Applies to every question type. Touches both the admin Export CSV/XLSX path and the Power BI long-form /results/{id}/csv REST endpoint so the analyst sees the same canonical name in both places.
* Feature: per-question Hidden flag for derived/auto-captured question types. The frontend renderer skips visible UI but still captures the question's derived value into the answer set. Available on date (e.g. with default = today), calculated_field, prepopulated, and surveyor. Not exposed on input or photo-driven types where a hidden field would have no value to capture. Hidden questions still appear in the builder canvas (visually dimmed with a badge) so admins can edit them.
* Schema additions are additive and backward-compatible: q.export_header defaults to "", q.hidden defaults to false; existing schemas auto-upgrade on the next save.

= 4.8.3 =
* Fix: surveys with no KPIs could not be saved. The builder's KPI validation enforced "total KPI weight must equal 100" unconditionally, so a survey with zero KPIs (totalWeight=0) was always flagged as invalid and save was blocked. A catch-all in the same function also turned informational messages (e.g. "KPI X has no scored questions assigned") into save-blockers. KPI weight-totals and scoring-without-KPI checks now only apply when at least one KPI is defined; informational messages no longer block save; the "question points to a missing KPI" inconsistency check still blocks save (now marked explicitly rather than via the catch-all).

= 4.8.2 =
* Fix: builder Types/My Library tab toggle was rendering invisible teal-on-teal text in the default state. WordPress's `.button-primary` class (which the tab JS applies to the selected tab) paints the background with the plugin's primary teal via an !important rule from the v4.x release; the v4.8.0 tablist CSS then set the selected-tab text color to the same teal, producing zero-contrast text. Scoped the tablist visual rule to `:not(.button-primary)` so WP's native white-on-teal renders when the tab uses .button-primary, and the custom tab styling still works when it doesn't. Also scoped the unselected `[role="tab"]` rule with `:not(.button)` so WP button styling survives on non-selected tabs.
* Fix (audit): three additional dark-mode contrast collisions surfaced by an adversarial sweep of stedica-builder.css and stedica-survey.css. (1) `.stedica-var` calculated-field pill — `--stedica-primary-soft` is redefined to #113837 in dark mode while text stayed `--stedica-primary-dark` (#278E8C), collapsing contrast to ~2:1; added a dark-mode override that lifts text to #5EE5E3. (2) `.stedica-calculated-value` on the respondent survey — same soft-bg / dark-text collision in dark mode; added a dark-mode override to #E5EEF7. (3) `.stedica-surveyor-identity` banner — hardcoded #0f5f5e text on a 10%-teal tint over the dark-mode near-black page background became unreadable; added a dark-mode override to #A7E8E6.

= 4.8.1 =
* Accessibility audit cleanup (cosmetic; no functional change). Emit literal role="status" / role="alert" attributes in static HTML markup alongside the existing dynamic setAttribute() calls so automated WCAG scanners that grep for static attributes recognize them on the first pass. Add explicit <fieldset> wrappers (or role="radiogroup") to the closed_ended and dichotomous question render paths so each radio-style question type carries its own statically-discoverable group semantic. Normalize ARIA attribute quoting to double quotes in the builder tablist (Types / 📚 My Library toggle). Rename builder keyboard-drag-alternative class .stedica-question-kbd-move → .stedica-kbd-move to match the published spec. Zero behavior changes for end users — screen-reader experience is unchanged from 4.8.0.

= 4.8.0 =
* Accessibility: WCAG 2.1 AA pass across the respondent-facing survey and the admin builder. ARIA roles + accessible names on every interactive element, semantic HTML (fieldset/legend for radio/checkbox groups, table semantics for matrix questions), label-input pairing via for/id, aria-required on required inputs, aria-describedby for help text, role="alert" / role="status" + aria-live on validation messages and status pills (Draft saved, AI status, collaborative-edit banner), focus management on section advance + validation error (focus moves to first errored input), and a "Skip to first question" link at the top of the survey wrapper.
* Accessibility (frontend): high-contrast media query support, prefers-reduced-motion respect (transitions and pulse animations disabled), tap targets ≥ 44×44 px on coarse-pointer devices.
* Accessibility (builder): keyboard alternative for drag-drop on question cards (Move up / Move down / Move to section buttons visible on focus-within), focus trap on all three modals (Preview, Shortcut Help, Save Conflict) with focus restoration on close, ARIA tablist semantics on the Types / 📚 My Library tab toggle, semantic h3/h4 heading structure on section and question cards.
* Accessibility (general): visible focus indicators (3px primary-color outline + 2px offset) on every focusable element using :focus-visible (not :focus) so mouse users don't get unnecessary outlines, .stedica-sr-only helper class for screen-reader-only text where needed.

= 4.7.0 =
* Feature: Per-KPI risk-threshold overrides — KPI Definitions admin can now override the engine's previously-hardcoded scoring numbers (bad-trend delta, weight per missing visit, volatility cap, target-miss weight, Low/Medium/High bucket boundaries) per KPI. KPIs without overrides keep the existing defaults; no behavior change for upgraders.
* Feature: Power BI /risk endpoint — new `GET /wp-json/stedica-surveys/v1/risk/{survey_id}` and `GET .../risk/{survey_id}/kpi/{kpi_id}` routes return the KPI Risk Engine's entity ranking as a flat table. Same auth (Application Passwords + stedica_edit_surveys cap) and rate limit (120 req/min) as the existing /results endpoint. CSV via `?format=csv`.
* Feature: Collaborative-edit locks — builder shows a banner when another user is currently editing the same survey (30s heartbeat via transient). On save conflict, a modal offers Reload / Save Anyway (force) instead of a generic error. Force saves are audit-logged.
* Docs: New Roboflow deployment guide in the YOLO pipeline repo (`docs/deploy-roboflow.md`) for users who want managed inference instead of self-hosted FastAPI. Includes a copy-pasteable Cloudflare Worker shim that bridges Roboflow's request shape to the Picture AI endpoint contract.

= 4.6.0 =
* Feature: Undo / Redo with 50-step ring-buffer history (Ctrl/Cmd+Z and Ctrl/Cmd+Shift+Z / Ctrl+Y). Adjacent edits within 400ms collapse into a single history entry. Page reload clears history — durability is covered by the 4.5.0 autosave.
* Feature: Drag questions between sections. Drop targets are now the section bodies themselves (not just inter-question gaps); a dashed teal outline shows the active drop zone.
* Feature: Default option presets — one-click insert of Likert 5/7, Yes/No/Maybe, Yes/No/N-A, Frequency, Satisfaction, Extended Agreement, NPS 0–10, and a frequency-by-period preset. Appends to existing options (dedupes by value); does not replace.
* Feature: Keyboard shortcuts — `/` (focus search), `d` (duplicate focused question), `Delete` / `Backspace` (delete focused question with confirm), `?` (shortcut help modal). All guarded against firing inside text inputs.
* Feature: Mobile preview viewport toggle in the Preview modal header — Desktop (760px) / Tablet (768px) / Mobile (375px). Pure CSS max-width change with a 150ms transition.

= 4.5.0 =
* Feature: **Autosave** — Builder debounces edits and writes drafts to `stedica_builder_draft_{template_id}_{user_id}` transients (7-day TTL). A restore prompt appears if you reopen the Builder with a newer draft than the saved schema.
* Feature: **Bulk Options Paste** — paste one-per-line or CSV into a question's options editor to populate choices in bulk.
* Feature: **Clone Survey** — duplicate a survey and its latest schema in one click; the copy is named "Copy of {original}".
* Feature: **Question Library** — save individual questions for reuse across surveys, managed under Stedica Surveys → Question Library (per-user ownership, stored in `{prefix}stedica_question_library`).
* Feature: **Test Mode** — interactive Preview that simulates respondent flow with branching and validation, entirely in-browser; no AJAX, no DB writes.
* Feature: **Schedule Dates** — Survey Settings now accepts `available_from` / `available_until` (ISO 8601 dates); the frontend gates rendering when the survey is outside its window.
* Feature: **Survey Complexity Badge** — per-row badge on the All Surveys page (Light / Medium / Heavy) computed from question count and logic surface.
* Feature: **Find/Jump** — header input on the Builder filters and scrolls to a question by label without leaving keyboard focus.

= 4.4.0 =
* Feature: NEW **Survey Location Map** on the Results page. Renders one Leaflet circle-marker per session that captured GPS coordinates (most recent geo-event per session). Honours the toolbar's Survey / Date / Status / User filters automatically, plus an additional **Question + Equals** filter pair that narrows to sessions whose answer to a chosen question contains a value (case-insensitive substring match against `value_text` and `value_json`). Pins click into a popup showing session ID, timestamp, status, respondent, and lat/lng. Hard-capped at 5000 points per query to keep refreshes snappy.
* Internal: two new admin-ajax handlers — `stedica_results_geo_points` (returns the filtered point list) and `stedica_results_questions` (lightweight schema-questions endpoint powering the map's question-picker dropdown). Both reuse the existing `assert_ajax_security` + `get_request_filters` + `build_session_where` helpers so the toolbar filters apply identically to the table and the map. Leaflet 1.9.4 + OpenStreetMap tiles (same CDN-load as the Geo Dashboard — no new dependency added).

= 4.3.5 =
* UX: KPI Risk Engine page now opens with a brief intro explaining what the page is about ("pick a KPI, see entities ranked by composite risk score") and what the visitor will see (latest value, recent trend, visit count, Low/Medium/High badge) — including a four-bullet breakdown of the underlying signals (trend, volatility, target miss, visit cadence) and an explicit "no ML model involved — deterministic rule-based scoring" note so users don't expect black-box AI. Lives between the page heading and the dashboard mount point in `includes/class-stedica-risk-engine.php::render_risk_dashboard`.

= 4.3.4 =
* UX: renamed the module formerly known as "AI Store Risk" to **KPI Risk Engine** in all user-facing surfaces (sidebar submenu, dashboard card, dashboard card button, page heading, System Status row, fallback "module not available" notice). The label was misleading — the module is deterministic heuristic scoring (KPI trend + volatility + target miss + visit cadence), not actual AI/ML; the new name accurately describes what it does and stops trading on AI branding it doesn't earn. The page slug `stedica-risk`, class `Stedica_Risk_Engine`, method `page_ai_risk()`, and capability gate (`stedica_edit_surveys`) are unchanged, so existing bookmarks, custom links, and third-party code keep working. Dashboard card description rewritten to honestly describe the heuristic ranking ("rank entities by composite risk score derived from KPI trend, volatility, target miss, and visit cadence").

= 4.3.3 =
* UX: Stedica Surveys → Dashboard now shows a **Power BI Integration** card alongside Builder / All Surveys / Survey Results / Store Intelligence / KPI Risk Engine. Same dashboard_card pattern as the other module cards; clicking it opens the in-WP walkthrough page added in 4.3.1.

= 4.3.2 =
* Docs: Power BI Integration admin page now ends with a **Security &amp; Data Integrity Compliance** section: three tables covering authentication/authorisation controls (App Password model, capability gating, rate limit, read-only surface), data integrity guarantees (live DB read, per-session pagination boundaries, ISO 8601 UTC timestamps, SQL/CSV injection defences), and compliance posture (GDPR exporter/eraser integration, PII minimisation by default, audit trail, credential rotation, data residency), plus a five-item incident-response checklist. Designed as a one-pager an analyst can forward to their security or compliance team before wiring up production Power BI refresh.

= 4.3.1 =
* Feature: NEW in-WP **Power BI Integration** admin page under Stedica Surveys → Power BI Integration. Same content as `docs/power-bi-integration.md` but site-aware — every endpoint URL is pre-filled with this site's `rest_url('stedica-surveys/v1/')`, the surveys list is loaded live from the DB, and every code snippet (curl commands, Power Query M templates for Results/Schema/Surveys, DAX measures) has a one-click Copy button. Includes an HTTPS / `manage_options` environment-check banner so the analyst sees blockers before hitting them in Power BI. Renderer lives in `admin/class-stedica-power-bi-page.php`; submenu registered with the `stedica_edit_surveys` capability so Survey Editors can self-serve.

= 4.3.0 =
* Feature: NEW Power BI / external-analytics REST API at `/wp-json/stedica-surveys/v1/`. Three endpoints: `/surveys` (list), `/results/{id}/schema` (question metadata), `/results/{id}` (long-format answer rows, paginated, filterable). Authentication via WordPress Application Passwords. Capability gate: `stedica_edit_surveys` for read; respondent PII (name/email) requires `manage_options`. JSON default; CSV via `?format=csv` or the dedicated `/results/{id}/csv` endpoint.
* Feature: pagination headers `X-WP-Total`, `X-WP-TotalPages`, `X-WP-NextPage` — Power Query's M handles these natively for "load all pages" flows.
* Security: per-user rate limit (120 req/min) via transient; SQL parameterization throughout; CSV formula-injection prefix reused from the admin export path.

= 4.2.7 =
* Fix: Survey Editor role didn't appear on sites that upgraded to 4.2.6 by file replacement — `register_activation_hook` only fires on a fresh Activate click, NOT on overwrite, so the role install code never ran. Added a self-heal check inside `Stedica_Init::maybe_upgrade()` that runs on every page load: if the stored `stedica_roles_version` option doesn't match the new `Stedica_Roles::ROLES_VERSION` constant (or the role is missing entirely), it re-installs idempotently. Future cap-map changes will propagate the same way — just bump the constant.
* Internal: introduced `Stedica_Roles::ROLES_VERSION` constant and `Stedica_Roles::maybe_install()` helper. The activator's `install()` call is now redundant on fresh installs (the self-heal also fires on first page load) but kept for clarity.

= 4.2.6 =
* Feature: NEW "Survey Editor" user role created on activation. Has access to the Survey Module (Builder, Results, All Surveys, Add New, Offline Sync Monitor, KPI Definitions, Geo / Territory / Store-Intelligence / AI-Risk dashboards), full page CRUD (edit/publish/delete own + others' pages, including private), Media Library access, and own-profile editing. Cannot access Picture AI add-on screens, the Account (Freemius) page, Plugin Health, Data Sources, the WordPress options page, plugin install/activate, or user management.
* Change: gating refactor — survey-management screens now check the custom capability `stedica_edit_surveys` instead of `manage_options`. A `user_has_cap` filter auto-grants this cap to any user who has `manage_options`, so existing administrator accounts keep working with no changes. Full-admin-only surfaces (Account, Plugin Health, /health REST endpoint, Picture AI / Data Sources add-on menus) still gate on `manage_options`.
* Internal: new `Stedica_Roles` class owns the role + capability lifecycle. The role is created on activation (idempotent — re-activation refreshes caps if the cap map changed), and removed on uninstall (revokes the cap from administrators at the same time).

= 4.2.5 =
* Fix: Image Recognition question type silently did nothing after photo upload — the frontend posted to a non-existent AJAX action (`stedica_ai_analyze_image_recognition`) while the server registers `stedica_ai_recognize_image`. Fixed.
* Fix: all three AI runners (Product Recognition, Image Recognition, Image Comparison) sent the question identifier as `question_code` but the server reads `question_id` via `sanitize_key`. The server couldn't load the per-question config (reference images, custom prompt, etc.) from the stored schema, causing silent failures. All three runners now send `question_id`.
* Fix: AI runners no longer swallow errors silently — a `.stedica-ai-status` row now appears in the question card showing "Analyzing photo with AI…" while a request is in flight, and shows the server's error message (rate limit, session gate, SSRF rejection, OpenAI failure) on failure instead of leaving the respondent staring at nothing.

= 4.2.4 =
* Fix: Builder "Preview" toolbar button now actually opens the preview modal — the click handler was missing entirely, and the modal's outer `.stedica-preview-modal` / `.stedica-preview-overlay` selectors had no positioning CSS so even after wiring, the overlay would have rendered un-positioned. Added the click + close + overlay-click + Esc-key handlers, the full modal/overlay positioning CSS, and a schema-driven read-only preview renderer that walks every section and renders a per-type representation (text inputs, option lists, ranking, matrix, AI placeholders, etc.).

= 4.2.3 =
* Change: companion release to the **Shelf AI Analysis → Picture AI Analysis** add-on rename (add-on v3.0.0). The add-on now powers three question types — Product Recognition, Image Recognition, Image Comparison — and the name has been generalised to match.
* Change: core stub class `Stedica_Shelf_AI` renamed to `Stedica_Picture_AI` (file `includes/class-stedica-shelf-ai.php` → `includes/class-stedica-picture-ai.php`). A `class_alias` keeps the old class name resolvable for one release cycle. The AJAX action `stedica_ai_analyze_shelf` is INTENTIONALLY preserved as the public API surface.
* Change: frontend survey JS function `runShelfAI()` renamed to `runProductRecognition()`. A thin shim `runShelfAI()` remains and delegates to the new function so any custom code that called it keeps working.
* Change: admin add-on card relabelled "Stedica Shelf AI" → "Stedica Picture AI"; the card now points at the `stedica-picture-ai-analysis/` plugin slug. Engine registry's `'shelf_ai'` key is preserved (it's a stored type-key) but now resolves to the renamed class.
* Compat: stored question-type keys (`product_recognition`, `image_recognition`, `image_comparison`, and the legacy aliases `shelf_ai` / `picos_recognition`) are UNCHANGED — no schema migration required.

= 4.2.2 =
* Change: Validator alias chain now collapses both legacy keys (`shelf_ai` AND `picos_recognition`) to the new canonical `product_recognition` so the Shelf-AI add-on's type-key rename auto-applies on every save. Also reads `picos_recognition_config` as a fallback when `product_recognition_config` is absent so pre-migration questions don't lose their config.
* Change: Frontend survey JS collapses all legacy product-recognition keys at render time and renames the result CSS class from `stedica-ai-result-picos` → `stedica-ai-result-product` for code clarity.

= 4.2.1 =
* Fix: Builder Survey Settings panel no longer crams the "Show live scorecard on the frontend" checkbox up against the SURVEY SETTINGS title. The h3 now has explicit line-height + 14px bottom margin, and the toggle row uses a dedicated CSS class instead of an inline style so WP admin theme overrides can't collapse it.

= 4.2.0 =
* Feature: 22 question types now formally registered with emoji icons in the builder palette.
* Feature: NEW "Surveyor" type — prefills with the logged-in WP user, falls back to open-ended for anonymous respondents.
* Feature: NEW "Date" type — HTML5 date picker with optional min/max and default-today.
* Change: `prepopulated` and `shelf_ai` are no longer registered in core; they appear only when their respective add-on is active (previously they were greyed-out placeholders).
* Change: Shelf-AI's product-detection question type renamed `shelf_ai` → `picos_recognition` (alias preserved for back-compat; existing surveys auto-migrate on next save).
* Hardening: validator now type-checks add-on-defined types and preserves unknown types in the schema rather than silently rewriting them to `open_ended`, so re-saving a survey without an add-on no longer destroys data.

= 4.1.9 =
* Fix: KPI sidebar inputs (Label, Weight) no longer lose focus after every keystroke — the panel previously re-rendered on each `input` event, destroying the input mid-type.

= 4.1.8 =
**Security release.** All installs should upgrade.
* Security: fix stored XSS via inline `<script>` JSON bootstrap (CVE-pending) — script-tag breakout via admin-typed schema fields.
* Security: schema validator now rejects unknown keys at every nesting level (whitelist rebuild).
* Security: sanitize `calculation.formula` and logic/branch nested conditions.
* Security: fix admin Results page button `onclick` attribute breakout via survey name.
* Hardening: `wp_unslash` before `sanitize_text_field` on builder POST inputs.
* Hardening: builder JS renders add-on-supplied question-type labels via `.text()` instead of HTML concatenation.

= 4.1.7 =
**Security release.** All installs should upgrade.
* Security: full GDPR personal-data exporter and eraser registered for `stedica-surveys`, honouring soft-delete mode and realpath-contained file deletion under `wp-content/uploads/stedica/`.
* Security: hardened uninstall — drops every plugin table, sweeps `stedica_*` options (including `stedica_session_token_backfilled` and `stedica_shelf_ai_settings`), purges `stedica_upload_rate_*` / `stedica_shelfai_rate_*` / `stedica_shelfai_compare_rate_*` transients, and recursively removes `wp-content/stedica-private/` while refusing to follow symlinks.
* Security: added i18n bootstrap via `load_plugin_textdomain` so translated security messages render correctly.
* Cleanup: removed dead `Stedica_Dashboards` loader (was never instantiated, required a missing path).
* Cleanup: removed obsolete `tools/migrate-fix-schemas.php` (targeted table names not in the current schema).
* Maintenance: DB version bumped to 3.3.0 — activator runs dbDelta + backfill on next admin pageview.

= 4.1.0 =
* Added: **KPI Definitions** — define aggregate metrics across question groups (avg, sum, min, max, count-if, weighted-avg) per survey template.
* Added: **Entity grouping** — roll KPIs up by any dimension question (store, agent, region) or GPS coordinates.
* Added: **KPI admin screen** under Stedica Surveys → KPI Definitions with survey picker, list table, and add/edit form.
* Added: `stedica_kpi_definitions` table (DB version 4.1.0, idempotent dbDelta migration).
* Changed: **Store Intelligence** dashboard is now **KPI Leaderboard** — ranks entities by any defined KPI respecting higher_better / lower_better direction.
* Changed: **AI Store Risk Prediction** dashboard is now **KPI Risk Alerts** — generic risk scoring (trend, volatility, target miss, visit gap) driven by any KPI instead of hardcoded compliance meta.
* Changed: Risk engine and leaderboard share a single `compute_entity_metrics()` entrypoint for consistency.
* Removed: Hardcoded `__compliance_meta` question-code dependency — dashboards now work with any survey schema.

= 4.0.3 =
* Added: complete `readme.txt` with Free/Pro feature matrix for wp.org submission.

= 4.0.2 =
* Fixed: free-tier license check now authoritative against build flag — Pro features strictly gated in the free build regardless of Freemius opt-in state.

= 4.0.1 =
* Changed: locked question types now displayed greyed-out with a tier badge (Pro / Data Sources / Picture AI) instead of being hidden — improves discoverability of upgrade path.
* Added: server-side enforcement of locked types in the save handler.

= 4.0.0 =
* Added: Freemius SDK integration with 7-day Pro trial, licensing, and auto-updates.
* Added: Free vs Pro tier system with 3-survey cap and "Powered by" footer on free tier.
* Added: Pro-gate for GPS and Photo Upload question types.
* Added: Pro-gate for Geo / Territory / Risk / Store Intelligence dashboards.
* Changed: survey creation capped at 3 active surveys for free-tier users.
* Changed: migrated uninstall logic from `uninstall.php` to Freemius `after_uninstall` hook.

= 3.9.x =
* Prior internal development versions — see commit history on stedica.com.

== Upgrade Notice ==

= 4.12.0 =
Moves to a single Business plan with a 14-day free trial. Start the trial or activate your license from Stedica Surveys → Account after upgrading.

= 4.11.0 =
Adds support for multiple surveys on one page, and makes a partial failure show a notice instead of hiding the whole survey. Safe drop-in update.

= 4.10.4 =
Fixes surveys that displayed a title with no questions at all. Required for anyone seeing an empty survey. No changes to your saved survey content.

= 4.10.3 =
Fixes a blank survey caused by a single question failing to display. Recommended for anyone seeing a survey title with no questions beneath it.

= 4.10.2 =
Fixes surveys showing their title but no questions on Elementor and other page-builder pages. Safe drop-in update; no schema changes.

= 4.10.1 =
Fixes renamed surveys still showing the old name on public pages. Repairs existing surveys automatically on upgrade — no migration, no changes to your saved survey content.

= 4.10.0 =
Adds a native Elementor widget for inserting surveys without a shortcode. Safe drop-in update; no schema changes, and the existing shortcode continues to work unchanged.

= 4.9.5 =
Survey names can now be edited from the builder header and the All Surveys page. Safe drop-in update; no schema changes.

= 4.9.4 =
Matrix questions now show a real grid preview in both the canvas card and the Preview modal. Safe drop-in update; no schema or behavior changes.

= 4.9.3 =
Builder polish: Image Choice + Ranking now have an options editor (Image Choice gets a WP Media Library picker), and the matrix preview shows accurate row/column counts. Safe drop-in update.

= 4.9.2 =
Bug fix: matrix questions now save correctly. Safe drop-in update; no schema or behavior changes.

= 4.9.1 =
Builder polish: full matrix-question editor + clearer save error messages. Safe drop-in update.

= 4.9.0 =
New per-question fields: canonical Excel/CSV header, and Hidden flag for derived question types (date=today, calculated_field, prepopulated, surveyor). Safe drop-in update; no schema or behavior changes for existing surveys.

= 4.8.3 =
Bug fix: surveys without any KPIs can now be saved. Safe drop-in update.

= 4.8.2 =
Bug fix: builder Types tab text was invisible in the default state due to a contrast collision. Safe drop-in update.

= 4.8.1 =
Cosmetic accessibility cleanup for static a11y audit tooling. Safe drop-in update; no schema changes, no behavior changes.

= 4.8.0 =
WCAG 2.1 AA accessibility pass on the respondent survey and the admin builder. No schema changes, no API changes, no behavior changes for existing users — just accessibility additions. Run any a11y scanner against your published surveys to verify improvement.

= 4.7.0 =
Adds per-KPI risk threshold overrides, a Power BI /risk endpoint, collaborative-edit conflict UI, and a Roboflow deploy guide for the YOLO pipeline. No schema changes; no migration required.

= 4.6.0 =
Polish Pack: Undo/Redo with 50-step history, drag questions between sections, one-click default option presets (Likert, NPS, Frequency, etc.), keyboard shortcuts (`/`, `d`, `Delete`, `?`), and a Desktop/Tablet/Mobile viewport toggle in the Preview modal. No schema changes.

= 4.5.0 =
Adds Autosave, Bulk Options Paste, Clone Survey, a per-user Question Library, in-browser Test Mode, schedule dates (available_from / available_until), a complexity badge on the All Surveys page, and a Find/Jump filter in the Builder header. New table `{prefix}stedica_question_library` is created automatically on upgrade.

= 4.4.0 =
Adds a survey location map to the Results page with date + question-based filters. Reuses the existing Leaflet load from the Geo Dashboard, so no new external dependency.

= 4.3.5 =
Adds an intro blurb to the KPI Risk Engine page describing what it does and what the visitor will see. No functional changes.

= 4.3.4 =
Renames the "AI Store Risk" module to "KPI Risk Engine" — same scoring engine, more honest name. No schema, slug, capability, or API changes; existing links keep working.

= 4.3.3 =
Adds a Power BI Integration card to the Stedica Surveys dashboard so the walkthrough is one click away from the landing page.

= 4.3.2 =
Documentation-only update: adds Security &amp; Data Integrity Compliance section to the Power BI Integration admin page. No schema or API changes.

= 4.3.1 =
Adds an in-WP Power BI Integration page (Stedica Surveys → Power BI Integration) that an analyst can follow without leaving the admin. Site URLs pre-filled, M snippets one-click-copyable, live surveys list.

= 4.3.0 =
Adds a Power BI / external-analytics REST API at `/wp-json/stedica-surveys/v1/` (surveys list, schema, paginated long-format results, JSON or CSV). Authenticate with a WordPress Application Password; reads gate on `stedica_edit_surveys`, respondent PII on `manage_options`. 120 req/min/user rate limit. Safe upgrade for everyone on 4.2.x — no schema or data migration.

= 4.2.7 =
Fixes the Survey Editor role missing after a 4.2.6 file-replacement upgrade. The role appears automatically on the next page load after upgrading — no deactivate/reactivate needed. Critical follow-up for anyone on 4.2.6.

= 4.2.6 =
Adds the Survey Editor user role for delegating survey management without granting full WordPress admin. Existing administrator accounts keep their access via an automatic capability grant — no manual cap assignment required.

= 4.2.5 =
Critical fix for Image Recognition (was silently broken) and silent-failure fix for all three AI question types (Product Recognition, Image Recognition, Image Comparison). Strongly recommended for any install using the Picture AI Analysis add-on.

= 4.2.4 =
UX-only fix: Builder Preview button now works (was previously a no-op — handler was missing). Safe upgrade for everyone on 4.2.3.

= 4.2.3 =
Companion release to the Picture AI Analysis add-on rename (previously Shelf AI Analysis). Renames the core stub class `Stedica_Shelf_AI` → `Stedica_Picture_AI` and the frontend `runShelfAI()` JS function → `runProductRecognition()`, with class_alias and JS shim for back-compat. AJAX action names and stored question-type keys are UNCHANGED — safe upgrade for everyone on 4.2.2.

= 4.2.2 =
Companion to Shelf-AI v2.1.0: the validator now recognises `product_recognition` as the canonical Shelf-AI product-detection type and auto-collapses both legacy keys. Safe upgrade for everyone on 4.2.1 even without the Shelf-AI add-on installed.

= 4.2.1 =
UX-only fix: Survey Settings panel spacing no longer collapses under the section title. Safe upgrade for everyone on 4.2.0.

= 4.2.0 =
Feature release: 22 question types with emoji icons in the builder, plus new Surveyor and Date types. The `prepopulated` and `shelf_ai` types now only appear when their add-on is active; the shelf-AI product-detection type is renamed `picos_recognition` (the old key still validates). Add-on-defined types in stored schemas are preserved across saves even when the add-on is inactive, so re-saving no longer destroys data.

= 4.1.9 =
Bug fix: KPI Definitions sidebar inputs no longer drop focus after typing the first character. Safe upgrade for everyone running 4.1.8.

= 4.1.8 =
Security release. Closes a stored-XSS path in the builder's inline JSON bootstrap, tightens the schema validator (unknown-key rejection, calculation/logic sanitization), fixes an admin Results page attribute breakout, and adds defence-in-depth around builder input slashing and add-on-supplied type labels. All installs should upgrade.

= 4.1.7 =
Security release. Hardens uploads, AJAX, uninstall cleanup, and adds a proper GDPR exporter/eraser. All installs should upgrade.

= 4.1.0 =
Introduces generic KPI Definitions — the Leaderboard and Risk Alerts dashboards now work with any survey schema via user-defined KPIs. Previous hardcoded compliance metrics are replaced; re-create any desired metrics under Stedica Surveys → KPI Definitions.

= 4.0.3 =
Documentation update for wp.org submission. No functional changes.

= 4.0.2 =
Fixes a gating hole where Pro features could unlock before license activation. All users on 4.0.x should upgrade.

= 4.0.0 =
Major release introducing Pro tier with licensing. Existing surveys and responses are preserved — no data migration required.
