---
title: "Crazy Highlights 'n Doodles | Hand-Drawn YOOtheme Pro Effects"
description: "Add hand-drawn highlights, doodles, arrows, stickers and masks to YOOtheme Pro. Create playful marker-style designs without code or complex setup today."
url: "https://www.yoozecase.com/?item_id=9"
date: "2026-10-08T06:46:14+00:00"
language: "en-GB"
---

#  Crazy Highlights 'n Doodles

Mark my words!

**Latest Version**
1.4.0
**Requires**
Yootheme Pro 5+, Joomla 5+, WordPress 6.5+, PHP 8.1 or newer
**Release date**
01 October 2026
**Made for**
Yootheme Pro, Joomla, Wordpress
**License**
GPL-2.0-or-later
**Price**
€25.00 (incl. 6 months of updates)

 [ 🛒 Get it for Joomla 👍 ](https://yoozecase.lemonsqueezy.com/checkout/buy/aa75d2c6-18f3-45ae-9555-2ca3b76a4e28)

 [ 🛒 Get it for WordPress👍 ](https://yoozecase.lemonsqueezy.com/checkout/buy/6096479a-aead-4480-b072-918f89fdbac3)

 [ Demo page ](https://www.yoozecase.com/crazy-highlights-n-doodles-demo)

Hand-drawn highlights, circles, arrows and doodles that scribble in as you scroll.

##  What's new in version 1.4.0

 ![Crazy Highlights N Doodles 1](https://www.yoozecase.com/files/crazy-highlights-n-doodles_1.png)

 [](#)

 [](#)

---

##  Description

Bring a hand-drawn feel to your YOOtheme Pro website with Crazy Highlights 'n Doodles. Add realistic marker strokes, doodles, arrows, stickers and creative accents that make your content feel unique and personal.&nbsp

Create playful designs with flowing highlights, handwritten notes, custom shapes, image masks, animations and theme-aware colours. No code needed, just pick your style and start drawing.

##  Feature request

 ### What is the next feature for this plugin?

Submit your idea or vote on existing ones.

  #### Top voted

   Website (leave empty)

'; } html += ''; if (oc.title) { html += '##### ' + escapeHtml(oc.title) + '

'; } if (oc.text) { html += '' + escapeHtml(oc.text).replace(/\\n/g, '
') + '

'; } html += '

'; } // Auto winner(s) - top N from the already vote-sorted features if (mode === 'auto_winner' || mode === 'both') { var n = oc.winnerCount | 0; if (n '; for (var i = 0; i ' + (w.image_url ? '![](https://www.yoozecase.com/'%20+%20escapeHtml(w.image_url)%20+%20')

' : '') + '' + (i + 1) + '' + '' + escapeHtml(w.title) + '' + '' + getIcon(this.cfg.voteIcon) + ' ' + (w.votes | 0) + '' + ''; } html += ''; } } if (html === '') { this.elOutcome.hidden = true; return; } this.elOutcome.innerHTML = html; this.elOutcome.hidden = false; }; /** * Roadmap view (v3.2.0): renders the items as status columns, in the * canonical status order from cfg.statuses. Each column is an 12. inside \* the existing .yc-fb-list , holding its own inner  of items - \* this keeps the delegated click/change handlers on elList working \* unchanged. Unknown statuses fall back into the "open" column so no item \* can silently disappear. Empty columns are skipped when \* cfg.roadmapHideEmpty is set. \*/ Board.prototype.renderRoadmap = function (items) { var stats = (this.cfg.statuses && this.cfg.statuses.length) ? this.cfg.statuses : \['open', 'planned', 'in\_progress', 'shipped', 'declined'\]; var labels = this.cfg.statusLabels || {}; var groups = {}; var i, st; for (i = 0; i ' + '##### ' + '' + escapeHtml(labels\[sv\] || sv) + '' + arr.length + '' + '

    ' + '' + itemsHtml + '
    ' + '
'; } return html; }; Board.prototype.renderItem = function (f, index) { var disabled = this.cfg.isClosed ? 'disabled' : ''; var voted = f.voted ? ' yc-voted' : ''; var descHtml = f.description ? '' + escapeHtml(f.description).replace(/\\n/g, '
') + '

' : ''; var thumbHtml = f.image_url ? '![](https://www.yoozecase.com/'%20+%20escapeHtml(f.image_url)%20+%20')

' : ''; // Status workflow (v3.0.0): badge for any non-default status. The // roadmap view (v3.2.0) skips the badge - the column already // communicates the status, a badge would be redundant noise. var st = (f.status || 'open'); var labels = this.cfg.statusLabels || {}; var statusBadge = (!this.isRoadmap && st && st !== 'open' && labels[st]) ? '' + escapeHtml(labels\[st\]) + '' : ''; // Design themes (v3.1.0): arena / neon / pixel render extra per-item // furniture - rank number, avatar (image or initials), a "leader" pill // on rank 1, and a "votes" label. Classic/colour themes are untouched. var t = this.cfg.theme || 'light'; var themed = (t === 'arena' || t === 'neon' || t === 'pixel'); var rank = (typeof index === 'number') ? (index + 1) : 0; var rankHtml = '', leaderHtml = '', avatarHtml = '', votesLabel = ''; if (themed) { rankHtml = '' + pad2(rank) + ''; if (rank === 1) { leaderHtml = '' + escapeHtml(this.cfg.leaderLabel || 'Leader') + ''; } if (f.image_url) { avatarHtml = '![](https://www.yoozecase.com/'%20+%20escapeHtml(f.image_url)%20+%20')

'; } else { avatarHtml = '' + escapeHtml(initialsOf(f.title)) + '

'; } votesLabel = '' + escapeHtml(this.cfg.votesLabel || 'votes') + ''; } // Moderation overlay: subtle corner buttons, only when canModerate. // Buttons appear on hover/focus (styled in CSS). Even in customizer // preview we render them when canModerate is true so the moderator // can see what visitors with moderation rights will see. var modHtml = ''; if (this.cfg.canModerate) { var parts = []; var hideTitle = escapeHtml(this.t('hideTitle', 'Hide this feature')); var deleteTitle = escapeHtml(this.t('deleteTitle', 'Delete this feature')); if (this.cfg.allowSoftHide) { parts.push( '' ); } if (this.cfg.allowHardDelete) { parts.push( '' ); } if (parts.length || (this.cfg.statuses && this.cfg.statuses.length)) { var sParts = parts.slice(); if (this.cfg.statuses && this.cfg.statuses.length) { var opts = ''; for (var si = 0; si ' + escapeHtml(sl) + ''; } var setStatus = escapeHtml(this.t('setStatus', 'Set status')); sParts.push( '' ); } modHtml = '' + sParts.join('') + '

'; } } var mediaHtml = themed ? avatarHtml : thumbHtml; return ( '32. ' + leaderHtml + rankHtml + '' + mediaHtml + '' + '##### ' + escapeHtml(f.title) + '

    ' + statusBadge + descHtml + '

    ' + modHtml + '
' ); }; /** * Detects whether a JSON response looks like a CSRF/token rejection. * Two cases: * 1. com_ajax-level rejection (Joomla's auto-token-check on POST): top * level message contains the JINVALID_TOKEN_NOTICE text and HTTP * status is typically 403. * 2. Plugin-level rejection from requireToken(): payload.error === "Invalid token". */ Board.prototype.isTokenError = function (status, raw, payload) { if (status === 403) return true; var hay = ''; try { hay = JSON.stringify(raw || {}) + '|' + JSON.stringify(payload || {}); } catch (e) {} return /invalid (security )?token/i.test(hay); }; /** * Fetch a fresh CSRF token from the server. Updates this.cfg.csrfToken so * the next call (and any future calls in this Board's lifetime) use the * correct one. Cache-bust with a timestamp so no upstream CDN caches the * response. */ Board.prototype.fetchFreshToken = function () { var self = this; var url = this.cfg.ajaxUrl + '&task=token&_=' + Date.now(); return fetch(url, { method: 'GET', credentials: 'same-origin', headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache' } }) .then(function (r) { return r.json(); }) .then(function (data) { var p = self.unwrapAjax(data); if (p && p.ok === true && typeof p.token === 'string' && p.token.length >= 16) { self.cfg.csrfToken = p.token; return p.token; } throw new Error('No fresh token in response'); }); }; /** * Generic POST-with-token helper. Builds FormData from a plain object, * adds the current token, posts. On 403 / token-error response, fetches a * fresh token and retries exactly once. * * @param {string} task The task name (appended to URL and body) * @param {object} fields Key-value form fields (no token) * @return {Promise} Resolves to the unwrapped payload on success. \* Rejects with Error containing .reason on failure. \*/ Board.prototype.postWithToken = function (task, fields) { var self = this; function attempt(token) { var fd = new FormData(); fd.append('task', task); for (var k in fields) { if (Object.prototype.hasOwnProperty.call(fields, k)) { fd.append(k, fields\[k\]); } } fd.append(token, '1'); return fetch(self.cfg.ajaxUrl + '&task=' + encodeURIComponent(task), { method: 'POST', credentials: 'same-origin', body: fd, headers: { 'Accept': 'application/json' } }).then(function (r) { return r.json().then(function (data) { return { status: r.status, raw: data, payload: self.unwrapAjax(data) }; }); }); } return attempt(this.cfg.csrfToken).then(function (res) { // Success path if (res.payload && res.payload.ok === true) { return res.payload; } // Token rejection? Fetch fresh and retry once. if (self.isTokenError(res.status, res.raw, res.payload)) { return self.fetchFreshToken().then(function (fresh) { return attempt(fresh).then(function (res2) { if (res2.payload && res2.payload.ok === true) { return res2.payload; } var err = new Error( (res2.payload && res2.payload.error) || (res2.raw && res2.raw.message) || self.t('requestFailed', 'Request failed.') ); err.payload = res2.payload || null; throw err; }); }); } // Some other error - surface it var err = new Error( (res.payload && res.payload.error) || (res.raw && res.raw.message) || self.t('requestFailed', 'Request failed.') ); err.payload = res.payload || null; throw err; }); }; Board.prototype.handleSubmit = function () { var self = this; if (isInBuilder()) { this.showStatus(this.t('previewSubmit', 'Preview mode - submissions are disabled in the YOOtheme builder. Try this on the published page.'), 'info'); return; } if (this.cfg.isClosed) { this.showStatus(this.cfg.closedMessage || this.t('votingClosed', 'Voting is closed.'), 'info'); return; } var title = (this.elTitle && this.elTitle.value || '').trim(); var desc = (this.elDesc && this.elDesc.value || '').trim(); // Client-side validation: minimum title length. Server re-validates. var minLen = (this.cfg.minTitleLength | 0) || 2; if (title.length for hide/delete, remember parent + sibling for rollback. var li = btn.closest('li'); var parentEl = li ? li.parentNode : null; var nextEl = li ? li.nextSibling : null; if (li && (action === 'hide' || action === 'delete')) { li.remove(); } btn.disabled = true; var task = action === 'hide' ? 'moderate\_hide' : action === 'delete' ? 'moderate\_delete' : action === 'restore' ? 'moderate\_restore' : ''; if (!task) { btn.disabled = false; return; } this.postWithToken(task, { feature\_id: featureId }) .then(function (payload) { // Success - keep the UI as-is (already removed) and refresh // so vote counts, top box, and hidden-features panel reflect // the new state. self.refresh(); if (self.hiddenOpen) { self.hiddenLoaded = false; // force a refetch self.fetchHiddenFeatures(); } var msg = action === 'delete' ? self.t('deleted', 'Feature deleted.') : action === 'hide' ? self.t('hidden', 'Feature hidden.') : action === 'restore' ? self.t('restored', 'Feature restored.') : self.t('done', 'Done.'); self.showStatus(msg, 'success'); setTimeout(function () { self.hideStatus(); }, 3000); }) .catch(function (err) { // Rollback: re-insert the row if we removed it. if (li && parentEl) { if (nextEl) { parentEl.insertBefore(li, nextEl); } else { parentEl.appendChild(li); } } btn.disabled = false; self.showStatus(err && err.message ? err.message : self.t('modFailed', 'Moderation action failed.'), 'error'); }); }; Board.prototype.handleStatusChange = function (featureId, status, sel) { var self = this; if (!featureId || !status) return; sel.disabled = true; this.postWithToken('moderate\_status', { feature\_id: featureId, status: status }) .then(function () { self.refresh(); var label = (self.cfg.statusLabels && self.cfg.statusLabels\[status\]) ? self.cfg.statusLabels\[status\] : status; self.showStatus(self.tf('statusSet', 'Status: %s', label), 'success'); setTimeout(function () { self.hideStatus(); }, 3000); }) .catch(function (err) { sel.disabled = false; self.showStatus(err && err.message ? err.message : self.t('statusFailed', 'Could not set status.'), 'error'); }); }; /\*\* \* Toggle the hidden-features panel open/closed. First open triggers \* an AJAX fetch; subsequent toggles just hide/show the cached list. \*/ Board.prototype.toggleHiddenPanel = function () { if (!this.elHiddenList || !this.elHiddenLabel) return; var arrow = this.elHiddenToggle && this.elHiddenToggle.querySelector('.yc-fb-hidden-arrow'); if (this.hiddenOpen) { this.hiddenOpen = false; this.elHiddenList.hidden = true; this.elHiddenLabel.textContent = this.t('showHidden', 'Show hidden features'); if (arrow) arrow.classList.remove('yc-open'); return; } this.hiddenOpen = true; this.elHiddenList.hidden = false; this.elHiddenLabel.textContent = this.t('hideHidden', 'Hide hidden features'); if (arrow) arrow.classList.add('yc-open'); if (!this.hiddenLoaded) { this.fetchHiddenFeatures(); } }; /\*\* \* Fetch the hidden-features list via the moderator-only endpoint. \* Re-callable to refresh the list after a restore. \*/ Board.prototype.fetchHiddenFeatures = function () { var self = this; if (!this.elHiddenList) return; this.elHiddenList.innerHTML = '2. ' + escapeHtml(this.t('loading', 'Loading...')) + '
'; var url = this.cfg.ajaxUrl + '&task=moderate\_list\_hidden&board\_id=' + encodeURIComponent(this.cfg.boardId); fetch(url, { credentials: 'same-origin', headers: { 'Accept': 'application/json' } }) .then(function (r) { return r.json(); }) .then(function (data) { var payload = self.unwrapAjax(data); if (!payload || payload.ok !== true) { self.elHiddenList.innerHTML = '4. ' + escapeHtml((payload && payload.error) ? payload.error : self.t('hiddenLoadFailed', 'Failed to load hidden features.')) + '
'; return; } self.hiddenLoaded = true; var features = payload.features || \[\]; if (features.length === 0) { self.elHiddenList.innerHTML = '6. ' + escapeHtml(self.t('noHidden', 'No hidden features.')) + '
'; return; } var html = ''; for (var i = 0; i ' + escapeHtml(f.description).replace(/\\n/g, '
') + '' : ''; var restoreTitle = escapeHtml(self.t('restoreTitle', 'Restore this feature')); html += '9. ' + '' + '##### ' + escapeHtml(f.title) + '

    ' + descHtml + '' + escapeHtml(self.tf('votesMeta', '%s votes', f.votes | 0)) + '' + '

    ' + '' + '
'; } self.elHiddenList.innerHTML = html; }) .catch(function () { self.elHiddenList.innerHTML = '11. ' + escapeHtml(self.t('hiddenNetwork', 'Network error while loading hidden features.')) + '
'; }); }; function initAll(scope) { var roots = (scope || document).querySelectorAll('.yc-fb-root'); for (var i = 0; i ---

##  Changelog

---

 [ v1.4.0 Breaking 01/10/2026 ](#) Added- \*\*A link on every element except the games, the toggle and the divider.\*\* Crazy Box, Callout, Check, Circle, Cross, Highlight, Handwriting, Neon, Sticky Note, Quote, Sparkle and Underline can now be a call to action, like the eight elements that could already carry a link. The Link tab offers the YOOtheme link picker (URL, page, file, email address, phone number), Open in a new window, Nofollow, Noreferrer and an accessible name. A new window always gets `rel="noopener"`. On an element with visible text the name is optional; on a drawing it falls back to a short label, so a screen reader never meets an empty link.

- \*\*The link and its accessible name follow Dynamic Content\*\* on every element that has a Source field, so a doodle in a list can point at each item's own page.

 Changed- \*\*One shared link routine for all 20 elements\*\* (`lib/link.php`) replaces eight copies of the same code. It refuses `javascript:`, `vbscript:` and `data:` URLs, also with hidden spaces in front of the scheme, and never nests a link inside a link.
- \*\*Links keep the element's colour on hover\*\* instead of picking up the theme's link styling.
- `tools/test-link.php` renders all 26 elements with a link and checks the markup: one `&lt;a&gt;` with the right href, target, rel and aria-label, no nested links, no link on a refused scheme, no link where the element has no link field, and the five link fields present in the Link tab.

 Fixed- \*\*All settings are visible again in YOOtheme Pro 5.\*\* The settings panel of every element showed only its first block (Arrow, Image mask, Sticky Note and so on); the blocks with sketchiness, layout, animation, link and Custom CSS were there in the files but never reached the panel. YOOtheme Pro 5 reads one layout per element and renders the rest as tabs, which this plugin did not use. Every element now has the same four tabs: Content, Style, Link and Advanced (the games: Content, Style, Texts, Advanced). No field was renamed and saved layouts render exactly as before. Reported by a user who found the link controls in the plugin files but not in the builder.

 [ v1.3.0 Breaking 30/09/2026 ](#) Changed- \*\*A licence key works on the edition it was bought for.\*\* Crazy Highlights 'n Doodles comes in two editions, one per platform, and each is sold separately; the All My Access bundle covers both. The plugin now tells the licence server which edition it is. A key that belongs to the other edition shows "Other edition" in amber, with a link to both editions in the shop. An activation on the right edition carries on as before, so there is nothing to do.

- \*\*Clearer licence texts.\*\* The licence screen, the notice on WordPress and the expired message now say what a licence is for: updates and support are part of an active licence. They no longer suggest that the plugin stops receiving updates without a key, because it does not. The elements keep working either way.
- \*\*WordPress: the game and toggle scripts load only on pages that use them.\*\* Visitors of every other page no longer download games.js and toggle.js. Logged-in users still get both on every page, so a game dragged into the builder preview works straight away.
- \*\*Sticky note size sliders read 0 as auto.\*\* The labels now say "0 = auto", and the help text explains that the size preset decides until you drag a slider.
- \*\*Licence files.\*\* LICENSE.txt now carries the full text of the GNU General Public License version 2. The note about the product name and logos moved to NOTICE.txt.

 Fixed- \*\*Saving the plugin options no longer clears the activation.\*\* After activating, a plain Save on the options screen dropped the stored activation: the panel went back to Not activated while the site kept its place on the licence server. The activation now survives a Save. The WordPress licence screen was not affected.
- \*\*Ink colour is back on Crazy Box, Crazy Callout and Crazy Hand, and the fill style on Crazy Hand.\*\* The options still worked but had dropped out of the settings panel, so a light ink for a dark section could not be picked.
- \*\*WordPress: Check again finds a new version straight away.\*\* The plugin kept its own answer from the update server for twelve hours, so a manual check on Dashboard > Updates could still miss a release made that day. A manual check now asks the update server again.
- \*\*An answer from the licence server that this version does not know\*\* now reads "Something went wrong. Try again." instead of "Unknown action."

 [ v1.2.1 Breaking 23/09/2026 ](#) Changed- \*\*The licence is checked again every day on Joomla.\*\* A key that expired or was switched off kept showing Active. Now the status is refreshed at most once a day from the administrator, and a lapsed licence shows a notice with a renewal link on the dashboard, the plugin manager and the extension manager. The elements keep working whatever the status.

- \*\*WordPress: the daily licence check also runs after an update.\*\* Sites that updated from 1.0.0 never got the check scheduled.
- \*\*Deactivating while the licence server is unreachable changes nothing.\*\* Before, the site let go of the key locally while the activation stayed taken on the server. Try again when the server is back.
- \*\*Builder icons in the brand pink at the stock size\*\*, so they are easy to spot in the element picker.

 Fixed- \*\*Draw in works again in Chrome and Edge.\*\* Arrow, check, circle, cross, divider, heart, underline, sparkle, sticker, quote and speech stayed invisible when Draw in was chosen: the browser never started the animation. The draw-in now hooks on a class instead of an attribute and plays as it should. Speech bubbles with an oval or round outline now draw in too.
- \*\*Crazy Divider is visible again with the straight, dashed and dotted styles.\*\* At the default sketchiness these three rendered nothing, and a rainbow, sunset or ocean colour on a straight line did the same. Two dividers on one page no longer share their sketch settings.
- \*\*Dashed and dotted dividers fade in\*\* when Draw in is chosen, instead of turning into a solid line.
- \*\*Crazy Circle, Triple line with a gradient colour\*\* draws all three lines.
- \*\*Handwritten captions use the bundled Caveat font\*\* instead of whatever cursive the visitor's system had.
- \*\*Alignment holds with a caption or sparks.\*\* A centred or right-aligned arrow with a caption, and centred or right-aligned handwriting with sparks, no longer jump to the left. Hangman now follows Center and Right.
- \*\*Wipe in no longer cuts off captions and overflow\*\* once the wipe has finished.
- \*\*WordPress: the filter library in the page footer is closed properly.\*\* Anything another plugin printed in the footer after it could end up inside it and fail to load or show.

 [ v1.2.0 Breaking 09/08/2026 ](#) Added- \*\*Custom CSS on every element.\*\* A new Advanced tab with one field for your own rules. Write `&amp;` for the element itself, so `&amp; svg { opacity: .6 }` reaches the drawing. Plain declarations without braces apply to the element. `@media` works, so a mobile tweak stays in the element instead of ending up in the theme. The rules are scoped to that one instance, so a rule can never reach the rest of the page, and an element without custom CSS renders exactly as before, without an id in the markup. `@import`, script-like values and anything that is not a declaration are dropped rather than passed through.

- \*\*The licence key now travels with the update request.\*\* On Joomla it goes into the update site's `extra\_query`, the same mechanism other vendors use for their download key; on WordPress it goes in a request header, which keeps it out of access logs. Nothing changes today because the feed is still open. The point is that the day it starts checking keys, every activated site is already sending one and no new release is needed.

 Changed- `tools/test-render.php` renders all 26 elements under both platform guards and checks for PHP notices, empty output, unescaped payloads and correctly scoped custom CSS. This is the test that would have caught the bug that made every element render empty on WordPress in 0.47.0.
- `tools/test-css.php` covers the custom CSS field: scoping, `&amp;`, media queries, and the refusals (`@import`, `expression()`, `javascript:`, breaking out of the style block).

 Fixed- \*\*The licence panel now scrolls its answer into view\*\* on both platforms. On a long settings page the badge changed below the fold, so a click looked like it did nothing. Reduced-motion settings are respected: a jump instead of a glide.

 [ v1.1.0 Breaking 09/08/2026 ](#) Added- \*\*Licence screen on WordPress\*\*, at Settings > Crazy Highlights 'n Doodles, with a Licence link on the plugins row so it is one click away. Paste a key, activate this site, see the state, deactivate to free the slot for another site. It mirrors the Joomla panel: the same gateway, the same 24 hour cache, the same seven day grace window when the licence server cannot be reached, and the same rule that the elements keep rendering either way.

- The WordPress build now ships \*\*six languages\*\*. The licence screen is translated into Dutch, German, French, Spanish and Italian, generated from the Joomla language files so a sentence cannot drift between platforms.
- A daily background check refreshes the licence verdict. It only calls out when the cached verdict is older than a day, and it never overwrites a status because of a network problem.
- Removing the plugin now releases the licence, so the customer gets the activation slot back without having to ask support. Switching the plugin off deliberately does not: a plugin that is off for a moment keeps its slot.

 Changed- One cache-busting value for every asset (YC.14). `marker.css` and `anim.js` used Joomla's `auto`, which resolves to the site-wide media version: a single string shared by every extension on the site, so they carried a different suffix than `games.js` and `toggle.js` and neither said anything about this plugin. Both now use the plugin version, or the media folder timestamp when debug is on. The comment above those calls claimed this was already the case; it was not.

 [ v1.0.0 Breaking 08/08/2026 ](#) Added- \*\*WordPress update check.\*\* The plugin answers WordPress' update filter and reads `wp-update.json` from the update server, so WordPress installations now see and install updates. The feed tool generates that file alongside the Joomla feeds.

- `BUILDINFO.txt` build fingerprint (build id, timestamp, content checksum) in every package.

 Changed- Update-feed entries are tagged `stable` (they were `beta` during 0.x).
- The package README and this changelog are brought up to date; they had lagged behind at 0.46.1 while the packages moved on.

 Fixed- The WordPress `Update URI` header pointed to a slug without the "n" (`crazy-highlights-doodles`), a path that does not exist on the update server.

 [ v0.50.1 04/08/2026 ](#) Added- `tools/feed.py` generates the update-feed blocks from the built zip's sha256 file, so a hand-typed hash can no longer break the update check.

 Fixed- The install message and log lines reported an old version number: the `VERSION` constants in the bootstrap and the installer were not updated by the build. The build now rewrites every `VERSION` constant on both platforms (plus `Stable tag` in the WordPress readme) and rejects the build when any constant drifts from the release version.

 [ v0.50.0 04/08/2026 ](#) Added- \*\*Tone field (Playful/Refined) on all 26 elements.\*\* Refined switches off the sketch distortion and the per-view randomness, turns spill fills into filled, uses the theme font for captions and picks the blunt shape variant where one exists. The default is Playful, which is exactly the previous behaviour, so existing pages render unchanged.

- Two new Crazy Quote shapes: a disc with the quote mark cut out (the background shows through) and a blunt typographic mark next to the existing brush shape. Refined picks the blunt mark automatically.

 [ v0.49.0 04/08/2026 ](#) Added- Caption on nine more elements: check, heart, hand, sparkle, sticker, underline, circle, cross and doodle. Three fields on a Caption tab: the text, the side it sits on (below, above, right or left; left/right fall back to below on phones) and handwritten or theme font. Crazy Doodle inherits the behaviour of the shape it switches to.

 [ v0.48.2 04/08/2026 ](#) Security- The licence status task on the public `com\_ajax` endpoint skipped the permission check, so anyone could query the site's licence state from outside. All licence tasks now require `core.manage` on `com\_plugins`; the CSRF token remains on the write actions. Joomla only; WordPress has no licence layer yet.

## Schema

```json
{
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [
        {
            "@type": "ListItem",
            "position": 1,
            "name": "Home",
            "item": "https://www.yoozecase.com"
        },
        {
            "@type": "ListItem",
            "position": 2,
            "name": "Extensions",
            "item": "https://www.yoozecase.com/extensions"
        },
        {
            "@type": "ListItem",
            "position": 3,
            "name": "Crazy Highlights 'n Doodles",
            "item": "https://www.yoozecase.com/"
        }
    ]
}
```
