=== Bodholdt Contact ===
Contributors: bodholdtlabs
Tags: contact form, contact widget, support, anti-spam, customer
Requires at least: 6.4
Tested up to: 7.1
Requires PHP: 8.0
Stable tag: 5.3.2
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

A secure contact widget with email verification and optional recognition for signed-in customers.

== Description ==

Bodholdt Contact adds a small floating contact button to your site. New installations keep the widget hidden until an administrator reviews the settings and enables it. When a visitor opens the enabled widget and enters an email address, one of two things happens:

* **A securely recognized, signed-in customer can skip the code.** The visitor must be signed in to a WordPress account whose email exactly matches the address entered. In Bodholdt Licensing mode, that same account email must also have a currently active, non-expired license. Merely entering a customer's email never bypasses verification.
* **Everyone else verifies by email.** The visitor receives a one-time 6-digit code before the message form opens. Challenges are short-lived, bound to the browser flow, attempt-limited, and single-use. A honeypot and atomic rate limits further constrain automated abuse.

This keeps the signed-in customer path quick without treating a known email address as proof of identity.

**Features**

* Floating contact widget (footer) with a distinctive, themeable look.
* Session-bound one-time-code email verification for unsigned-in and unrecognized senders, with resend and retry controls.
* Honeypot protection plus atomic source, destination, and global rate limits.
* "Recognize returning customers via": choose none, matching signed-in WordPress users, or matching signed-in users with an active Bodholdt Licensing entitlement.
* A settings page that opens with a plain-language status card ("is it working?") and a live preview you can step through all three visitor screens: brand name, accent color with curated swatches, light/dark/auto appearance, corner style, header/intro/success copy, and the email that messages are sent to.
* Operator controls for an optional weekly activity email and the private "Working the Door" Fun Pass, including its sound and confetti effects.
* Fully translatable, with a translation template (`languages/bodholdt-contact.pot`) included in the download, and layouts that follow the reading direction so right-to-left languages display correctly.
* Multisite ready: network activation prepares every existing site and every site added later, each with its own settings and storage.

== Installation ==

1. Upload the `bodholdt-contact` folder to `/wp-content/plugins/`, or install the ZIP via Plugins → Add New → Upload.
2. Activate the plugin.
3. Go to **Settings → Bodholdt Contact** to set your brand name, accent color, copy, notification email, customer-recognition mode, and optional operator reports.
4. Review the live preview, select **Show the widget**, and save. The widget then appears in your site footer.

== Frequently Asked Questions ==

= Do my customers have to verify by email? =
Usually, unless they are securely recognized. In WordPress-user mode, a visitor can skip the code only while signed in to an account whose email exactly matches the address entered. In Bodholdt Licensing mode, that signed-in account must also have an active, non-expired license for the same email. Signed-out visitors always verify, even if they enter a customer's address.

= Why is the widget not visible immediately after activation? =
Fresh installations are deliberately disabled so an unfinished contact form is not exposed with an unreviewed destination address or recognition policy. Open **Settings → Bodholdt Contact**, review the configuration, enable **Show the widget**, and save. Upgrading from an earlier version preserves the site's existing enabled or disabled setting.

= Does it require Bodholdt Licensing? =
No. The Bodholdt Licensing recognition mode needs it, but the widget works without it using matching signed-in WordPress-user recognition or email verification for everyone.

= Where do submitted messages go? =
To the email address you set in Settings → Bodholdt Contact (defaults to your site admin email), with the sender's address as Reply-To.

= Does it work with the other Bodholdt Labs plugins? =
Yes. If a compatible **Bodholdt Tickets** release is already active, Contact can route accepted messages into that local ticket store. If the compatible ticket API is unavailable or ticket creation cleanly fails, Contact uses its configured email route. If **Bodholdt Licensing** is already active, its local entitlement table can recognize a matching signed-in account with an active, non-expired license. These are passive local compatibility paths; Contact does not install, advertise, purchase, or contact either product or any outside service.

= Can I translate it? =
Yes. Every visitor-facing and administrator-facing string is translatable, and the download includes a ready-made template at `languages/bodholdt-contact.pot`. Open it in any translation editor, save the finished `bodholdt-contact-{locale}.mo` file into the plugin's `languages/` folder, and it loads automatically. Your own brand name, header, intro and success copy are typed on the settings page rather than translated, so they read exactly as you set them.

= Does it work on a multisite network? =
Yes. Activating it for the whole network prepares every existing site, and any site created later is prepared automatically the moment it is added, so you do not have to visit each site's admin. Every site keeps its own settings, its own verification tables, and its own scheduled cleanup. Deactivating across the network clears the scheduled tasks on every site, and deleting the plugin from the network removes its data site by site.

= What database tables does it create? =
Two of its own, both prefixed with your WordPress table prefix: `bodholdt_contact_challenges` (short-lived verification state, with raw email but HMAC-hashed codes and flow tokens) and `bodholdt_contact_rate_limits` (pseudonymous abuse counters keyed by a server-secret, time-windowed HMAC of an email address or source IP address). A rate row contains the HMAC bucket, non-sensitive scope, hit count, creation time, and scheduled expiry; it never contains the raw email address or IP address. Because this site can recompute a matching HMAC during a retained window, these counters must be inventoried as pseudonymous personal data, not anonymous data.

Email and IP scopes use 15-minute or one-hour fixed windows. A row stops being used at its scheduled expiry, no later than two hours after its window opened, and the plugin schedules hourly deletion. WordPress scheduling or database outages can delay physical deletion until cleanup can run successfully. The optional Working the Door arcade adds four shared `bod_arcade_*` tables (`bod_arcade_progress`, `bod_arcade_achievements`, `bod_arcade_events`, `bod_arcade_celebrations`) shared with any other Bodholdt plugin using the arcade. Those tables hold operator gamification data, not visitor contact-form data. Uninstall drops both Contact-owned tables and removes only Contact's rows from the shared arcade tables.

= What personal data does it store, and how do I handle export and erase requests? =
The plugin stores the email address entered with the short-lived verification state in `bodholdt_contact_challenges`. It also stores short-lived pseudonymous rate counters for destination email and source IP. The counters exist only to prevent automated verification-mail, guessing, form, and submission abuse. Their fields are scope, hits, creation time, scheduled expiry, and a time-windowed HMAC bucket. They stay in your WordPress database and hosting environment; Contact sends them to no recipient or processor of its own. You must document the lawful basis you actually rely on for this security processing, your hosting/mail/ticket processors, and any jurisdiction-specific limits or objections with qualified counsel.

**Tools → Export Personal Data** returns both verification records and matching active email-keyed counters. **Tools → Erase Personal Data** deletes both and fails visibly if either Contact table cannot be read, a delete fails, or only part of the selected set is removed. Email addresses are validated and lowercased through the same canonical method at collection and during either privacy operation, so a mixed-case request still finds its active records. If a requester separately supplies the source IP address used with the form, the operator can call the bounded `Bodholdt\Contact\Privacy::export_rate_identifier( $ip, 'ip' )` or `erase_rate_identifier( $ip, 'ip' )` method from an authenticated administrative tool; it enumerates only still-retained documented IP scopes/windows and never exports the HMAC. IPv4 and IPv6 are canonicalized with `inet_pton()`/`inet_ntop()` at collection and lookup, so equivalent IPv6 spellings reproduce one subject-only candidate set. Version 5.2.4 performs a one-time deletion of all older ephemeral rate rows before committing its database-version marker because releases through 5.2.2 hashed raw IPv6 presentation text and those rows cannot be migrated exhaustively without the original address. Do not put a requester-supplied IP into a shell command or public endpoint. Where messages are saved by a compatible **Bodholdt Tickets** release, the eraser truthfully reports that the ticket store must also be handled. With no ticket destination, the message is emailed and Contact stores no message copy.

= Does it set a cookie? =
Yes. The necessary `bodholdt_contact_flow` cookie contains opaque verification-flow and session bindings so a cached page can continue the same request. Its `HttpOnly` attribute means JavaScript cannot read the cookie itself. The same-origin JSON replies also return the current flow-token values, which the plugin's own widget JavaScript keeps in memory while the verification flow is open. The cookie is path-wide, `SameSite=Lax`, `Secure` on HTTPS, lasts no more than 10 minutes, and is deleted earlier after a message is sent successfully. Review how your jurisdiction presents necessary cookies with qualified counsel and describe it in the site's cookie/privacy notice. Contact sets no advertising or analytics cookie.

== Screenshots ==

1. The floating contact widget open on a site, ready for an email address.
2. A recognized visitor signed in to the matching account skips the code and goes straight to the message box (with an "account recognized" badge).
3. Unknown senders verify with a one-time 6-digit email code before their message goes through.
4. The settings page, with a live preview that updates as you change the brand name, accent color, and copy.

== Third-party libraries ==

* **canvas-confetti 1.5.1** - ISC license. Source and issue tracker:
  https://github.com/catdad/canvas-confetti
  Shipped at `assets/arcade/js/confetti.min.js` from jsDelivr's minified
  distribution for the upstream 1.5.1 tag, with a local license preface. The
  jsDelivr body is pinned at SHA-256
  `4de41904d58bcadfaf01b81f8345b2384fa131d2da40cd9e337175f7f4df50ed`, and the
  unmodified ISC license text is alongside it at
  `assets/arcade/js/LICENSE-canvas-confetti-ISC.txt`. It is only enqueued when the
  optional celebration effect is switched on, and everything works with it absent.
* **Orbitron** (SIL Open Font License 1.1) and **Roboto Mono** (Apache License 2.0),
  self-hosted under `assets/arcade/fonts/` with their license texts alongside. They
  are served from your own site; this plugin never requests a font from a third party.

== External services ==

This plugin makes one kind of outside request: it checks bodholdtlabs.com for plugin
updates. You can turn that off, and everything else stays on your server.

* **What the update check does.** When WordPress refreshes its plugin update list,
  typically twice a day, the plugin asks
  `https://bodholdtlabs.com/wp-json/bodholdt-licensing/v1/update-check/bodholdt-contact`
  for the current version number. A successful answer is cached for 12 hours, so ordinary
  admin page loads read the cached answer instead of the network. If the check cannot
  complete (no network, an error, or a request your host blocks), that result is cached
  too and the plugin tries again no sooner than an hour later. If a newer version exists,
  the update offered on your Plugins screen downloads the same public file anyone can
  fetch from bodholdtlabs.com.
* **What it sends.** The request sends two things: the plugin version and your site
  address, in the user agent, the same way WordPress itself identifies your site when it
  talks to WordPress.org. It sends nothing else: no messages, no visitor email addresses,
  no settings, and no license key (this plugin has none). That is the whole payload.
* **How to turn it off.** Add this line to a small site plugin or your theme's
  functions.php: `add_filter( 'bodholdt_contact_update_check_enabled', '__return_false' );`
  The plugin then makes no outside requests at all, and you update by downloading and
  reinstalling the ZIP yourself.
* **Everything else is local.** The plugin contacts no third-party service, loads no
  remote fonts, scripts, or styles, and sends no analytics or telemetry. Verification
  codes and notifications go out through your site's own configured mail (wp_mail), so
  apart from that email and the update check above, nothing leaves your server.

The update endpoint is operated by Bodholdt Labs, the maker of this plugin, under the
same privacy policy as the rest of the site: https://bodholdtlabs.com/privacy-policy/

== Security ==

Found a security problem in this plugin? Please tell us before you publish it, and we
will tell you what we found and when a fix will ship.

Email support@bodholdtlabs.com with "Security" in the subject line. We acknowledge
within five business days. Full policy, including what is in scope and what we ask of
you, is at https://bodholdtlabs.com/security/

We do not run a bug bounty and cannot pay for reports. We would rather say so up front
than have you find out afterwards.

== Changelog ==

= 5.3.2 (2026-09-05) =
* The changelog now reads like a changelog. Several entries from August described release machinery rather than the plugin: package validators, audit receipts, hash-bound tool inputs and publishing gates. None of it is anything you can see or use, and it was sitting on the product page and the site changelog where people decide whether to install. Removed rather than reworded, because there is no reader-facing version of most of it. Everything describing an actual change to the plugin is untouched.
* One of those bullets also claimed the translation template pointed at our support address. It did not; the file carried the WordPress.org default, which goes nowhere for a plugin that is not hosted there. The claim is withdrawn from the release it was attached to.
* The note about turning the arcade back on pointed at an "Arcade settings" screen that does not exist. It is under Settings, then Bodholdt Contact, in the row headed "Working the Door".
* The full download no longer contains a second readme describing the WordPress.org build, which creates a different set of database tables. Two readmes disagreeing about the same product is confusing on its own; the build now refuses to ship it.

= 5.3.1 (2026-09-04) =
* Confetti in the admin now really does start switched off, the way the documentation has always said it does. The setting was never written down when the plugin was activated, so a new site fell back to the checkbox's own default and started with it on. If you already had confetti, you keep it; nothing is switched off underneath you.

= 5.3.0 (2026-08-28) =
* **The Door Report no longer looks broken before it has anything to report.** On the first open after you turn on Working the Door, all four counters were zero, and the display font draws a zero with a diagonal bar through it, which reads as the browser's broken-image icon. A panel that has not counted anything now says so in a sentence, and once real counts exist the big numerals come back. A single zero sitting among real counts is drawn in the plain admin font instead.
* **You can now see the bot trap working.** The contact form has always hidden a trap field that only automated senders fill in, but nothing ever showed you it was doing anything unless you had switched the activity report on. The settings status card now carries a line for it, and there is a matching entry under Tools, Site Health. It counts trap trips over the last 7 days and says nothing more than that, because a sender turned away earlier by the request checks never reaches the trap and cannot be counted.
* **The verification email says who it is from.** The subject was "Your site name: verification code" and is now "Your {site name} verification code", so it reads as a sentence and is easier to find in a crowded inbox. Your site name, or the brand you set in the plugin, is what appears.

= 5.2.12 =
* **The widget stops offering categories that need a support ticket, and stops quietly turning one into an email.** When Bodholdt Tickets is installed with its paid support model switched on, the widget used to mirror every category, including the ones that cost a credit. If someone picked one, the ticket engine refused, this plugin threw the reason away, sent the message to you as plain email instead, and told the visitor it had been sent. So the same question was paid on one page and free on another, and you got it as mail with no ticket to track. The widget now leaves those categories out, and in the rare case one is submitted anyway, the visitor is told plainly to open it on the Support page instead of being quietly rerouted.

= 5.2.11 =
* **The readme now describes the one request this plugin makes.** The External services
  section said the plugin made no external requests and had no update checker. It has had
  an update checker since 5.2.6, and that section was never updated. It now says exactly
  what the check asks for, what it sends (the plugin version and your site address, and
  nothing else), how long each answer is cached, and how to switch it off.
* **The update check now has an off switch.** Add
  `add_filter( 'bodholdt_contact_update_check_enabled', '__return_false' );` and the plugin
  makes no outside requests at all.
* **Uninstall now removes the update-check cache** it had been leaving behind.

= 5.2.10 =
* **The arcade is fully translatable now, and its toasts can be dismissed.** Several labels the arcade shows (the dismiss button, the "Nice" message on a plain points toast, and the level badge) stayed in English no matter what language your site runs in, because the shared arcade code never passed them through for translation. They now translate like everything else, the level number is formatted for your locale, and every celebration has a close button. Toasts also pause while you hover or focus them, and skip their timer entirely if you have asked your system to reduce motion.
* **A site that cannot create the arcade tables now keeps trying.** On a host where the database user is not allowed to create tables, the plugin recorded the arcade as installed anyway and never tried again, so the arcade stayed silently broken forever. It now confirms the tables really exist before recording success, and retries later if they do not.

= 5.2.9 =
* **The arcade settings box now matches the arcade.** Since 5.2.8 the arcade has been off on a fresh install, but the checkbox on the Settings screen still drew itself as switched on, and saving your settings for any reason turned the arcade on for real. The box now shows the truth, and saving your settings leaves the arcade exactly as you found it. If you had turned it on yourself, it stays on.

= 5.2.8 =
* **Gamification is now off by default.** The arcade (XP, ranks, the HUD, and confetti) starts off. Turn it on any time under Settings, then Bodholdt Contact, in the row headed "Working the Door". Nothing else changes.

= 5.2.7 =
* **American spellings throughout.** The plugin and this changelog used British spellings in places (licence, colour, behaviour, cancelled). They now match the rest of Bodholdt Labs. Wording only, nothing behaves differently.

= 5.2.6 (2026-08-21) =
* **This plugin can now update itself.** Until today it had no update client at all, so a new version, including a security fix, only reached you if you happened to visit the site and reinstall by hand. Bodholdt Contact now checks for a newer version and offers it on your Plugins screen like any other plugin. It carries no license key and sends none: the check asks a public address for the current version number, and the download is the same public link a person would click.

= 5.2.5 (2026-08-21) =
* **The contact card now takes keyboard focus when it opens.** The card is hidden behind a short fade, and focus was being moved into it before that fade had started, so the browser discarded it and focus stayed on the button behind the card. Anyone using a keyboard or a screen reader landed outside a dialog that announces itself as modal, and pressing Tab walked into the page behind instead of moving through the form. Focus now waits for the card to be on screen, which also restores the Tab trap that keeps you inside the form until you close it.

= 5.2.4 (2026-08-11) =
* **Failed-code accounting is proven or unavailable.** The OTP repository validates the exact database write, follow-up row, counter, state, and database error before returning a remaining-attempt count. Storage uncertainty returns a visible 503 without an invented attempt budget, while a concurrent fifth failure returns the proven locked count.
* **Legacy IPv6 rate rows have a complete transition.** Before the 5.2.4 database marker can advance, the bounded upgrade deletes every Contact-owned pre-canonical ephemeral rate row and verifies the table is empty. New collection, export, and erasure use one canonical `inet_pton()`/`inet_ntop()` representation for every equivalent IPv6 spelling.

= 5.2.3 (2026-08-10) =
* **Identifier matching is canonical end to end.** Collection, export, and erasure share one validated lowercase email normalizer. Source addresses use `inet_pton()`/`inet_ntop()` at collection and lookup, with a bounded compatibility set for still-active pre-5.2.3 IPv6 HMAC rows.
* **Rate-limit storage faults fail closed.** The deciding counter read and its write accept only exact database result types and reject false, null, non-integer, negative, or last-error states with a visible storage-unavailable response.
* **Diagnostics cannot retain subject data.** Mail health stores only allowlisted context/status timestamps for at most seven days, and an upgrade removes the older arbitrary-error field. Debug logs accept only request-independent event codes; ticket IDs, challenge IDs, addresses, inputs, and exception text are not accepted by the logger.
* **Cookie wording matches runtime.** The inventory now distinguishes the JavaScript-unreadable HttpOnly cookie from flow values returned in same-origin JSON to the widget.

= 5.2.2 (2026-08-10) =
* **Pseudonymous counters are classified and handled truthfully.** The privacy inventory, suggested policy, exporter, and eraser now cover still-active email-keyed abuse counters. A bounded operator API handles a separately supplied IP address without scanning or exposing unrelated rows. Database read, delete, and partial-delete faults remain visible rather than reporting a completed request.
* **Delivery diagnostics are subject-free.** A rejected message logs only a request-independent transport error; no stable email-derived identifier or raw sender address is written to the server log.
* **The necessary flow cookie is disclosed.** Operator copy names `bodholdt_contact_flow`, its opaque purpose, 10-minute maximum, `HttpOnly`/`SameSite=Lax` controls, HTTPS-only Secure flag, and early deletion.

= 5.2.1 (2026-08-10) =
* **Privacy requests now fail visibly if storage fails.** Export and erase callbacks return a WordPress error when the verification table cannot be read, a delete fails, or only part of the selected page is deleted. They no longer report a completed request that never reached the data.
* **Failed resends no longer spend delivery quota.** When the mail transport returns false or throws, the destination and source-address hits are refunded to the exact windows originally charged while global anti-abuse counters remain in place.
* **No held-product promotion.** The settings-page Ticketing cross-sell and its sales link were removed. Passive local compatibility with already-installed Tickets and Licensing releases remains documented without a purchase link.

= 5.2.0 (2026-08-05) =
* **The plugin can now actually be translated.** Every screen was already written for translation, but the download carried no template to translate against and no folder for finished translations, so it stayed English in every language. Both now ship. The download includes `languages/bodholdt-contact.pot`, and a finished `.mo` file placed in `languages/` is loaded automatically.
* **Right-to-left languages.** Every style now follows the reading direction, so the widget, the settings page and the activity report lay out correctly in Arabic, Hebrew, Farsi and any other right-to-left locale.
* **You are told when your site email stops working.** A broken mail transport takes the contact form down for every visitor, and it also swallows the weekly summary that would otherwise have warned you, so a dead mailbox used to look like a quiet week. The settings page and an admin notice now say plainly what could not be sent, when, and what the mail server said about it. The warning clears itself as soon as mail works again.
* **A warning when a proxy hides your visitors.** If a CDN or load balancer sits in front of your site and your web server does not pass the real visitor address through, every visitor shares one hourly limit and the contact form stops for everybody after fifteen codes. **Tools → Site Health** now reports this, the settings page says so too, and both explain how to fix it. Forwarded headers are still never trusted on their own, because anyone can send one.
* **Numbers and waiting times read correctly in every language.** "You can try again in about 1 minutes" is gone. Waits, counts and percentages are now formatted for the reader's language.
* **Smaller fixes.** "Wrong email? Go back" keeps what the visitor typed again, which is what the widget always said it did. Handing back an unused verification code now credits the correct hour. The public token-refresh endpoint follows the same on/off and storage checks as the rest of the form and is rate limited. Deleting the plugin no longer leaves two internal markers behind. Decorative characters in the interface were replaced with drawn icons.
* The minimum WordPress version is now 6.4, lowered from 6.5 to match the oldest release this code actually needs.

= 5.1.0 (2026-07-19) =
* **Messages now reach you exactly as they were typed.** A link containing encoded characters used to lose part of itself on the way to you, so a pasted address could arrive broken. Quoted markup, such as a customer showing you the HTML on their page, could vanish from the message entirely. Both are fixed. Messages routed into Bodholdt Tickets are now escaped as well, so nothing a visitor sends can render as live content in your Support Desk.
* **Asking for a fresh code works again.** The hourly limit on codes sent to one address was exactly the number the widget itself could spend, so "start again to request a new code" was always refused. The limit now leaves room to start over, and when a visitor does have to wait, the widget says roughly how long.
* **Clearer answers when something is wrong.** If the plugin cannot create its database tables, the settings page and an admin notice now say so plainly instead of reporting that the widget is live. If a saved setting had to be shortened, or had formatting removed, the page now names the field that changed instead of only saying "Settings saved".
* **Friendlier recovery in the widget.** After too many incorrect codes the widget returns the visitor to the email step, rather than offering a Resend button that could never work. The resend countdown now follows real time, so it no longer looks stuck after someone switches to their mail app on a phone.
* **Sites behind a proxy or CDN.** Added a `bodholdt_contact_client_ip` filter so operators whose web server does not pass through the real visitor address can supply it. Without it, every visitor could end up sharing a single rate limit bucket. Forwarded headers are still never trusted by default.
* **Smaller fixes.** A three-digit color such as #0af no longer produces invalid styling. The message box now shows its length limit before you send. The suggested privacy policy wording and the erase result describe the ticket store accurately when Bodholdt Tickets is handling messages. The Licensing note no longer contradicts the status card. Gamification labels can now be translated. A failed table creation no longer repeats a full migration on every page load, and no longer disables the activity report permanently.

= 5.0.2 (2026-07-19) =
* **Privacy tools (GDPR export and erase).** The plugin now integrates with the built-in WordPress privacy tools. **Tools → Export Personal Data** returns the verification records held for an email address, and **Tools → Erase Personal Data** deletes them, so you can answer data subject requests the standard way. The plugin also adds suggested wording to **Settings → Privacy** for your privacy policy. No change to how the contact form works.

= 5.0.1 (2026-07-19) =
* **Accessible contact form (WCAG 2.1 AA).** The visitor widget now passes accessibility review: readable success and error text in dark mode, visible field borders and focus outlines, autocomplete on the email and verification-code fields, a properly announced delivery confirmation, and correct focus handling on Resend. This matters if your site serves visitors in the EU, where the European Accessibility Act applies to consumer-facing forms.
* **Licensing and packaging.** The bundled confetti library now ships its required copyright notice, and the release archive uses correct, non-world-writable file permissions. The readme now names the database tables the plugin creates.

= 5.0.0 (2026-07-16) =
* **Stronger verification state.** One-time codes and opaque flow tokens are stored as HMAC hashes in plugin-owned per-site tables. Each challenge is bound to its browser flow, expires quickly, enforces exact six-digit input and attempt/resend limits, and can authorize at most one delivered message through atomic state transitions.
* **Bounded abuse handling.** Atomic database-backed limits now cover source addresses, destination emails, global mail volume, widget opens, and message attempts. An hourly security task removes expired challenges and counters; a failed delivery releases the verified flow for a safe retry instead of silently consuming it.
* **Precise customer recognition.** Skipping the emailed code requires a signed-in WordPress account whose email exactly matches the address entered. Licensing mode additionally requires a currently active, non-expired Bodholdt license, checked again when the message is submitted. A known email address alone is never proof of identity.
* **Safer request boundaries.** Each public action has its own nonce, cached pages can refresh the complete nonce set, malformed non-scalar fields are rejected cleanly, and message length is validated rather than silently truncated.
* **Safer delivery.** A support ticket is considered delivered only after its first message is stored. If that partial write fails, direct-email fallback occurs only after the empty ticket is confirmed removed, preventing duplicate or orphaned customer conversations.
* **Deliberate first run and upgrades.** Fresh installations start with the widget hidden and weekly summary email off, then show a direct setup notice. Existing installations retain their prior live/hidden behavior and automatic reports even if they never opened the old settings page. In-place and automatic updates create required storage on the first request instead of waiting for an administrator visit.
* **Operator controls that match the documentation.** The settings page now exposes the weekly activity email and the private Fun Pass, sound, and confetti switches. Blank display copy falls back consistently in both the preview and live accessible dialog.
* **Complete lifecycle support.** Activation creates or upgrades per-site security storage and schedules cleanup; network activation covers existing sites and sites added later. Deactivation clears short-lived security state and scheduled tasks without deleting configuration. Uninstall removes this plugin's settings, owned tables, legacy verification remnants, scheduled tasks, and only its own rows from shared Bodholdt Arcade tables.
* Requires WordPress 6.4 or newer and PHP 8.0 or newer.

= 4.7.4 (2026-07-11) =
* **Sturdier under odd input.** The contact endpoints and the settings save now shrug off malformed requests (for example a field sent in the wrong shape) and return a normal validation response instead of erroring. This only ever affected hand crafted requests, never a real visitor or a normal save, but it keeps your error log clean.
* **Precise multisite cleanup.** On a multisite network, turning the plugin off on a single site now leaves the other sites' weekly report untouched; only a full network deactivation clears them all.
* **Housekeeping.** Trimmed a few unused entries from the "Working the Door" scoreboard so its catalog only lists rewards you can actually earn.

= 4.7.3 (2026-07-11) =
* **Cleaner activity readout.** The "last 7 days" line and the weekly email always show sensible figures now. Percentages stay between 0 and 100, the counts never read backwards, and the wording reads correctly even when a number is 1.
* **Tidier multisite cleanup.** On a multisite network, turning the plugin off now clears its scheduled weekly report on every site, leaving nothing behind.

= 4.7.2 (2026-07-11) =
* **True open-rate.** The activity summary now also counts how many people opened the widget, so you can see the whole funnel: opened, entered an email, and finished. It reports the overall share of opens that turned into a message, which tells you at a glance whether people are dropping off before they even start.

= 4.7.1 (2026-07-11) =
* **See how your contact form is doing.** The settings page now shows a short "last 7 days" activity line (how many people started, how many were sent a code, and how many finished after verifying), and you get an optional weekly email summary. This makes it easy to tell whether the email step is turning visitors away, so you can switch recognition mode if it is.

= 4.7.0 (2026-07-11) =
* **Stronger proof before the code is skipped.** A returning customer now skips the email code only when they are signed in to the matching account. A visitor who simply types a known customer's email address is asked to verify by code like anyone else. This closes a way someone could send you a message posing as one of your customers.
* **Tougher on abuse.** Verification codes are now capped per destination address as well as per sender, a valid code can be used only once even under a rapid burst of requests, and automated "bot" traffic is rate limited before it can touch anything. Together these bound spam and reduce the widget's usefulness for sending unwanted code emails to other people.
* **Off means off.** When you switch the widget off, its background endpoints stop responding too, not just the visible button.
* **Accurate tallies.** The "Working the Door" counts tick over only after a message is actually delivered, so your Door Report reflects reality even when mail is misconfigured.
* **Polish.** Very long messages are trimmed to a sensible length, the celebration animations stay within the screen, and the arcade labels are translatable (the shared level-up HUD badges stay in English, since a shared component cannot carry a fixed text domain).

= 4.6.0 (2026-07-11) =
* **The code is checked the moment you enter it.** When a visitor types their verification code and continues, the widget now confirms it right away, so a wrong or mistyped code is caught before they write their whole message instead of after.
* **The widget keeps working on cached sites.** If your pages are served from a cache and the security token in the page has gone stale, the widget quietly refreshes it and carries on, so the contact form no longer breaks with a "network error" on heavily cached sites.
* **You are told when a code cannot be emailed.** If your site cannot send the verification email (for example mail is not configured yet), the visitor now sees a clear "please try again in a few minutes" message instead of being stranded on a code screen with no code ever arriving. Submitted messages are handled the same way, so a message is never quietly lost.
* **Clean removal.** Deleting the plugin now removes its settings, its "Working the Door" tallies, and its rows in the shared arcade tables. The four shared `bod_arcade_*` tables and the `bod_arcade_db_version` option are deliberately left alone, because uninstall has no safe way to tell whether another Bodholdt plugin still needs them. If Contact was the only Bodholdt plugin on the site, those four tables stay behind empty and you can drop them yourself.
* **Polish.** The notification address now tells you if what you entered was not a valid email instead of silently ignoring it, the header, intro and success text show exactly what you typed, those fields gained sensible length limits, and the "Working the Door" operator text is translatable.

= 4.5.1 (2026-07-11) =
* **The widget stays readable with any accent color.** Button text and links now choose their color from the accent's real contrast, so a light accent (like a bright cyan) gets dark, legible text instead of washing out on the white card. The admin live preview reflects the same result.

= 4.5.0 (2026-07-11) =
* **Working the Door.** A calm, optional arcade for you, the operator. Because contact activity happens while you are away, the settings page now greets you with a Door Report of who you waved through, the strangers who verified, the bots you bounced, and the messages handled since your last visit, plus a rank and XP that grow as your door gets busier. It sits behind a Fun Pass switch (on by default, sound off) and stores everything in your own database.

= 4.4.0 (2026-07-10) =
* **A clearer settings page and a preview you can walk through.** The page now opens with a status card that answers "is my widget live and doing what I set up?" in plain sentences: whether it shows, where messages go, who skips verification, and the address you are notified at. The live preview steps through all three screens a visitor sees (email, then the 6-digit code, then the message and its success note), so the verification flow you configure is no longer invisible. On wide screens the form and preview sit side by side. Accent color gains curated swatches and a readability note, and the appearance and corner-style choices became tidy segmented controls.
* **The verification step no longer dead-ends.** If a visitor's code does not arrive or they typed the wrong address, they can now "Resend code" (with a short countdown that respects the anti-spam limit) or go back and fix their email. Running out of attempts now says so and offers a fresh code instead of failing silently. The opening line also explains why the email is asked for first.

= 4.3.0 (2026-07-09) =
* Added a dismissible panel on the settings page inviting you to turn contact messages into trackable Bodholdt Tickets when that plugin is not active. This mirrors the confirmation shown when it is active. Nothing about the widget changed.

= 4.2.0 (2026-06-05) =
* **White-label appearance.** The widget now offers Light, Dark, or Auto (match the visitor's system) display modes plus a corner-style choice, all derived from your single accent color with readable text and contrast shades computed automatically, so it blends into any site. The settings page got a matching refresh and the live preview reflects every change.

= 4.1.3 (2026-06-04) =
* WordPress.org readiness pass: added translator comments for placeholder strings, removed the manual text-domain load (WordPress loads translations automatically), and hardened a server-variable read. No change to how the widget behaves.

= 4.1.2 (2026-06-03) =
* Renamed the Bodholdt Tickets integration to its current name and refined the verification step layout. The email address and the "enter your code" instruction now sit on their own lines.

= 4.1.1 (2026-05-22) =
* Settings page now shows a **live preview** of the widget that updates as you change the accent color, header, and intro (§7.4 Steve Jobs pass).

= 4.1.0 (2026-05-20) =
* Bodholdt Tickets front-door. With a compatible Bodholdt Tickets release active, contact submissions can create a support ticket (and notify you by email) instead of a plain email, so customers can track and reply to them, and you work them in the Support Desk. If Tickets has categories, the widget shows a “What’s this about?” picker. Without a compatible Tickets API, it behaves exactly as before (emails you). All recognition + anti-spam unchanged.

= 4.0.0 (2026-05-20) =
* Reconfigured for distribution: renamed to Bodholdt Contact, added a multi-tenant settings page (brand name, accent color, copy, notification email, customer-recognition mode), made visitor-facing branding configurable, internationalized the interface (text domain `bodholdt-contact`), and packaged it for release. All anti-spam protections (one-time code, honeypot, rate limiting, brute-force lockout, timing-safe comparison) retained.
