== Changelog ==

Full version history for SDAweb Social Galleri Feed. The most recent releases are also listed in readme.txt.

= 5.4.0 =
* Changed: pages load much faster. A page with a gallery used to carry the complete cached feed of every account — all 300 posts with every picture address and every album slide, about 1 MB on a page that shows 50 posts. The page now carries only what its galleries can show, with one extra "Load more" batch so the first click is still instant; later clicks fetch the next posts from the plugin's own REST route, and visitors who never click download nothing more. Accounts not used on the page are no longer written into it, and posts Instagram delivers without a picture never leave the server. A gallery with a hashtag, exclude or media-type filter still receives the whole feed, because it filters in the browser. On the test site the page went from 1.2 MB to well under half of that.
* Changed: the REST route /wp-json/sdawsoga/v1/feed accepts offset and per_page to page through a feed; without them it answers with the whole feed as before.
* Changed: the plugin's per-address rate limit (10 requests per 5 minutes) now applies only to requests that actually go to Instagram. It used to count every call to the plugin's REST route, cached answers included, so a visitor clicking Load more more than ten times in five minutes — or a classroom behind one shared address — was cut off with an error.
* Changed (developers): the `sdawsoga_cached_feed_data` filter now also runs on the page itself, where nearly every gallery view gets its posts; before, it ran only on the REST and AJAX routes, so a filter written after docs/hooks.md had no visible effect. The schema.org JSON-LD is now printed by PHP whenever the feed is on the page, so the `sdawsoga_schema_markup` filter fires too (same shape, once per page). docs/hooks.md gains a "Which hooks fire" section: eleven of the documented hooks live in renderer helpers the default browser-built gallery never calls, and the reference now says so and shows how to get the same result with `sdawsoga_cached_feed_data`.
* Added (developers): four new hooks. `sdawsoga_feed_item` filters one post at a time just before it leaves the server (page and REST route) — rewrite a picture address or caption, or return false to hide the post; `sdawsoga_feed_refreshed` fires after a fresh feed has been stored, the moment to purge a page cache; `sdawsoga_feed_need` changes what a gallery asks to have inlined; `sdawsoga_frontend_config` filters everything handed to the browser script. All documented in docs/hooks.md.
* Fixed: the gallery block's "Wide width", "Full width" and HTML anchor settings had no effect on the page — the block declared them but its output never carried the alignment class or the id. They work now. In the editor the block used to show "Loading gallery…" for ever, because the script that builds the gallery never runs inside the editor; it now shows a card with the block's settings instead, and the gallery appears on the published page as before.
* Fixed: in a theme that styles every picture inside a full-width block (The7 sets them to the full viewport width), a gallery block set to Wide or Full width showed each tile as a zoomed-in sliver of its picture, and the lightbox's profile picture became viewport-wide. The gallery's own picture sizes now take precedence over such theme rules.
* Fixed: the Stories viewer now behaves as a dialog for keyboard and screen-reader users. It is announced as “Instagram Stories”, focus moves to its Close button when it opens, Escape and the left/right arrow keys work wherever focus is, Tab stays inside the viewer, and closing it returns focus to the profile avatar. Before, the keys only worked after clicking inside the viewer, and Tab wandered through the gallery behind it. A story video that cannot load now moves on to the next story instead of stopping the viewer.
* Changed: the browser is told to connect early to the picture servers the feed actually uses. Instagram hands out regional addresses (for example scontent-arn2-1.cdninstagram.com), so the fixed address the plugin named before was never the one the pictures loaded from.
* Changed: the pictures in the first row of the gallery are no longer marked for lazy loading, so the browser fetches them at once instead of waiting until layout is done; pictures further down stay lazy.
* Fixed: the gallery's buttons (Load more, lightbox arrows and Close) are now type=“button”, so a gallery placed inside a form no longer submits it when they are clicked.
* Changed: readme and admin texts brought up to date — the Third-Party Services section now explains that visitors’ browsers load the pictures from Instagram’s servers, and that the token is also sent when a visitor opens a gallery with an empty cache; the Stories description no longer says videos play with sound (they are muted); a stale pointer to a removed “Tools” section is fixed; one outdated entry in the translation template is corrected.
* Fixed: the profile avatar in the gallery header lost its round frame. Since 5.1.0 a style rule meant to strip the browser's button look also removed the avatar's size, round shape and thin grey border, so the picture was shown bare and at its own size, and the Stories ring sat off-centre. The avatar is framed again, the per-size and per-screen sizes apply again, and the Instagram gradient shows only while there are active Stories, as described. The avatar button also gets its keyboard focus outline back.
* Fixed: the "Posts label", "Followers label" and "Profile picture" fields under Translations had no effect on the gallery header — it always said "posts" and "followers" in English, and the profile picture's text alternative was always "Profile picture". The header now uses the three fields, like the rest of the gallery already did. If you had filled them in, you will see the change right after the update.
* Fixed: when the cached feed had expired and the scheduled refresh was late (after a run of failed refreshes, or on sites where WP-Cron is switched off without a server cron), the page kept showing the backup copy of at most 50 posts and nothing ever asked Instagram for fresh posts. A page view that has to use the backup copy now starts a background refresh, as the gallery's own data requests already did.
* Fixed: in the lightbox, the up and down arrow keys now move to the previous and next post, as the hint for screen-reader users has always said. Left and right still step through an album's pictures.
* Fixed: the lightbox announced itself to screen readers with the "Post counter" word from Translations ("Post", "Innlegg") instead of its name, "Post viewer".
* Fixed: "Add Account" saved the token before testing it and reported success whatever the test said, so a mistyped or expired token was added with a green message, and the feed was first fetched by the first visitor to the page. The token is now tested first — a failing token is not saved and the reason is shown — and the feed is fetched and cached at once, as the setup guide has always said. The same applies when you change an account's token: a token that fails the test does not replace the working one. The message after adding or changing an account now names the Instagram account and the number of posts fetched, or says why the feed could not be fetched (for example a token without the instagram_manage_insights permission).
* Fixed: changing "Cache Duration" or "Max posts to sync" in Global settings had no effect until the next scheduled refresh, hours later, on every site that has accounts in the Accounts tab — the save cleared only the cache of the old single-account layout. Each account's cache is now cleared and refreshed in the background right after saving; visitors keep seeing the backup copy in the meantime.
* Fixed: the Instagram account details the plugin remembers next to a token (account ID and username) were handled the wrong way round when something went wrong. A network timeout threw them away (costing extra Instagram requests on the next refresh), while an Instagram error about the username — for example after the Instagram handle was renamed — kept them forever, so every refresh failed until an administrator clicked Test. Some older releases could also store the details with the username missing, which had the same effect. The plugin now keeps the details on a timeout, re-checks and retries once when Instagram rejects the username, and never stores details without a username. No action needed; this repairs itself on the next refresh.
* Fixed: a field left empty under Translations (or the Follow / Load more button text under Appearance) showed nothing at all — an emptied month name made the lightbox date read "15. ", an emptied button text left an icon-only button — although the tab says "Leave empty to use defaults". An empty field now shows the default. The default is now also translated: the English text that activation writes into every field no longer counts as your own text, so on a site in another language the plugin's own translation of these strings shows as soon as one is installed. Text you typed yourself is used exactly as before.
* Fixed: on screens 1024 px wide or narrower (tablets, phones, a small desktop window) the Tab key could leave the open lightbox and land on the page behind it, because the keyboard trap counted controls that are hidden at those sizes. Tab and Shift+Tab now stay inside the lightbox at every size.
* Fixed: the hashtag filters matched the start of longer hashtags — hashtag="jul" also showed posts tagged #juli and #julegave, and exclude_hashtags="ad" also hid #advent. A tag now has to match the whole hashtag. If you relied on the old partial matching, add the longer tags explicitly.
* Fixed: the settings page meant to warn before you leave with unsaved changes, but the warning never appeared in current browsers. It now does. It is only armed by the settings fields themselves, not by typing in the account manager or the Shortcode Builder, which are not saved with the form.

= 5.3.14 =
* Fixed: on sites with a page cache (LiteSpeed Cache, WP Super Cache, Cloudflare …) the gallery could stop loading for all logged-out visitors until the cache was purged. It happened when a page was cached while the Instagram posts were not yet built into it — right after adding an account, or while Instagram was unreachable: the security codes in the cached page expire after 12–24 hours, and WordPress then refused the gallery's own request. Logged-out visitors no longer send such a code — the feed is public, read-only data — so an old cached page keeps working. Nothing changes for logged-in users.
* Fixed: after an image failed to load, "Load more" showed one post twice.
* Fixed: a post that Instagram delivers without a picture address (it does that for some flagged or copyrighted content) made the browser request a non-existent page on your site on every page view, and showed as a broken image in the lightbox. Such posts, and such slides inside an album, are now left out.
* Fixed: when the gallery could not be loaded, the error message was written and then hidden again at once, so visitors and administrators saw an empty area. The message — the "error" text under Translations — is now shown, and announced to screen readers.
* The complete version history has moved to changelog.txt in the plugin folder; this section lists the most recent releases.

= 5.3.13 =
* Security hardening: two old copies of the Instagram access token are now removed. (1) Sites that upgraded from 4.x to 5.0 kept the original single-account token after it was copied to the new account; if an administrator later deleted every account, the plugin quietly started using that old token again. It is now deleted once the accounts have their own token, and when all accounts have been removed. (2) Sites that once ran a release older than 4.6 still had that release's token stored unencrypted under its old option name. It is now deleted.
* Fixed: deleting the plugin never removed the options of releases older than 4.6 (including that unencrypted token), because the clean-up checked a flag it had just deleted. They are now removed — but only when they are recognisably this plugin's own, so another plugin's settings that happen to share the short old prefix are never touched.
* Deleting the plugin now also removes the short-lived markers introduced in 5.3.12.
* No visible changes and no settings to review.

= 5.3.12 =
* Fix: limit="X" on the shortcode (and "Posts to show" in the block) had no effect — the per-device post counts from the settings always won. An explicit limit now sets a fixed count on every screen size, as the settings page always described. If your shortcode contains a limit, the gallery will show that number of posts after this update.
* Fix: columns="X" on the shortcode (and "Columns" in the block) had no effect. An explicit value now sets the column count on desktop and laptop screens and caps the smaller breakpoints, so a gallery never shows more columns on a tablet than on a desktop. If your shortcode contains columns, the layout will follow it after this update.
* Changed: in the block, "Posts to show" and "Columns" now default to 0 = "use the per-device values from the plugin settings". Existing blocks that were left at the old defaults (9 / 3) keep looking exactly as they do today.
* Fix: every apostrophe in a lightbox caption was corrupted — "It's" appeared as "It&", a link "#39" and ";s".
* Fix: hashtags and mentions with non-English letters were cut at the first such letter — "#søndag" became a link "#s" followed by plain "øndag". Hashtags and mentions are now fully Unicode-aware, email addresses are no longer turned into mentions, and links are URL-encoded.
* Fix: when an account's fetch kept failing (for example a token without the instagram_manage_insights permission), every page view by every visitor ran the full Instagram request sequence again — twice. A failed fetch is now remembered for five minutes; scheduled refreshes, "Refresh now" and "Clear cache" are unaffected.
* Fix: an unexpected response from Instagram without feed data was stored as an empty gallery and written over the backup copy. It is now treated as a failure, and an empty result never replaces a backup that contains posts.

= 5.3.11 =
* Fix: Connecting with a User token that manages several Facebook Pages could fail with "No Instagram Business Account found" when the Instagram account was linked to any Page other than the first one returned by Meta. The plugin now checks every Page in the list. Single-Page setups and Page tokens were never affected.
* Fix: Graph API error messages returned by the public feed endpoint are now shown only to logged-in administrators; visitors get a neutral "Could not fetch Instagram data" instead. Meta's raw messages can include app IDs and permission names, which have no business reaching the front end. The full message is still recorded in the plugin's recent-errors list for troubleshooting.
* Fix: Grid tiles whose image can no longer be loaded (post deleted on Instagram, copyright takedown, or an expired CDN link served from the backup cache) are now removed instead of leaving a broken-image box in the grid.
* Fix: The lightbox no longer shows "0 likes" for posts where the account owner has hidden like counts — Meta omits the field entirely in that case, so the like stat is hidden rather than shown as zero.
* Fix: The admin "Using backup data" cache status now reports the number of cached posts (it was reading the wrong key and always came back empty).
* Fix: The setup guide's example URLs and the rate-limit heading showed Graph API v22.0; they now read the live API version (currently v26.0).
* Fix: Guarded a PHP notice in the thumbnail helper for media where Meta omits `media_url` (copyrighted / flagged content).
* Performance: Removed the front-end preconnect to graph.facebook.com — the Graph API is called server-side only, so the browser never uses that connection — and dropped the `crossorigin` attribute on the image-CDN preconnect, which opened a second, unused connection.
* Compatibility: Tested up to WordPress 7.1.

= 5.3.10 =
* Tweak: Updated the Instagram Graph API version used for all requests from v22.0 to v26.0. This is a maintenance update to stay current with Meta's supported API versions — v22.0 was still valid (through May 2027), but moving to v26.0 now avoids a future forced migration. No fields, endpoints, or behaviour changed; verified against Meta's official changelogs for v23.0–v26.0 that nothing this plugin uses (business_discovery, media fields, Stories endpoint) was altered across those versions.

= 5.3.9 =
* Documentation: The setup guide, Help tab, and readme now correctly list `instagram_manage_insights` as a permission required for the feed (not only for Stories). The plugin reads your posts through the Graph API Business Discovery endpoint, which requires this scope; without it the feed fails to load with "(#10) Application does not have permission for this action". The troubleshooting entry for that error now points straight to the fix.

= 5.3.8 =
* New: The per-account "Clear Cache" button is now "Refresh now". It clears the cache and fetches your latest Instagram posts in one click, and shows a clear confirmation ("Cache cleared and N latest posts fetched from Instagram") instead of silently reloading the page. The button can no longer get stuck in a disabled state.
* Improved: Rewrote the in-plugin setup guide for obtaining a permanent Page Access Token so it works for Pages managed through Meta Business Suite / Business Portfolios. The old "/me/accounts" step returns an empty list for those Pages; the guide now fetches the token directly from the Page by its ID. Also added a second way to extend the token (the Access Token Debugger's "Extend Access Token" button), steps for finding your Page ID, and clearer verification notes.

= 5.3.7 =
* New: Active Instagram Stories now show a subtly pulsing gradient ring on the profile avatar, so visitors notice there are Stories to watch. The pulse respects the "reduce motion" accessibility preference.
* New: An occasional, dismissible note on the plugin's settings page invites happy users to leave a review. It appears only after a feed has been set up and the plugin has been in use for about two weeks, and never returns once dismissed.
* Improved: Moved the "Show Stories" toggle from the Behavior tab to the Appearance tab, directly under the Header/Avatar settings. The story ring renders on the profile avatar, so this is where users look for it — matching how other Instagram feed plugins group it. The setting name is unchanged, so existing configurations are unaffected.
* Fix: In the admin settings, the dropdown arrow on several select fields overlapped the longest option text. The field's right padding was too small to clear the arrow; selects now reserve enough room.
* Changed: Video Stories now play muted by default in the Story viewer. This also makes autoplay more reliable, since browsers commonly block autoplay that includes sound.
* Fix: The cache status ("N posts cached, refreshes in …") now shows the correct remaining time on sites using an external object cache such as Redis or Memcached, where it could previously appear blank.
* Fix: Resolved a "translation loading was triggered too early" notice on WordPress 6.7+ (seen with WP_DEBUG on translated sites). Default strings are no longer translated at load time — translation now happens at display time, the correct warning-free approach. Strings remain fully translatable and your gallery looks identical.
* Cleanup: Removed two duplicate "Follow button" / "Load more button" fields from the Translations tab. Those labels are set under Appearance (Follow Button / Load More Button); the Translations copies were never applied on the front end. No other translatable strings changed.
* Cleanup: Removed a few internal settings that were defined but never used (image-quality and lazy-load-style placeholders, and an inert "username" shortcode attribute). No documented feature is affected.
* Hardening: The JSON-LD schema output (SocialMediaPosting) now escapes `<` and `>` as Unicode sequences via `JSON_HEX_TAG`, so a `</script>` sequence can never close the schema's `<script>` block early. The injected values were already sanitized, so this is defense-in-depth — there is no user-visible change.

= 5.3.6 =
* Compatibility: Tested with WordPress 7.0.

= 5.3.5 =
* Improvement: The lightbox now uses the full viewport width on mobile and tablet (≤ 1024px). Previously the modal sat at ~90% width with a 20px margin on phones, leaving 5–10% of black gutter on each side of the image. The modal is now full-bleed on small screens, giving the photo the maximum possible room.
* Compatibility: Uses the dynamic viewport unit `100dvh` (with a `100vh` fallback) so the modal no longer leaves a gap under the iOS Safari address bar. The image-area cap rose from `60vh` to `65vh` to take advantage of the extra real estate. No visual change on desktops above 1024px.
* Improvement: Landscape phones — the on-image carousel arrows ( < > ) are hidden in this orientation, since they overlap the photo when vertical room is scarce and swipe already handles slide navigation. The carousel dots and post-counter pill stay visible so users still see their position. The post-nav pill is also slightly compacted (smaller padding, smaller buttons) so it consumes less vertical space.
* Accessibility: Admin settings tabs now implement the WAI-ARIA tabs keyboard pattern. Arrow Left/Right moves focus between tabs, Home/End jumps to first/last, and a roving `tabindex` (`0` on active, `-1` on inactive) lets screen reader users navigate the tablist exactly as the semantic markup advertises.
* Compatibility: Preconnect hints to `scontent.cdninstagram.com` and `graph.facebook.com` now also fire on pages that contain the Gutenberg block (not only the shortcode). Block-only pages now save the same ~50–150ms on first thumbnail load.
* Cleanup: Synced the Gutenberg block's `block.json` version, the editor-script asset fallback, and the translation template (`.pot`) header version with the plugin header. Tightened a few admin `$_POST`/`$_GET` reads to use `wp_unslash()` + `sanitize_*` for Plugin Check codesniffer cleanliness. No user-visible behavior change from these.

= 5.3.4 =
* Fix: The mobile post-navigation pill ("Innlegg 1 / 20") was positioned inside the image area and overlapped the bottom portion of the photo. It now pins to the modal's dark backdrop below the content panel, clear of the image and the caption. Uses `env(safe-area-inset-bottom)` so it stays above the iOS home indicator.

= 5.3.3 =
* Fix: In the Highlight layout on a single-column responsive breakpoint (typically `Columns › Mobile Portrait = 1`), every other post became invisible because the featured first item's `grid-column: span 2` caused CSS Grid to create an implicit second column sized to 0px. Auto-placed items then alternated between the visible 1fr column and the invisible 0px column. The first-item span is now clamped to the available column count via `min(2, var(--sdawsoga-active-cols))`, so single-column layouts stack cleanly with no missing posts. No effect on 2+ column layouts — the featured first-item treatment is preserved everywhere it visually makes sense.

= 5.3.2 =
* Cleanup: Synced the Gutenberg block's `block.json` version field and the editor-script asset fallback with the plugin header. Source files now match the deployed version directly instead of relying on the build pipeline's regex-sync at zip time. No user-visible change — the block editor's runtime cache-buster already used the live plugin version via the `SDAWSOGA_VERSION` constant.

= 5.3.1 =
* Fix: Undefined variable `$month_labels` in admin settings "Month Names" section. The month label header now correctly reads from `$month_defaults`. On PHP 8+ this previously emitted PHP warnings; functionally the field labels were empty.
* Compatibility: Verified compatibility with PHP 8.5.

= 5.3.0 =
* Developers: Formally documented 21 existing WordPress filter and action hooks as a stable public API. See docs/hooks.md inside the plugin folder for full reference with copy-paste examples. Hooks cover rendering (sdawsoga_shortcode_attributes, sdawsoga_gallery_template_path, sdawsoga_gallery_output, sdawsoga_thumbnail_url, sdawsoga_permalink, sdawsoga_formatted_timestamp, sdawsoga_caption_before_process, sdawsoga_caption_after_process, sdawsoga_is_carousel, sdawsoga_media_type_label, sdawsoga_error_message_html), post filtering (sdawsoga_filtered_posts, sdawsoga_hashtag_filtered_posts, sdawsoga_exclude_hashtags_filtered_posts), API and cached data (sdawsoga_cached_feed_data, sdawsoga_fresh_feed_data, sdawsoga_after_api_fetch, sdawsoga_stories_data), SEO schema (sdawsoga_schema_markup), and lifecycle events (sdawsoga_before_api_fetch, sdawsoga_cache_cleared).
* Developers: All documented hooks are now stable within the 5.x major version — they will not be removed or renamed without going through the standard _deprecated_hook() cycle.
* Documentation: New FAQ entry "Can I extend the plugin from my theme or another plugin?" pointing developers at the hooks documentation.
* No user-visible changes — if you don't write code against the new public hooks API, the plugin behaves identically to 5.2.4.

= 5.2.4 =
* Performance: Gallery item images now have explicit width and height attributes parsed from the Instagram CDN URL pattern (/p640x640/, /s320x320/, etc.). Eliminates Cumulative Layout Shift on masonry and original-aspect-ratio layouts.
* Reliability: Pagination cursor sanitization no longer strips period characters. Meta has historically used several base64 variants in pagination cursors; the previous regex was overly aggressive and could silently break pagination if Meta returned a cursor containing a "."
* Hygiene: When the Stories feature is toggled off, the plugin now immediately clears any pending Stories cron events for all accounts. Previously they kept consuming Graph API quota until each one fired and saw that stories were disabled.
* i18n: The "Loading gallery..." text in the initial template now respects the admin-customizable label from Components > Translations, matching how the rest of the frontend strings work.
* Documentation: Admin setup guide and readme FAQ now document the additional `instagram_manage_insights` permission required if you enable Stories.

= 5.2.3 =
* Accessibility: Lightbox modal now declares dialog semantics statically (role="dialog", aria-modal, aria-label, aria-describedby) so accessibility audits and screen-reader navigation see them on initial render, not only after open
* Accessibility: Added a screen-reader hint inside the lightbox describing the keyboard shortcuts (Escape, arrow keys, swipe-up to close)
* Accessibility: Broadened prefers-reduced-motion coverage from a few elements to a blanket plugin-scoped pattern using the 0.01ms idiom (preserves animationend events for any code waiting on them)
* Accessibility: Expanded :focus-visible outline coverage to include post navigation arrows, dot indicators, and Story viewer controls
* i18n: The block editor script now loads its translations via wp_set_script_translations() so the inserter labels and inspector controls can be localized
* Privacy: Privacy policy content now also discloses that profile pictures, post images, and video thumbnails are loaded directly from Instagram's CDN (scontent.cdninstagram.com), exposing visitor IPs to Meta on every page render
* Improved: SDAWSOGA_VERSION constant now reads dynamically from the plugin header via get_file_data() — single source of truth, eliminates version drift
* Improved: Build script now auto-syncs block.json and block-editor.asset.php version metadata from the plugin header at build time
* Fixed: block-editor.asset.php fallback version updated from a stale 4.10.1 to current
* Cleanup: Plugin header author corrected — "Rune Stake Stavdal" (the original spelling, restored after a previous propagated error in a sibling plugin's checklist template)

= 5.2.2 =
* Improved: Full internationalization audit — all 36 frontend label defaults now wrapped in translation functions
* Improved: All admin form labels and placeholders in the Translations tab are now properly translatable
* Improved: Replaced hardcoded Norwegian placeholder text with translatable English defaults
* Improved: Created complete POT file (597 entries) — was previously missing entirely
* Improved: Added block.json title and description to POT for block editor translation
* Fixed: Added translator comments for all strings containing %d placeholders

= 5.2.1 =
* Fixed: Gallery images no longer flicker or fade-in again when scrolling up and down.
* Performance: Removed content-visibility that caused re-paint flicker on scroll.
* Performance: GPU compositing layers (will-change) now only activate on hover instead of permanently.
* Performance: Removed duplicate hover transform on images to reduce compositing work.
* Changed: Removed grey placeholder background from grid items for a cleaner loading appearance.

= 5.2.0 =
* Added: Permanent Page Token support — tokens that never expire now work alongside 60-day User Tokens.
* Added: Setup guide updated with step-by-step instructions for obtaining a non-expiring token.
* Added: Copy-to-clipboard toast notification replaces inline button text swap in Shortcode Builder.
* Changed: All default strings now in English (base language). Existing sites with saved translations are unaffected.
* Changed: Images load with a fast 200ms opacity fade from placeholder background instead of slow staggered animation.
* Fixed: Account deletion now correctly detects and reassigns the default account.
* Fixed: Avatar renders as a plain element (not a button) when Stories are disabled.
* Fixed: Custom bio HTML no longer rendered as plain text on the frontend.
* Fixed: Double clipboard toast no longer fires when copying shortcodes.
* Fixed: Enter key in account fields triggers Add Account instead of submitting the settings form.
* Fixed: Keyboard listeners cleaned up when gallery is removed from DOM during open modal.
* Fixed: Double-escaped gallery ID in template output.
* Fixed: Uninstall now removes all plugin options including Stories and filter settings.
* Fixed: CSS aspect-ratio fallback added for Safari 14 and older.
* Improved: Block editor script now runs in strict mode.
* Improved: Render method docblocks document escaped HTML return contract.

= 5.1.1 =
* Fixed: Images no longer flicker or show blank placeholders when scrolling back to the feed on mobile.

= 5.1.0 =
* Added: Instagram Stories integration — active Stories display as a gradient ring around the profile avatar.
* Added: Fullscreen Story viewer with progress bars, auto-advance (5s for images, video duration for videos), tap-to-navigate, and keyboard support.
* Added: Per-account Stories cron refresh every 15 minutes (only when Stories feature is enabled).
* Added: "Show Stories" toggle in Behavior tab (default: off).
* Added: `show_stories` shortcode attribute for per-feed control.
* Added: Stories data injected inline per account for zero-AJAX display.
* Added: Video Stories play automatically with sound in the viewer.
* Note: Stories expire after 24 hours (Instagram API limitation). Only available for accounts you own a token for.

= 5.0.0 =
* Added: Multi-account support — connect up to 20 Instagram accounts with individual tokens, caches, and cron schedules.
* Added: Accounts admin tab — add, edit, delete, test connection, and clear cache per account.
* Added: `account` shortcode attribute and Gutenberg block attribute for selecting which account to display.
* Added: Per-account cache keys, stale backups, and failure counters — accounts don't interfere with each other.
* Added: REST API `?account=` parameter with sanitize_key validation.
* Added: Shortcode Builder redesigned with prominent account selector header showing username and post count.
* Added: Auto-migration from single-account to multi-account storage on upgrade.
* Changed: Admin restructured from 9 tabs to 7 — Header, Buttons, and Lightbox merged into Appearance; API renamed to Global (token management moved to Accounts); Advanced renamed to Behavior.
* Changed: Gallery Name (formerly Feed ID) now has clearer labeling and description.
* Changed: Plugin overview, setup guide, and help documentation updated for multi-account workflow.
* Improved: Per-account WP-Cron scheduling with proper args-based event management.
* Improved: Uninstall cleanup handles all per-account options, transients, and cron events.

= 4.12.0 =
* Improved: Lightbox backdrop changed to solid black for full immersion.
* Improved: Grid item hover effect upgraded — snappier scale (1.03), GPU-accelerated with will-change.
* Improved: Hover overlay now slides up with cubic-bezier easing for a premium feel.
* Improved: Grid images fade in smoothly on load instead of popping in.
* Improved: Close button hover uses scale instead of rotate for a modern look.
* Improved: All modal close paths (button, backdrop click, Escape, swipe) now animate consistently.
* Improved: Swipe detection uses velocity + lower threshold (35px) for more responsive touch navigation.
* Improved: Swipe-up-to-close also uses velocity detection for natural feel.
* Improved: Swipe hint shows for 1.5s (was 3s) and repositioned above mobile nav buttons.
* Improved: Focus rings use :focus-visible with Instagram blue (#0095f6) for better keyboard accessibility.
* Improved: Next/previous post images preloaded in lightbox for instant navigation.

= 4.11.0 =
* Added: Exponential backoff on API failures — retry interval doubles per consecutive failure (capped at 24h), resuming normal schedule on success.
* Fix: Consecutive failure counter resets when admin changes the API token, so backoff doesn't carry over to a new token.

= 4.10.5 =
* Fix: Consistent cron cleanup — uninstall.php and clear_all() now use wp_clear_scheduled_hook() matching deactivation behavior.

= 4.10.4 =
* Fix: Hashtag filtering now uses word-boundary matching — #pizza no longer matches #pizzanight.
* Fix: Instagram API pagination cursor no longer corrupted by over-aggressive sanitization on large feeds.
* Fix: Template path filter validated against plugin/theme directories to prevent arbitrary file inclusion.
* Fix: IP rate limiting uses salted hash (wp_hash) instead of plain MD5.
* Fix: Token encryption uses random_bytes() instead of deprecated openssl_random_pseudo_bytes().
* Fix: Schema.org JSON-LD data sanitized with esc_url_raw() and sanitize_user().
* Fix: Token mask check uses strict pattern to prevent edge-case save bypass.
* Fix: Plugin deactivation uses wp_clear_scheduled_hook() for reliable cron cleanup.
* Fix: Token decryption failures now logged when WP_DEBUG is enabled; base64 decode uses strict mode.
* Accessibility: Admin settings tabs have proper ARIA roles (tablist, tab, tabpanel).
* Accessibility: Gallery grid container has aria-live="polite" for screen reader announcements on load-more.
* Code quality: Duplicate hex color regex extracted to constants in admin script.
* Code quality: Feed ID attribute capped at 50 characters.

= 4.10.3 =
* Fix: SVG icons in lightbox (swipe hint, close button, nav arrows, media type badge) could render at wrong size due to CSS specificity conflict with the modal SVG reset. All SVG rules now use scoped selectors to prevent theme or reset overrides.

= 4.10.2 =
* Fix: Removed legacy unprefixed tg_gallery shortcode alias for WordPress.org compliance.
* Improved: Block editor UI strings internationalized with wp.i18n for translation support.
* Added: Tested up to header in main plugin file.

= 4.10.1 =
* Fix: Re-tagged release to ensure update delivery to all users.

= 4.10.0 =
* Performance: Grid items use DocumentFragment for batched DOM appends (single reflow instead of per-item).
* Performance: Hover overlay DOM created lazily on first mouseenter instead of at render time (~12 fewer nodes per item).
* Performance: CSS content-visibility: auto on grid and highlight layout items skips rendering for off-screen posts.
* Performance: Images use decoding="async" for off-thread decode in grid and lightbox.

= 4.9.0 =
* Added: Gutenberg block — insert the gallery from the block editor with visual settings in the sidebar.
* Added: sdawsoga_max_sync_posts to default options registry (was missing from activation and migration).
* Improved: Stale feed backup now truncated to 50 posts to prevent wp_options table bloat.
* Improved: REST API fetch includes X-WP-Nonce header for authenticated request context.
* Improved: Resize event listener cleanup via MutationObserver prevents memory leaks in SPA environments.
* Fixed: Legacy tg_ prefix options are now cleaned up on plugin uninstall.

= 4.8.0 =
* Post navigation arrows in lightbox backdrop — jump between posts on desktop, hidden on mobile where bottom nav bar handles it.
* Shortcode Builder admin tab — visually configure feeds and copy generated shortcodes with live preview.
* Clickable hashtags and @mentions in lightbox captions — matches PHP-rendered hover overlay.
* Enable/disable lightbox setting now wired to frontend — when disabled, grid images link directly to Instagram.
* Schema.org JSON-LD markup injected when feed loads (SocialMediaPosting with author data).
* Admin quick-links (Overview, Setup Guide) now switch to API tab first when clicked from other tabs.
* Updated Overview panel with post navigation docs and expanded shortcode reference table.
* Renamed legacy _tg references, removed duplicate variable declarations, cleaned up file header versions.

= 4.7.8 =
* Clear Cache now immediately re-fetches feed data — works reliably when WP-Cron is disabled (DISABLE_WP_CRON).
* Reduced API page size from 100 to 33 posts per request — prevents "reduce data" errors on accounts with carousel/children data.
* Clear Cache shows actual API error message on failure instead of generic text.
* Improved "Posts to show" description — clarifies Load More batch behavior and sync relationship.

= 4.7.5 =
* Responsive "Posts to show" — 6-breakpoint system matching responsive columns (desktop, laptop, tablet landscape, tablet portrait, mobile landscape, mobile portrait).
* Debounced resize handler recalculates visible posts on viewport change, preserving Load More state.
* Access token masked in admin HTML source (credential security hardening).
* Unsaved changes warning (beforeunload) in admin settings.
* Fixed stale [TG Admin] prefix in debug log.
* Replaced trademark dashicon (dashicons-instagram) with generic icon.
* Corrected author header format to match WordPress.org standard.

= 4.7.3 =
* Max posts to sync increased from 50 to 300 with automatic API pagination.
* Improved admin description for sync setting.

= 4.7.2 =
* Admin UI redesign: quick-access pill bar, tabbed Plugin Overview card (Layouts, Lightbox, Content, Performance, Security, Shortcodes), collapsible info cards, info tooltips on technical terms, shortcode reference table with parameter docs
* Fixed unprefixed variables in uninstall.php
* Added missing contributor to readme.txt

= 4.7.1 =
* Removed Plugin URI header to comply with WordPress.org guidelines (was identical to Author URI)

= 4.7.0 =
* Added: Video badge on grid items — shows a "Video" badge on VIDEO posts (toggle in Advanced > Carousel & Video)
* Added: Keyboard support for carousel dots — dots are focusable with Tab, activatable with Enter/Space
* Added: Default hashtag and exclude filters — set global defaults in Layout tab, shortcode attributes override
* Added: Cache refresh failure counter — red inline count shown in API tab cache status when consecutive failures > 0

= 4.6.0 =
* Added: Content moderation — blacklist words filter hides posts with blocked words/phrases
* Added: Video autoplay on hover — muted video preview plays when hovering over video posts in the grid
* Added: Carousel autoplay in lightbox — auto-advance slides with configurable interval and progress bar
* Added: Persistent API failure notice — warning shown on settings page when feed refresh fails 3+ times consecutively

= 4.5.4 =
* Fixed missing JS, template, and include files that were absent from the 4.5.3 SVN tag
* Whitespace and trailing-space cleanup across CSS, PHP, and JS files

= 4.5.3 =
* Renamed all frontend CSS classes and variables from generic tg- prefix to unique sdawsoga- prefix for better namespace compliance

= 4.5.2 =
* Fixed fatal error: wp_salt() called before pluggable.php is loaded
* Deferred cron scheduling to init hook for compatibility with all hosting environments

= 4.5.1 =
* Removed custom CSS editor per WordPress.org guidelines
* Fixed inline CSS escaping with wp_strip_all_tags()
* Fixed main plugin filename to match plugin slug
* Fixed settings link URL on Plugins page

= 4.5.0 =
* Security: Constant definition guards, CSS sanitizer hardened, IP validation, error code sanitization
* Fixed: XSS — lightbox and caption rendering switched to DOM element creation
* Added: 6-breakpoint responsive column cascade via CSS variables
* Improved: Conditional asset loading, smart cache clearing

= 4.4.0 =
* Max posts to sync setting, translatable post counter, improved cache status and mobile follow button

= 4.3.1 =
* WordPress.org trademark compliance: renamed plugin, added directory protection and license

= 4.3.0 =
* Inline JSON feed loading, REST API endpoint, stale-while-revalidate caching, WP-Cron background refresh

= 4.2.0 =
* Lightbox engagement stats, translations tab, swipe-up-to-close, security hardening (Bearer auth, CSRF, XSS)

= 4.1.0 =
* Initial release with grid/masonry/highlight layouts, lightbox, profile header, responsive columns
