# Changelog

All notable changes to the MentorKit LMS plugin will be documented in this file.

## [Unreleased]

## [1.7.0] - 2026-06-05
### Added
- **Two freemium release channels.** Release packaging now builds a WordPress.org-clean freemium zip (`mentorkit-lms-pro-freemium`) and a temporary Kernl freemium zip (`mentorkit-lms-pro-freemium-kernl`) from the same source. The wp.org artifact strips custom update metadata, Plugin Update Checker, the Kernl URL, and the updater module; the Kernl artifact injects `Update URI`, includes Plugin Update Checker, and registers against the temporary Kernl manifest.

## [1.6.1] - 2026-06-05

### Fixed

- **Webinar (and other link-delivered course types) no longer show "Content Unavailable" / "no SCORM content" to non-enrolled visitors.** A webinar carries no SCORM package by design, but the grid card, the single-course CTA resolver, and the single-course hero warning all treated missing SCORM content as a dead end — so a logged-out visitor saw "Content Unavailable" (card) and "This course does not have SCORM content yet." (single page) instead of the normal enrol/login CTA, making the course impossible to enrol in. All three now skip the no-content dead end when the course type provides its own learner delivery (detected via the handler's get_learner_cta()): non-enrolled visitors get the normal enrol/login/buy CTA, enrolled learners get "Til webinaret". Follow-up to the 1.6.0 webinar CTA. (TSK-18790)

## [1.6.0] - 2026-06-05

### Added

- **Channel conflict guard and wp.org handoff bridge.** The freemium bootstrap now blocks the official wp.org and temporary Kernl builds from running together before normal plugin bootstrap can load duplicate symbols. A bridge-mode Kernl release can install the canonical wp.org package, deactivate the temporary build, and redirect admins to activate the wp.org build while keeping shared plugin data intact.
- **Setup wizard now blocks Creator connection on Plain permalinks.** Sites using WordPress Plain permalinks get a new `Permalinks` prerequisite step between account creation and Creator connection. The step explains why Creator needs pretty REST URLs, offers an explicit nonce-protected action to switch WordPress to the Post name structure, and links to WordPress Permalink Settings for admins who want another non-plain structure. Direct `connect` step requests are routed back to the prerequisite until permalinks are compatible, with notices for success, already-compatible state, and missing `manage_options` permission.
- **Webinar courses surface their meeting link to enrolled learners.** A webinar is a normal course learners enrol in; the external registration / Teams link (`_webinar_url`) is the post-enrolment payoff. `Scorm_Course_Type_Webinar_Handler::get_learner_cta()` now provides a learner CTA, consulted by `scorm_player_course_get_cta()` (after the sequential-lock check, before the no-content check, via `method_exists()` so it stays additive — no interface change): a user with access and a webinar link set sees a **"Til webinaret"** primary CTA opening the meeting in a new tab, instead of the dead "Innhold ikke tilgjengelig" a content-less webinar would otherwise show. Non-enrolled visitors keep the normal enrol/purchase CTA; a webinar with no link behaves like any course. Gated behind `scorm_course_types_enabled`. (TSK-18790)
- **The Setup wizard menu item can now be hidden.** A new **Show the Setup wizard in the admin menu** toggle under **Settings → Setup Tools** controls the `Oppsett` (Setup) menu entry. By default ("auto") the item appears during onboarding and **auto-hides once the wizard is completed or skipped**, so a finished site is no longer cluttered by it; turning the toggle on pins it permanently, off hides it for good. The wizard page itself stays routable (the "Open Setup Wizard" button still works) — only the menu entry is conditional. New helper `scorm_player_should_show_setup_menu()` derives visibility from the explicit setting, falling back to the existing wizard `status`. nb_NO translations included.
- **Course type shown on the frontend course listings.** When the course-types feature is enabled (`scorm_course_types_enabled`), a discreet grey pill marks the course format on the three frontend course surfaces: the `[scorm_courses_grid]` cards (above the title), the group/module landing rows (in the module meta line, via the `scorm_group_module_meta` filter — sub-group module rows carry no type and are skipped), and the `[scorm_my_profile]` "my courses" rows. Only the non-default formats are badged — the standard **Digital** type is left unmarked to avoid cluttering ordinary e-learning courses — so Fysisk / Webinar / custom types stand out. The pill carries a slug class (`type-webinar`, …) and reads its colours from `--scorm-course-type-badge-bg` / `--scorm-course-type-badge-color` so themes can restyle it. New helper `scorm_course_types_get_badge_html()`. (TSK-18817)
- **Course-type fields and admin list column.** The course-type meta box now exposes type-specific fields: selecting **Webinar** reveals a "Webinar link" URL field (stored on the `scorm_package` as `_webinar_url`, for the meeting or a webinar guidance page), and **Fysisk** now adds an "Address" textarea (`_scorm_physical_address`) next to the existing external signup URL. The `scorm_package` admin list gains a **"Course type" (Kurstype)** column showing the assigned type, with the webinar link / physical address surfaced as a hint under it. All additive and gated behind the existing `scorm_course_types_enabled` toggle; switching type leaves previously-entered values untouched. (TSK-18817)

### Changed

- **Course & group admin lists are easier to scan.** On the group (`scorm_group`) list, the "Courses"/modules column is now collapsed by default — a native `<details>` disclosure showing just the count (e.g. "5 kurs") that expands the full module/course list on click — so rows stay short even when many modules are attached. The B2B "Status" and "Buyer" (Innkjøper) columns are now hidden by default on the group list (still re-enableable per user via Screen Options; only customer-group sites need them). Both the course (`scorm_package`) and group lists now give the Title column more room and keep the narrow info columns compact via list-table CSS. (TSK-18817)

### Removed

- **The "Manual Enrollments & Certificates" admin page (`mentorkit-enrollments`) is removed from the menu.** The standalone page is no longer registered as a submenu item and its legacy redirect (`scorm-player-enrollments`) is dropped, so the page is gone from the LMS Pro menu and is no longer routable. The shared enrollment-data helpers it defined (`scorm_player_enrollments_get_enrollment_map()`, `…_get_group_enrolled_user_ids()`) stay in place — they back the CSV report exporter — but the page UI, its bulk enrollment/certificate forms, and per-row actions are gone. Enrolment is handled via the group/course editors and the bulk tools elsewhere.

### Fixed

- **Block-editor Embed block now works for restricted LMS editor roles.** The Gutenberg Embed block (YouTube/Vimeo/Spotify) resolves embeds through the `/wp-json/oembed/1.0/proxy` REST route, which WordPress core gates behind the core `edit_posts` capability. The custom `mentorkit_lms_manager` and `mentorkit_course_creator` roles can edit courses, groups and pages but intentionally lack `edit_posts` (to keep the Posts admin menu hidden), so every embed failed in the editor with "Sorry, this content could not be embedded" (kurs.bondelaget.no — teaser videos on courses). A new `user_has_cap` filter (`scorm_player_grant_oembed_proxy_cap`) grants `edit_posts` virtually — scoped to the oEmbed proxy request only — to users who can already edit courses/groups/pages, so the Embed block resolves embeds without exposing the Posts admin menu. (TSK-18835)
- **Certificate admin meta box no longer emits an `Undefined array key "score"` warning.** The "Issued certificates" table on the certificate-template editor (`scorm_player_certificate_issued_callback`) reads each record straight from stored user meta via `scorm_player_get_certificates_by_template()`. Legacy certificate records saved before the `score` / `awarded_date` keys existed lacked those keys, so rendering the Score column (and the awarded-date `usort`) raised a PHP warning that surfaced on production (kurs.bondelaget.no, Sentry SCORM-PLAYER-34). The builder now normalizes each record with `wp_parse_args()` defaults (`score => 0`, `awarded_date => ''`) so all consumers see a complete shape. Display-only; no data is rewritten. (TSK-18828)
- **Course/group list-table columns no longer collapse into vertical single-letter text.** The `scorm_package` / `scorm_group` list CSS pinned many columns to fixed percentages (title 26 %, several at 9 %, etc.). Because WordPress renders these tables with `table-layout: fixed`, those fixed widths summed to ~93 % and **starved any column the rules didn't account for** down to near-zero width — so its header and content wrapped one character per line. On a plain site that was the custom "Course type" (Kurstype) column; on a WooCommerce site it was the WooCommerce / price columns. Replaced the fragile percentage grid with a single `width` on the title column only (plus `min-width` fallbacks): in fixed-layout the remaining columns then split the leftover width equally, so no column is ever starved regardless of which columns a site adds (WooCommerce, Polylang translation, certificates, …). (TSK-18817)
- **The Manual completion admin page (`mentorkit-manual-completion`) is now fully translated to Norwegian.** All 42 source strings on the page were correctly wrapped for translation but had no `nb_NO` `msgstr`, so the page rendered in English on Norwegian sites. Filled in the Norwegian translations (header + stats, the three wizard steps, search/filter controls, the confirm panel, the audit-log table, and the JS status strings). Source unchanged; only `languages/nb_NO.{po,mo,l10n.php}` + JSON.
- **Course and group landing templates now use English source strings.** The defaults in `templates/single-scorm_package.php` and `templates/single-scorm_group.php` no longer ship Norwegian text for breadcrumbs, badges, module/course labels, cohort seat purchasing, status pills, intro headings, registration links, and certificate panels, so English/default installs do not show Norwegian labels unless a locale translation overrides them.

## [1.6.0] - 2026-06-04

### Added

- **Webinar meeting link now also shows on the course-grid cards.** The `[scorm_courses_grid]` card actions consult the course-type handler's `get_learner_cta()` (the same one the single-course page uses), so an enrolled learner sees the **"Til webinaret"** card CTA — opening the meeting in a new tab — instead of the dead "Content Unavailable" pill a content-less webinar would otherwise render. Gated by the handler itself (access + a URL set), so guests and non-webinar types are unaffected; mirrors the existing Fysisk external-signup card branch. (TSK-18790)
- **Webinar courses surface their meeting link to enrolled learners.** A webinar is a normal course learners enrol in; the external registration / Teams link (`_webinar_url`) is the post-enrolment payoff. `Scorm_Course_Type_Webinar_Handler::get_learner_cta()` now provides a learner CTA, consulted by `scorm_player_course_get_cta()` (after the sequential-lock check, before the no-content check, via `method_exists()` so it stays additive — no interface change): a user with access and a webinar link set sees a **"Til webinaret"** primary CTA opening the meeting in a new tab, instead of the dead "Innhold ikke tilgjengelig" a content-less webinar would otherwise show. Non-enrolled visitors keep the normal enrol/purchase CTA; a webinar with no link behaves like any course. Gated behind `scorm_course_types_enabled`. (TSK-18790)
- **The Setup wizard menu item can now be hidden.** A new **Show the Setup wizard in the admin menu** toggle under **Settings → Setup Tools** controls the `Oppsett` (Setup) menu entry. By default ("auto") the item appears during onboarding and **auto-hides once the wizard is completed or skipped**, so a finished site is no longer cluttered by it; turning the toggle on pins it permanently, off hides it for good. The wizard page itself stays routable (the "Open Setup Wizard" button still works) — only the menu entry is conditional. New helper `scorm_player_should_show_setup_menu()` derives visibility from the explicit setting, falling back to the existing wizard `status`. nb_NO translations included.
- **Course type shown on the frontend course listings.** When the course-types feature is enabled (`scorm_course_types_enabled`), a discreet grey pill marks the course format on the three frontend course surfaces: the `[scorm_courses_grid]` cards (above the title), the group/module landing rows (in the module meta line, via the `scorm_group_module_meta` filter — sub-group module rows carry no type and are skipped), and the `[scorm_my_profile]` "my courses" rows. Only the non-default formats are badged — the standard **Digital** type is left unmarked to avoid cluttering ordinary e-learning courses — so Fysisk / Webinar / custom types stand out. The pill carries a slug class (`type-webinar`, …) and reads its colours from `--scorm-course-type-badge-bg` / `--scorm-course-type-badge-color` so themes can restyle it. New helper `scorm_course_types_get_badge_html()`. (TSK-18817)
- **Course-type fields and admin list column.** The course-type meta box now exposes type-specific fields: selecting **Webinar** reveals a "Webinar link" URL field (stored on the `scorm_package` as `_webinar_url`, for the meeting or a webinar guidance page), and **Fysisk** now adds an "Address" textarea (`_scorm_physical_address`) next to the existing external signup URL. The `scorm_package` admin list gains a **"Course type" (Kurstype)** column showing the assigned type, with the webinar link / physical address surfaced as a hint under it. All additive and gated behind the existing `scorm_course_types_enabled` toggle; switching type leaves previously-entered values untouched. (TSK-18817)
- **Course and group editors now expose the native Excerpt ("Utdrag") field.** `scorm_package` and `scorm_group` register `excerpt` support, so the Excerpt panel appears in the Gutenberg sidebar (and the Excerpt meta box in the classic editor). The course/group card grids and the single-course hero already render this excerpt as the short intro line (`has_excerpt() ? get_the_excerpt() : trimmed content`); until now the field had no editor UI, so editors could not set the intro text without WP-CLI/DB access. Purely additive — existing posts without an excerpt keep the trimmed-content fallback.
- **Setup wizard can import demo Creator courses.** The optional onboarding step now queues the bundled MentorKit demo courses through the shared background SCORM download pipeline, shows per-course import status/progress, skips already-imported packages, stores demo metadata, and keeps sensitive download tokens out of logs.

### Fixed

- **Course landing CTAs and completion overlay now use English source strings.** The defaults in `includes/frontend/course-helpers.php` no longer ship Norwegian text for no-content, locked, start/continue, buy, enroll, and completion-overlay states, so English/default installs do not show Norwegian labels unless a locale translation overrides them.
- **Group landing CTAs and helper labels now use English source strings.** The defaults in `includes/frontend/group-helpers.php` no longer ship Norwegian text for purchase, enroll, start/continue, certificate, breadcrumb, format, and completion states, so English/default installs do not show Norwegian labels unless a locale translation overrides them.

## [1.5.0] - 2026-06-03

### Added

- **Freemius Insights is now part of the MentorKit setup wizard.** Fresh onboarding includes a new optional `Help Improve MentorKit` step between Creator connection and optional add-ons (`welcome -> create_account -> connect -> insights -> optional -> first_course`). The step uses the existing MentorKit wizard card/list/button styling, explains that Freemius Insights is lifecycle/diagnostic analytics only, links to Freemius privacy/terms, and lets admins either share diagnostic data or continue without sharing. Accepting calls the Freemius SDK opt-in flow and marks only the `insights` step complete; declining calls `skip_connection()` anonymous mode and marks only `insights` skipped. Freemius native activation redirects and after-connect/after-skip URLs are routed back to the MentorKit wizard so the SDK prompt does not hijack onboarding.

## [1.4.0] - 2026-06-03

### Added

- **Production-safety guard for `SCORM_ENV` + Creator webhook alerting.** A site deployed by raw-source/rsync (not a packaged build) keeps the plugin's `local` default, which silently routes the Creator API to `https://localhost:5001` **and** disables Sentry — so course publishing breaks with zero alerts (observed on two live customer sites). New `includes/utils/environment.php` distinguishes a genuinely-local dev install (host allowlist `*.local` / `*.test` / `*.ddev.site` / `localhost` / loopback, or `wp_get_environment_type()` of `local`/`development`) from `SCORM_ENV=local` on a real host. When misconfigured: a persistent red wp-admin notice (`includes/admin/admin-notice-env.php`, **SDK-free** so it renders even when `vendor/` is absent on a raw deploy), an `error_log` line, and a best-effort `fatal` Sentry event — Sentry now initialises despite `local` in this one case, rate-limited to one report per host per hour. The developer `*.local` / wp-env flow is byte-for-byte unchanged (host allowlist). New `scorm_player_capture_message()` Sentry helper.
- **Creator webhook failures are now surfaced instead of swallowed.** Both background webhook processors (`class-scorm-upload-processor.php`, `class-scorm-download-request.php`) capture every transport error and non-2xx response to Sentry (deduplicated by fingerprint, never logging the request token or PII) and record a dismissible "last Creator sync failed" wp-admin notice (`includes/admin/creator-sync-status.php`) that self-clears on the next successful sync. On a misconfigured `local`-on-production host the webhook now **fails closed** (refuses the `localhost` target) instead of hanging on a 30-second connection timeout.

### Fixed

- **Release builds now fail if a package still ships `SCORM_ENV=local`.** Both release workflows (`release-plugin.yml`, `release-branch-prerelease.yml`) assert the packaged `scorm-v4.php` does **not** contain `define('SCORM_ENV', 'local')`, in addition to the existing `'live'` assertion — so a build whose environment rewrite did not run can never be published to the fleet.
- **Group lesson list no longer offers a dead-end "Start" on lessons without uploaded SCORM content.** A leaf lesson (`scorm_package`) with no package files showed a clickable Start/Fortsett pill in the group/module landing list that launched an empty player ("no SCORM course attached"). The list now downgrades such rows to a fully non-clickable `no-content` state ("Innhold ikke tilgjengelig") — **both the action pill and the lesson title become inert spans**, so with sub-group navigation enabled an enrolled learner can no longer click the title and be redirected straight into an empty player. Reuses the canonical `scorm_player_course_has_package_files()` check the single-course hero CTA already relies on. Applies to both the flat module list (`templates/single-scorm_group.php`) and the sectioned renderer (`includes/frontend/group-path-render.php`). New `scorm_group_module_pill_text` filter lets a client theme relabel any pill state without forking the template (e.g. show "Kommer snart" on a pre-launch course still in production). Styled with a muted `is-no-content` pill (`assets/css/single-group.css`).

## [1.3.1] - 2026-05-28

### Fixed

- **Empty course grid on client sites that render `[scorm_courses_grid]` cards via the public API.** Restores `SCORM_Courses_Grid_Shortcode::render_card_for_id( $course_id, $args )` — a public wrapper around the private `render_course_card()` that sets up and restores the global `$post` via `setup_postdata()`, so external code (REST endpoints, blocks, custom widgets) can render the exact `.scorm-course-card` markup the shortcode emits, outside The Loop; returns an empty string for non-`scorm_package` IDs. The Bondelaget hybrid courses filter (`bondelaget-rjr-customizations`) calls this method through a `method_exists()` guard, which fell through because the method was absent from the released plugin — so its `/kurs/` REST endpoint returned empty card HTML for every course and the grid rendered blank.

## [1.3.0] - 2026-05-28

### Added

- **Sub-group navigation is opt-in (new setting `scorm_player_module_navigation`, default off).** The parent group → sub-group → course navigation — exit-to-module redirect, skipping the lesson landing for enrolled learners, and the `Group › Module › Lesson` breadcrumbs — is gated behind one LMS Pro setting (*Sub-group navigation*, player section). Default off, so existing flat-course fleet sites are unchanged; structured certification sites (e.g. Avløserkortet) opt in. Gated at one chokepoint: `scorm_player_resolve_return_group_id()` returns 0 when disabled, reverting player exit, completion screen, breadcrumbs and the lesson-landing redirect to stock. Affects navigation only — never course access. (TSK-18707)
- **Course player "Tilbake til modulen" exit — learners return to the module landing page instead of being stranded.** When a course (`scorm_package`) is opened from a group landing page, that group is carried through as a `return` query arg and the SCORM player's exit button now sends the learner back to the module (sub-group) landing page rather than the global course archive. New helper `scorm_player_resolve_return_group_id( $user_id, $course_id )` in `includes/core/url-helpers.php` resolves the target (explicit `return` arg → `course_access_via_group_<course_id>` enrollment marker → none). The exit button becomes visible whenever a return target resolves (`.scorm-player--has-return`), and `data-scorm-return` makes the course-view script honour the explicit href instead of the fragile `history.back()` referrer chain. Standalone course views (no group context) are unchanged. (TSK-18707)
- **"Tilbake til modulen" now also covers course completion and the SCORM content's own exit/close controls.** Extends the exit-button work so every way out of the player returns to the originating module (sub-group) landing page when launched via `?return=<id>`. The course-view script's `myCoursesUrl` (used by the SCORM content's header-close / exit-button / `closeCourse` / postMessage overrides) now resolves to the return group, and the completion overlay's "next" action (`scorm_player_course_get_completion_context()`) prefers the explicit return sub-group over the master enrollment group, labelled "Tilbake til modulen". Sequential next-module and group-certificate flows are unchanged; only the master-group fallback is redirected to the precise module. (TSK-18707)
- **Public completion API for client sites — `scorm_player_user_has_completed()` and `scorm_player_get_user_course_progress()`.** Two prefixed helpers in `includes/utils/tracking.php` let client sites read real per-user SCORM completion instead of faking it. `scorm_player_user_has_completed( $user_id, $course_id )` returns `true` only when the tracking status is `passed`. `scorm_player_get_user_course_progress( $user_id, $course_id )` returns `['status' => 'passed'|'failed'|'attempted'|null, 'completed' => bool, 'score' => float|null]`, parsing the numeric score from stored SCORM runtime data (resolution order `cmi.score.scaled` → `cmi.score.raw` → `cmi.core.score.raw`, `null` when unavailable). Both reuse the existing `scorm_player_get_user_course_status()` / `scorm_player_get_scorm_runtime_data()` getters, validate inputs with `intval`, decode JSON defensively, and are wrapped in `function_exists()` guards.
- **Enrollment URL helper — `scorm_player_get_enroll_url()`.** Public client-integration helper in `includes/core/url-helpers.php`; wraps `scorm_player_get_course_view_url()` and appends the `enroll_course` query arg so client themes stop hand-building enrollment routing. Accepts a post ID, object, or slug.
- **`INTEGRATION.md` — canonical client-integration contract.** Documents the public PHP API (completion/access/routing), the `--scorm-*` CSS token contract, the `course_view` template-deferral pattern, composition hooks, and a per-site Definition-of-Done. The contract for building custom-skinned sites on LMS Pro.
- **Content modules as sub-groups — multi-step certifications (TSK-18688).** A master `scorm_group` can now be a multi-step course where each **module is a sub-group** that owns its lessons and renders its own editorial landing page (title, content, featured image). "Subgrupper" is a **third structure option** in the group Courses field, alongside *Flat bundle* and *Sections (multi-step)*, reusing the same attach + reorder widget: you build the whole path from the **master's** edit screen. The master owns the ordered list `_scorm_group_module_subgroups` as the single source of truth for module membership + order (mirrors how sections store their modules); attaching a group reparents it under the master, removing it makes it standalone again. A sub-group is a content module iff it is in its master's list — there is no per-sub-group role flag. The master's effective lesson chain is the ordered union of its modules' lessons, so the existing enrolment, sequential-progression and certificate engines span the whole certification with no parallel machinery: a single enrolment in the master grants and tracks every module lesson, sequential progression is one continuous chain across modules, and the master certificate is credited when the last lesson completes (even when lessons are shared with other groups). New core helpers live in `includes/core/module-subgroup.php` (`scorm_group_has_module_subgroups()`, `scorm_group_get_module_subgroups()`, `scorm_master_get_lesson_chain()`, …); the builder + save are in `includes/admin/meta-fields-group-path.php`. Module landing pages are indexable (overriding the blanket noindex applied to B2B cohort sub-groups). Lessons are shared `scorm_package` leaves, so the same module can also be sold standalone via a separate sales-group pointing at the same packages — progress is shared automatically. Leaf/sectioned groups and B2B cohort sub-groups are unaffected (every new code path is guarded behind `scorm_group_has_module_subgroups()`).
- **Subgroup landing pages now show Modul/Del badges and a master-aware breadcrumb.** When a `scorm_group` is a subgroup (its `post_parent` is another `scorm_group`), `templates/single-scorm_group.php` renders a "Modul N" badge — the ordinal within the master group's ordered module list (`_scorm_group_module_subgroups`, the single source of truth for module membership + order, so the badge matches the admin's reorder widget and never counts B2B cohort sub-groups that merely share the same `post_parent`) — plus a "Del" badge from the group's own `scorm_category` term, and the breadcrumb's middle segment links to the master group instead of the global `scorm_group` archive. Two new guarded helpers in `includes/frontend/group-helpers.php`, `scorm_player_group_get_module_ordinal()` (gated on `scorm_subgroup_is_module()`, so only real content modules are numbered) and `scorm_player_group_get_del_term()`. Master and standalone groups are unchanged; the badges degrade gracefully when no ordinal or Del term exists. (TSK-18707)
- **Context breadcrumb across the learning hierarchy — `Avløserkortet › Modul › Leksjon`.** New helpers `scorm_player_get_context_trail()` and `scorm_player_render_breadcrumb()` in `includes/frontend/group-helpers.php` build a breadcrumb from the SAME launch context as the player exit (`?return` / `course_access_via_group` / last-active), so breadcrumb and redirect can never disagree. Rendered on the SCORM player view and the lesson landing page (`templates/single-scorm_package.php`); the lesson page falls back to its generic public trail when no module context resolves (e.g. logged-out discovery). `.scorm-breadcrumb` styling added to `assets/css/scorm-course-view.css`. The module (sub-group) page drops the leading "Hjem" so its trail reads `Avløserkortet › Modul`. (TSK-18707)
- **Last-active module fallback for shared lessons.** `scorm_player_resolve_return_group_id()` gains a third resolution tier: when no `?return` and no enrollment marker resolve, it falls back to the learner's last-active module (`scorm_last_active_subgroup` user meta) — but only when that module still contains the course, so a shared lesson belonging to several modules never returns to a stale one. The marker is recorded on `template_redirect` for course-view requests carrying a sub-group `?return` (master groups are never stored). Covered by `tests/ReturnGroupResolverTest.php`. (TSK-18707)

### Changed
- **Logged-in learners skip the lesson landing page.** When a logged-in learner with access opens a `scorm_package` with an explicit `?return=<module>` context, they are redirected straight into the player (carrying the same `return` so the exit lands back on the module). The redirect runs on `template_redirect` (`scorm_player_skip_lesson_landing_for_module_return()` in `includes/core/url-helpers.php`) — before `get_header()` emits any output — so it is reliable regardless of PHP `output_buffering` (and scoped to the lesson landing only; the course-view/player route is excluded so the redirect never targets itself); it previously ran in the template body after `get_header()` and failed silently when output buffering was off. Context-less / bookmarked visits and logged-out discovery still see the landing page. On a module sub-group page both the lesson's Start/Fortsett pill *and* its title link now thread the module as `?return=<module>` (when sub-group navigation is on), so clicking either takes an enrolled learner straight into the player — previously only the pill carried the hint, so clicking the lesson title stranded the learner on the landing page. For logged-out visitors the lesson title on a module page is rendered non-interactive (a `<span>`, not a link) when sub-group navigation is on, since they cannot consume the lesson until they sign in / enrol at the group level. (TSK-18707)
- **Groups admin list uses neutral parent/sub wording.** The `LMS Pro › Grupper` list (`groups-list-customizations.php`) now presents the hierarchy as **Master group** / **Subgroup** (Norwegian: *Toppgruppe* / *Undergruppe*) — a strictly two-level model keyed only on `post_parent` — instead of the B2B-specific *Master / Kundegruppe / Sub-organization* terms. Tabs are now *All groups / Master groups / Subgroups* and the master column is renamed *Parent group* (*Hører til*), with a short intro line explaining the model. Strings are English source with `nb_NO` translations, so labels follow the active admin language (the term was previously hard-coded Norwegian and leaked into English). Native WordPress hierarchical indentation marks sub-rows. Status / Innkjøper / Plasser columns are unchanged. Presentation only — the underlying group/sub-group data model is untouched. Pill styles live in `assets/css/admin-styles.css`.
- **The SCORM course player is now brand-themable.** `assets/css/scorm-course-view.css` reads the `--scorm-*` token contract (with fallbacks to the previous hard-coded values, so vanilla sites are visually unchanged). Client sites that set `--scorm-*` in their theme now get an on-brand player instead of the WP-admin-blue default — closing the last un-tokenized user-facing frontend surface.
- **`INTEGRATION.md` brand-theming precedence corrected.** Documents that the LMS Theming options (`scorm_*`, generated by `theming-styles.php`) are the authoritative `--scorm-*` source — they win the cascade over a theme `tokens.css`. The contract now prescribes setting brand via the Theming options (driven from the theme's design tokens by `/mentorkit-lms-setup`, versioned as a `scorm-theming.json` snapshot, `scorm_use_astra_colors=0`); the theme `tokens.css` owns `--color-*` only.
- **`scorm_player_get_course_access_state()` and `scorm_player_course_has_package_files()` documented as stable public client-integration API** (see `INTEGRATION.md`), so client themes can drop raw post-meta fallbacks.

### Fixed

- **Empty modules on a master path no longer mislead with "Krever påmelding" / "Låst".** On a master group built from module sub-groups, a module whose lessons don't resolve to a published `scorm_package` (lessons never added, drafts, or trashed/deleted) produced zero gateable lessons, so `scorm_master_render_modules()` fell through to the locked branch and rendered "Krever påmelding" (non-sequential) or "Låst" (sequential). For an already-enrolled learner — and for admins, who are always treated as enrolled — being told to enrol in an empty module is nonsense. Such modules now render a neutral, honest "Ingen leksjoner ennå" state (new `is-empty` row/pill style) instead of implying a gate. The module sub-group's own landing page matches: `scorm_player_group_get_cta()` returns a disabled "Ingen leksjoner ennå" button (kind `empty-module`) for a course-less module, instead of the misleading "Fullfør forrige modul først" locked CTA. Surfaced on the Avløserkortet path, where most modules pointed at trashed package IDs. (TSK-18710)
- **Group-leader "send email to one learner" always failed with "Please fill in all fields"** — the per-learner email modal in `assets/js/group-leader-dashboard.js` resolved the group id with `$(this).closest('tr').find('.scorm-leader-remove-btn').data('group')`, but the participant list is rendered as `<div class="gld-participant-row">` elements (no `<table>`/`<tr>`), so the lookup always returned `undefined`. The modal's Send button got `data-group=""`, the AJAX `scorm_leader_send_email` request reached the server with `group_id = 0`, and the server-side `if ( ! $group_id || ... )` guard rejected it — so the email never sent no matter what was typed (the red field was just the brand-coloured focus ring, not a validation marker). The envelope button now carries its own `data-group`, and the modal reads it directly with `$(this).data('group')`. Also hardened: the modal is removed before a new one opens, and the subject/message inputs use scoping classes read via `$btn.closest('.gld-email-modal').find(...)` instead of duplicate `id`s. Reported on the Livsgledehjem portal.

### Changed

- **Group-leader email modal is now translatable** — the modal heading ("Send email to %s"), the "Subject"/"Message" labels, and the "Cancel"/"Send" buttons were hard-coded English strings built in JavaScript. They are now passed through `wp_localize_script` (`scormLeader.i18n`) so they honour the active locale, and Norwegian translations are provided (`Send e-post til %s`, `Emne`, `Melding`, `Avbryt`, `Send`).

- **Fatal error on sites without WooCommerce** — `scorm_b2b_checkout_seat_editor_localize()` is hooked on `wp_head` and called `is_checkout()` without guarding for WooCommerce being active. On any LMS Pro site that does not run WooCommerce this produced `Call to undefined function is_checkout()` and white-screened every page (frontend and admin-rendered headers). All four `is_checkout()` early-returns in `includes/woocommerce/b2b-auto-subgroup.php` now guard with `function_exists( 'is_checkout' )`, matching the existing `is_product()` guard convention in the same file. (TSK-18495 follow-up)

### Removed

- **Dead "Sort Order" course field.** The per-course `_scorm_course_sort_order` number field (rendered + saved in `includes/admin/meta-fields.php`) no longer had any effect: `get_sorted_group_courses()` now orders by the stored `_scorm_group_courses` arrangement (drag-to-reorder in the group builder), not by that meta. Keeping the input made admins set a value that did nothing. The field is removed; sequential order is controlled entirely by the group's reorder widget. (TSK-18688 review follow-up)

## [1.2.0] - 2026-05-20

### Added

- **Sub-groups inherit sequential progression from the master (Beta)** — A customer group (cohort) now follows its master's sequential-progression setting by default, matching how it already inherits the course list and certificate template. `SCORM_Group_Handler::is_sequential_group()` falls back to the master's `_scorm_group_sequential_progression` value for sub-groups; a new "Arv sekvensiell progresjon fra master" toggle in the sub-group meta-box "Arv" section lets admins opt out per cohort. New meta `_scorm_subgroup_inherit_sequential` (default: inherit) + helper `scorm_group_inherits_sequential()`. Fixes auto-created cohorts always being non-sequential even when the master was sequential. (TSK-18631)
- **Sub-group "Åpen via lenke" access mode (Beta)** — Sub-groups (kundegrupper) reuse the existing `_scorm_group_access` model on the master scorm_group: `free` = the permalink works and any logged-in colleague with the URL can self-enrol via the standard AJAX handler (drawing seats from the pre-paid plassbudsjett, respecting the inherited e-postdomene-allowlist); `closed` = the page renders but the join CTA is disabled, only the buyer/admin can add members manually. Master groups keep their three-mode radio (Gratis / Kjøp / Lukket); sub-group edit screens show a simplified two-mode radio (Åpen via lenke / Lukket). New auto-created cohorts default to `access=free` so the buyer can share the link immediately; the leader-dashboard share-card lets the buyer flip the toggle via AJAX. Sub-group URLs are marked `noindex,nofollow` via a slim `wp_robots` filter. New file `includes/frontend/subgroup-share-card.php` (replaces the prior parallel post_status mechanism). (TSK-18495)
- **Customer-group dashboard status panel (Phase 5, Beta)** — The `[scorm_group_leader_dashboard]` shortcode now renders a status panel between the dashboard subtitle and stats row when the viewed group is a sub-group: a seat-budget progress bar (color-shifts green → amber → red as utilization rises), a validity/expiry pill (only when `_scorm_pool_valid_until` is set), and a buyer-info pill. Masters render unchanged. New action `scorm_leader_dashboard_before_stats_row` powers the injection — no new shortcode added, the existing single dashboard is extended in the same hook-based pattern as the bulk-invite and rename additions. (TSK-18495)
- **B2B sub-group seat-pool (Beta)** — Master groups can now have child sub-groups (one per B2B purchase). Each sub-group represents an innkjøper's seat allocation: it carries its own `_scorm_group_seats`, `_scorm_buyer_user`, `_scorm_pool_valid_from` and `_scorm_pool_valid_until` meta, while inheriting the master's course list and certificate template via `post_parent`. The Group Settings meta box gains a "Group type" dropdown (master vs sub-group of X) plus the purchase fields; the group-leader dashboard gains a paste-multiple bulk-invite section with seat-budget enforcement; sub-groups are hidden from `/groups/` archives and the `scorm_groups_grid` shortcode by default (opt-in with `include_subgroups="yes"`). Group leaders assigned only to a sub-group naturally see only their cohort's members. (TSK-18495)
- **B2B auto-purchase flow (Beta)** — A new "Sell as B2B package — quantity = number of seats" checkbox on WC products turns the quantity selector into a seat-count picker. When a customer orders qty ≥ 2 of a B2B-enabled product, the order-completion handler auto-creates a sub-group with seats = qty, sets the buyer as group leader (`mentorkit_group_leader` role + `_scorm_group_leaders` meta), inherits the master's email-domain allowlist, and links the order line item to the new sub-group via `_scorm_subgroup_id` (idempotent — re-fires create no duplicates). qty = 1 keeps the existing individual-enrolment behaviour. Master groups can configure a "Default B2B validity (days)" so the auto-created sub-group expires after N days; empty = no expiration. The WooCommerce thank-you page renders a "Go to your cohort →" CTA linking to `/my-groups/?group=<new-id>`, and the buyer receives a welcome e-mail with the same link via `SCORM_Email_Manager` (template `b2b_welcome`, with a wp_mail fallback). New action `scorm_b2b_subgroup_created` and filters `scorm_b2b_min_qty`, `scorm_b2b_validity_days`. (TSK-18495)
- **Cohort first-class concept (Phase 4, Beta)** — UI now consistently calls sub-groups "Cohorts" (NB: "Kohort"). Sub-group remains the internal code term. New admin page **LMS Pro › Cohorts** lists every cohort across all masters with seat-bar, status badges (Active / Expiring soon / Full / Expired), and filters by master/status/buyer. The "Enable cohort sales" flag has moved from the WC product to the master scorm_group itself (one source of truth; legacy product-level flag still honoured as a fallback and auto-mirrored down on master save). Master groups gain a "Default cohort validity (days)" field for time-bound cohorts. (TSK-18495)
- **Cohort top-up on repurchase (Beta)** — When a buyer purchases additional seats for a master they already have an active cohort of, seats are added to the existing cohort instead of creating a new one. The buyer sees a notice at checkout describing what will happen, with an opt-out checkbox to force a new cohort. New action `scorm_b2b_cohort_topped_up`, filter `scorm_b2b_topup_on_repurchase`. (TSK-18495)
- **Cohort lifecycle (Beta)** — (a) Refund handler: when an order containing a B2B cohort is refunded, the cohort's `valid_until` is set to today (or seats are reduced for top-up refunds) and the buyer receives an e-mail. (b) Daily wp-cron expiry-warning job sends a notice 30 / 7 / 0 days before `valid_until` (idempotent via `_scorm_b2b_expiry_warn_N` meta flags). (c) Cohort leaders can rename their cohort inline from the leader dashboard via a new "Rename" button (sanitized, max 60 chars, leader-permission-gated AJAX). New actions `scorm_b2b_cohort_refunded`, `scorm_b2b_expiry_warning_sent`. (TSK-18495)
- **Sub-group inheritance helpers (Beta)** — New helpers `scorm_group_inherits_courses()`, `scorm_group_inherits_certificate()`, `scorm_group_get_branding()`, `scorm_group_should_hide_from_archive()`, `scorm_group_collect_hidden_subgroup_ids()`. New filter `scorm_subgroup_branding`. Sub-groups inherit course list and certificate template from the master via `post_parent` by default; per-sub-group overrides exposed in the meta box. The future datterselskap-flow (intern sub-organization mode) will be implemented as a separate type later — the type-picker has been removed for now since only kundegrupper are in active use. (Presis Infra rollout)
- **Explicit inheritance toggles (Beta)** — Sub-groups gain "Inherit course list from master" and "Inherit certificate template from master" checkboxes (default on, backwards compatible). When disabled, an empty own list is treated as "no courses / no certificate" instead of falling back to the master, so admins can intentionally narrow a sub-group. Meta keys: `_scorm_subgroup_inherit_courses`, `_scorm_subgroup_inherit_certificate`. (Presis Infra rollout)
- **Per-sub-group branding override (Beta)** — Sub-groups can carry their own logo (attachment ID) and accent colour (hex), exposed via `scorm_group_get_branding( $group_id )` and filterable via `scorm_subgroup_branding`. Meta keys: `_scorm_subgroup_logo_id`, `_scorm_subgroup_accent_color`. Themes and certificate templates can consume the helper to render sub-organization-specific branding without forking templates. (Presis Infra rollout)
- **Per-sub-group archive visibility override** — A new "Public archive visibility" dropdown lets administrators force-show or force-hide a sub-group on `/grupper/` independently of its type default. The `scorm_groups_grid` shortcode adds an `auto` mode for `include_subgroups` (now the default) which respects the per-group + type default, while `yes` / `no` keep the original include-all / exclude-all semantics. Meta key: `_scorm_subgroup_archive_visibility`. (Presis Infra rollout)
- **Time-limited course access (`TSK-18365`).** Course access can now expire a configurable number of days after enrollment, independent of WooCommerce. Resolution order: per-group `_scorm_group_access_days` (overrides the rest, for members of that group) → per-course `_scorm_course_access_days` → site-wide `scorm_default_access_days` option. `0` = unlimited at every layer; leaving a field blank falls through to the next layer. Expiry is computed on read from each user's `course_enrollment_date_<id>` meta so policy changes take effect immediately and no migration is needed. The course access-state helper surfaces `expires_at` plus a new `access_expired` reason code; the existing date-check helper accepts an optional `$userId` and emits a `'expired'` status. New admin inputs live next to the existing start/end-date fields under *Availability* (course and group editors) and *LMS Pro → Settings → Player & Completion* (site-wide default). The Enrollments tab in the course editor shows a new "Access expires" column.
- **Visibility toggle on courses — hide from public courses listing (Beta).** Courses can now be flagged as hidden from the public `[scorm_courses_grid]` shortcode / courses archive, mirroring the existing `_scorm_group_hide_from_listing` toggle on groups. The course remains fully accessible via its direct URL, group enrollment, and dashboards — it just no longer appears in the public grid. Useful for group-only courses where a customer (e.g. Bondelaget's *Opplæring i landbruksregnskap for regnskapsførere*) wants the modules to be reachable only through the group, not via the open "Kurs" tab. Meta key: `_scorm_course_hide_from_listing`. (TSK-18574)

### Changed

- **"Cohort" → "Customer group" terminology rename.** User-facing labels for B2B-purchase sub-groups updated from "Cohort" / "Cohorts" (Norwegian: "Kohort" / "Kohorter") to "Customer group" / "Customer groups" ("Kundegruppe" / "Kundegrupper"). The change reflects that these groups represent a buyer-account relationship rather than an academic cohort, and reads more naturally in both EN and NO. Internal identifiers — type slug `b2b-purchase`, hook names like `scorm_b2b_cohort_refunded`, meta keys like `_scorm_b2b_*` — are unchanged for backwards compatibility with third-party integrations.
- **Course types taxonomy moved from `product` to `scorm_package`.** The course is now the source of truth for its type (Digital / Fysisk / Webinar); WooCommerce products that sell a course inherit the type via the existing `_scorm_wc_linked_product` linkage, resolved on read by a new bridge in `Scorm_Course_Type_Registry::get_term_for_object()`. `_external_signup_url` is likewise canonical on the package, with a per-request cached lookup helper `scorm_course_types_get_package_for_product()`. The Course type meta box now appears only on the SCORM Package editor — the standalone product-editor meta box and the product↔package sibling-sync writer were removed. WC-dependent filters are now gated behind `class_exists( 'WooCommerce' )`, so course-types loads cleanly on sites without WooCommerce active.

### Removed

- **One-shot course-types migrations (`includes/course-types/migration.php`).** Both `scorm_course_types_run_legacy_migration()` (Fysisk-from-`_external_signup_url`) and `scorm_course_types_run_default_digital_migration()` (default-Digital on every SCORM product/package) ran on every admin pageload until their flag was set, performing unbounded `WP_Query( post_type=product, posts_per_page=-1 )` scans + `wp_set_object_terms` in a foreach. On larger product catalogues this fatalled the admin request when the *Enable course types* toggle was first flipped on, leaving the site in WordPress fatal-error / recovery mode and giving the appearance that WooCommerce had been deactivated. The feature had never actually been used in production (Bondelaget was the only site to attempt enabling it, and that attempt is what surfaced the bug), so the migrations had nothing to migrate. Customers re-tag the handful of relevant courses manually on the SCORM Package editor after upgrade.

### Fixed

- **Sub-group members and admins no longer see "Krever påmelding" / "Låst" on every course row (TSK-18624).** Two related causes left enrolled members locked out of a group's courses: (1) Sub-group enrolment writes `enrolled_group_<sub_id>` only, so visiting the *master* group's landing page failed the `enrolled_group_<master_id>` check. `scorm_player_group_user_is_enrolled()` now also accepts users enrolled in any of the master's descendant sub-groups (new helper `scorm_group_user_has_subgroup_membership()`), and bypasses the check entirely for users with the `manage_scorm_courses` capability so admins / LMS managers can preview the landing page without an enrolment record. (2) On the *sub-group's own* landing page, `scorm_player_group_unlocked_course_ids()` delegated to `SCORM_Group_Handler::get_unlocked_courses_for_user()`, which reads only the group's own `_scorm_group_courses` meta — empty for a sub-group that inherits its course list from the master — so it returned no unlocked courses and every displayed (inherited) row showed "Låst". It now computes the unlock list against the same inheritance-aware, role-filtered, ordered course list the template renders, preserving sequential progression (unlock up to and including the first not-yet-passed course).
- **`_scorm_group_access` no longer reset by WC / Certificate sub-metabox saves.** Saving the WooCommerce or Certificate sub-metabox on a group did not include the access-mode radio in `$_POST`. The save handler in `meta-fields-group.php` defaulted absent values to `'free'` and unconditionally wrote them, silently overwriting admin choices and the leader-dashboard share-card AJAX writes. The handler now resolves the effective value from DB when the save did not originate from the main meta box, and only writes the meta when `$from_main_metabox` is true — mirroring the existing pattern for `_scorm_group_hide_from_listing`.
- **Saving the *Enable course types* toggle no longer unchecks the WooCommerce integration settings.** The course-types tab form posted to `options.php` with `option_page=scorm_player_settings` — the same option-group as the WooCommerce integration checkboxes (`scorm_woocommerce_enabled`, `scorm_woocommerce_grant_access_on_offline_payment`, `scorm_woocommerce_auto_complete_paid_orders`). The WP Settings API invokes every registered sanitizer in the option-group on save; HTML-unchecked checkboxes do not submit at all, so the checkbox sanitizer interpreted their absence as "unchecked" and overwrote each option to `'no'`. The Course Types feature now registers its own option-group (`scorm_course_types_settings`) and the toggle form submits to that group instead, isolating its sanitizer pass from the player-settings group. Mirrors the slug-reset fix from 1.0.0 but applied at the option-group level rather than per-sanitizer. (Hit on Bondelaget after enabling course types under 1.1.0 — WC integration silently flipped off.)
- **LMS Manager can now manage Course Categories and Course Tags.** The `scorm_category` and `scorm_tag` taxonomies were registered without an explicit `'capabilities'` map, so WordPress fell back to its default mapping that requires `manage_categories` for term CRUD — a cap held only by administrators/editors, not by `mentorkit_lms_manager`. The taxonomies now map `manage_terms`/`edit_terms`/`delete_terms` to `manage_scorm_courses` (held by admin, editor, shop_manager, and LMS Manager) and `assign_terms` to `edit_scorm_courses` (additionally held by Course Creator). LMS Managers can now create, edit, and delete course categories/tags from wp-admin without being granted access to regular WordPress post categories. Hit kurs.bondelaget.no where Martin's LMS Manager users couldn't adjust their course category structure.
- **LMS Manager and Group Leader roles regain `wp-admin` access on WooCommerce-enabled sites.** WooCommerce's `WC_Admin::prevent_admin_access()` redirects any user lacking `edit_posts`, `manage_woocommerce`, or `view_admin_dashboard` to `/my-account/` and hides the admin bar on the frontend. Neither `mentorkit_lms_manager` nor `mentorkit_group_leader` carries those caps, so on any client site that activates WooCommerce (Bondelaget, Culligan, etc.) Martin & co. were locked out of the LMS admin surface entirely. New filters whitelist users that pass `current_user_can( 'view_scorm_analytics' )` — the same cap that gates the LMS top-level menu — leaving regular subscribers correctly blocked.
- **Removed `null` parent slug from `add_submenu_page` in the pro course-setup flow.** WP 6.6 + PHP 8.4 emits `Deprecated: str_replace()/strpos(): Passing null` via `plugin_basename → wp_normalize_path` on every wp-admin request. Switched to an empty string, which keeps the page hidden from menus without tripping the deprecation.

## [1.0.0] - 2026-05-15

### Added

- **Hide group from public listing (Beta)** — LMS Managers can now flag individual groups to be excluded from the public `/groups/` archive via a new **Visibility** checkbox on the group edit screen. Hidden groups remain fully accessible via direct URL and keep working for enrolment, invitations, and dashboards — useful for demo or stealth groups. Default off (backwards compatible).
- **Per-group email domain allowlist (Beta)** — LMS Managers can restrict a group's enrolment to one or more approved email domains via a new **Email Domain Allowlist** field on the group edit screen. The check is enforced on self-registration (form + AJAX), AJAX self-enrolment, LMS Manager invite, Group Leader dashboard (add + invite), and admin group-edit leader auto-enrolment. Empty list keeps existing groups open (backwards compatible). Matching is case-insensitive and tolerant of `@`-prefix, newlines, and commas; subdomains are not matched automatically. A `scorm_group_email_allowed` filter supports overrides. (TSK-17958)
- **Custom outbound webhooks (Beta)** — Configure webhook endpoints under **LMS Pro > Webhooks** to receive real-time HTTP notifications for key LMS lifecycle events: `course.completed`, `course.failed`, `course.retake_started`, `group.completed`, `group.retake_started`, `certificate.awarded`, `group_certificate.awarded`, and `enrollment.created`. Each endpoint has its own secret; requests are signed with HMAC-SHA256 (`X-MentorKit-Signature-256`) and include event, delivery ID, and timestamp headers. Endpoints support subscribing to all events or a selected subset, with delivery logging, exponential-backoff retries, test-send, and secret regeneration from the admin UI.

### Changed

- Suppressed third-party admin notice nags (Astra Pro license banner, Wordfence upsell, WP Mail SMTP review prompt, Complianz wizard, Code Snippets, plus common offenders like Elementor/RankMath/Yoast/Woo/Jetpack) for non-super-admins so LMS Managers and other restricted roles see a clean admin. Denylist is filterable via `scorm_player_notice_denylist_prefixes`.

### Security

- Rejection log messages now record only the email domain, not the full address, to keep personally identifiable information out of log files and Sentry breadcrumbs.

### Fixed

- `wp-login.php` redirect now allows Two-Factor plugin actions (`validate_2fa`, `backup_2fa`, `revalidate_2fa`) so 2FA challenge submissions complete instead of being redirected to the frontend login page.
- SCORM course view template now resolves `$post`/user and runs its redirect guards before loading the site header, fixing "Cannot modify header information — headers already sent" warnings when a non-SCORM post or a sequentially-locked course hit `/courses/*/view` (TSK-18139).

## [0.8.2] - 2026-05-10

### Added

- **Completion overlay with next-step CTA for non-Creator SCORM packages.** When a package without a built-in completion screen (Synthesia, Articulate Rise without outro, manually uploaded SCORM) reports completion, the player now shows a popup confirming completion and offering a direct link to the next module in a sequential group (or the group certificate when on the last module, or the group/course-archive as fallback). Replaces the silent "Mark Complete becomes enabled, scroll and click, then navigate back to the group" UX. Suppressed for Creator-API uploads since those already render their own outro screen. (TSK-18472)
- **`_scorm_has_builtin_completion` post_meta + admin checkbox** in a new "Player & Completion" section on the `scorm_package` edit screen. Defaults off for manual uploads; auto-set to `1` in `class-scorm-upload-processor.php` and `class-scorm-download-request.php` when the upload job carries Creator API context (`creator_course_id` + `lms_customer_id`). Admins can override per-package.
- **`scorm_player_course_get_completion_context($user_id, $course_id)` helper** in `course-helpers.php`. Resolves `has_builtin_completion` + a context-aware next-step (`next-module` / `group-cert` / `group-overview` / `course-archive` fallback) using the existing `SCORM_Group_Handler::is_sequential_group`, `scorm_player_group_get_ordered_courses`, and `sp_get_user_group_certificate` infrastructure. The next-module URL is built via `scorm_player_get_course_view_url()` so localized CPT rewrite slugs are respected.

## [0.8.1] - 2026-05-10

### Fixed

- **Course `/view` URLs respect the localized CPT slug everywhere.** Eight call sites (courses-grid shortcode, my-courses dashboard rows + resume CTA, WooCommerce thank-you + order-items, access-control redirect, certificate redirect, and the classic/minimal single-course templates) hardcoded `home_url('/courses/<slug>/view')` and bypassed the `scorm_courses_slug` option. They now route through `scorm_player_get_course_view_url()`, so sites with a localized CPT slug (e.g. `/kurs/` on jakobsenkompetanse.no) get consistent links from every entry point — not just the unified single-course template.

## [0.8.0] - 2026-05-09

### Added

- **Unified `single-scorm_group.php` and `single-scorm_package.php` templates.** Replaces the prior classic templates (635 + 416 lines) with a single coherent design language across both group and course landing pages. Hero with title + lead + featured image + context-aware CTA; intro section ("Hva du lærer"); module list (sequential ON: numbered, sequential-locked; sequential OFF: numbered flat); course-level certificate panel. All previous `scorm_*` action hooks preserved with same signatures. (TSK-18455 + TSK-18458)
- **`scorm_player_get_course_view_url($course)` helper.** Single source of truth for composing the SCORM player URL — reads the same `scorm_courses_slug` option as the CPT registration. Used by group module rows, group hero CTA ("Fortsett kurset"/"Start kurset"), profile "Fortsett der du slapp", and profile course-row.
- **`scorm_group_module_meta` filter.** Slot in the group module row (between course title and action pill) for site-specific labels (e.g. course-type taxonomy term, level, custom badge). Default empty so the column collapses cleanly. Sites that want a label hook in via `add_filter` and the row gets a `.has-meta` class.
- **`RELEASING.md` documenting the release strategy** for this plugin (trunk-based + SemVer + Conventional Commits + Keep a Changelog). Pointers added from `CLAUDE.md` and `AGENTS.md`. Aimed at human contributors and AI coding agents alike — single source of truth for "how do we cut releases here". No code changes.

### Changed

- **Module list redesigned as card-rows with status pills.** Each course row is its own card with subtle border + hover lift; status is a high-contrast pill (`Start` / `Fortsett` / `Fullført` / `Låst` / `Krever påmelding`) that never relies on theme accent for legibility. Whole row becomes a clickable `<a>` when accessible, plain `<div>` when locked. Number badge always rendered (replaces the bullet for non-sequential).
- **Group module rows + group hero CTA + profile "Fortsett der du slapp" jump straight to the SCORM player.** Skip the course landing on click — the user already saw the group/profile context. URL goes directly to the `/view` route handler.
- **Course-view rewrite rule respects `scorm_courses_slug` option.** Localized CPT slugs (`/kurs/`, etc.) now resolve at the player route without 404. Backward-compat `/courses/<slug>/view` rule registered as well so legacy hardcoded URLs keep working. Plugin auto-flushes rewrite rules on update from <0.7.30.
- **Templates lean on Astra customizer for fonts + colors.** Drops hardcoded Lora/navy/cream palette in favor of `var(--ast-global-color-N)` for primary/accent (with editorial-civic fallback). Headings inherit Astra's heading-font cascade naturally — no longer overrides h1/h2 font-family. Surface tokens (`bg-warm`, `ink-muted`) derived via `color-mix` from ink so they stay legible regardless of which palette slot the customizer assigns to global-color-4/6.
- **Filterable `scorm_group_module_meta` slot replaces hardcoded duration column.** The per-module `%d min` label (read from `_scorm_estimated_minutes` post meta) is gone — clients that care can hook in via filter.

### Removed

- **Manager-statistics panel from group landing template.** Was admin-only chrome rendered on a learner-facing page. Action hooks `scorm_before_group_statistics` and `scorm_after_group_statistics` retained so any site that wants admin stats can render their own UI inside them.
- **Spec strip + "KURS" eyebrow tag from hero.** Replaced by a cleaner data-driven flow that derives label/state from the same source as the CTA helper.
- **`scorm_player_group_total_minutes()` and `scorm_player_group_format_duration()` helpers.** Dead code after duration removal.

### Fixed

- **`.ast-container` is `display: flex` with a reserved sidebar column.** Without `flex: 1` the scorm article rendered at ~878px inside a 1240px container on multi-column Astra layouts. Force `article.scorm-{group,course}` to `flex: 1 1 100%` so it fills the inner content width.
- **Cream content background no longer bleeds into the site header.** `body.single-scorm_* .ast-container { background: cream }` was too broad — it also colored the header's inner `.ast-container`, producing visible cream patches in the middle of dark theme headers. Drop `.ast-container` from the selector and add an explicit `transparent` override on header's inner container.
- **`Krever påmelding` and breadcrumbs now legible on every Astra preset.** `--scorm-*-color-ink-muted` previously fell back to `--ast-global-color-4`, which on some customizer presets resolves to a near-white surface color. Switch to `color-mix(ink 60%, transparent)` so muted text always tracks ink.
- **PHPCS: dynamic `<a/div>` tag in module row replaced by explicit if/else** so the WordPress.Security.EscapeOutput sniff stays clean.

### Notes

- **MINOR-bump (0.7 → 0.8) for the visible UX overhaul.** No breaking changes — all action hooks, meta keys, and CPT/role contracts are preserved.
- **Migration: rewrite rules auto-flush on first admin page-load after update.** No manual permalink-resave needed for sites that update via Kernl.

## [0.7.21] - 2026-05-08

### Added

- **Manual course completion for LMS Managers and Group Leaders.** New admin page `Manual course completion` (under MentorKit → Manual course completion) and a new `Manual marking` tab on the Group Leader Dashboard at `/my-groups/`. Lets staff mark courses as completed on behalf of participants who reviewed the content outside the system — plenum-økter, klasseromskurs, dokumentert ekstern sertifisering. A single action sets `wp_scorm_tracking.status = 'passed'`, awards the certificate (when the course has a template) and writes an audit row to a new `wp_scorm_manual_completions` table. Idempotent — re-clicking the same `(user, course)` pair is a no-op so rapid double-clicks never duplicate. Group Leader access is scoped to users in groups they lead AND courses assigned to those groups; LMS Manager has broad access. New REST endpoints `POST /scorm-player/v1/manual-complete` and `GET /scorm-player/v1/manual-complete-candidates`. Source strings are English, translated to Norwegian via `nb_NO.po`. (TSK-18439)

## [0.7.20] - 2026-05-08

### Added

- **Auto-detected Exit button for SCORM packages with cross-origin embeds** (Synthesia, Vimeo, YouTube SCOs). When a same-origin SCORM iframe contains a cross-origin child iframe, JS adds `.scorm-player--external` on the outer container, which fades in a plugin-rendered "Avslutt"-button (top-right) and switches the player to a 16:9 aspect-ratio layout. Local SCOs (Creator, Articulate offline, iSpring, Lectora) keep their existing full-viewport layout and rely on their own internal exit UI — the plugin button stays invisible (`opacity: 0`) so there's no overlap or flash. Detection runs on `iframe.onload`, classifies within ~100ms, and a `MutationObserver` upgrades to external mode if a cross-origin iframe is injected later. Default Exit destination is the scorm_package archive (locale-aware via `get_post_type_archive_link`), with `history.back()` preferred when a same-origin referrer is present. Solves the previously unrecoverable state on Synthesia courses where the cross-origin iframe exposes no exit control. (TSK-18427)

## [0.7.19] - 2026-05-07

### Fixed

- **SCORM 1.2 packages reporting `cmi.core.lesson_status="completed"` now actually update the tracking-table status.** The case for `"completed"` in `scorm-api-emulator.js#checkAndSendStatus()` was commented out, so video-based SCOs (Synthesia exports, default Articulate Rise/iSpring exports, and other SCORM 1.2 packages that report `"completed"` rather than `"passed"`) silently failed to award certificates: runtime data persisted correctly but `wp_scorm_tracking.status` stayed `attempted`. Backend `/status-update` endpoint already accepts `"completed"` and maps it to `"passed"`; the SCORM 2004 branch already calls `sendStatusUpdate("completed")` with the same signature — fix restores SCORM 1.2 parity. Verified via E2E on Jakobsen prod with a Synthesia package. (TSK-18426)

## [0.7.18] - 2026-05-07

### Fixed

- **`SCORM_Email_Manager::disable_default_new_user_email()` no longer returns `null` from the `wp_new_user_notification_email` filter.** Returning `null` crashed WP core (`wp-includes/pluggable.php:2413` reads `$email['to']` without a null-check), surfacing as `Trying to access array offset on null` warnings on every manual admin user-create across MentorKit sites (Sentry SCORM-PLAYER-C/B/D, 22+ events on prod). The filter now sets `to` to an empty string and returns the array — `wp_mail()` aborts silently on empty recipient, identical effective behaviour without crashing core. (TSK-18391)

## [0.7.17] - 2026-05-07

### Fixed

- **Adding new courses to an existing group now grants access to all current members.** Previously, `update_post_meta('_scorm_group_courses', ...)` updated the group's course list but did not loop over enrolled members, so they missed the `access_course_<id>` user-meta flag. Dashboard ("Mine kurs") still showed the new courses (it reads the group's course list), but the single-course template asked them to enrol again. Save handler now diffs old vs new course list and calls a new `SCORM_Group_Handler::grant_new_courses_to_members()` helper that honours role requirements and sequential progression. (TSK-18418)
- **`SCORM_Group_Handler::user_has_group_access_to_course()` fallback meta_query now matches integer-serialized course IDs.** `_scorm_group_courses` is saved via `array_map('intval', ...)`, so PHP serialization stores `i:116;` (no quotes), while the fallback query searched for `"116"` with quotes — never matching. The fallback now uses an OR meta_query covering both shapes (`i:<id>;` and `"<id>"`) for forward and legacy data. (TSK-18418)
- **Refactored `SCORM_Group_Handler::enroll_user_in_group()`** to delegate to two new shared helpers — `filter_courses_for_user()` (role + sequential filtering) and `grant_course_access_to_user()` (per-course grant + email dispatch). Same behaviour for first-time enrolment; ensures the new "add courses to existing group" path uses identical logic. (TSK-18418)

## [0.7.16] - 2026-05-06

### Changed

- **LMS Manager role now includes page-editing capabilities** (`edit_pages`, `edit_others_pages`, `edit_published_pages`, `publish_pages`). LMS Managers commonly need to maintain landing/info pages around their courses (course catalogue, about, FAQ) without getting full Editor scope, which on MentorKit pulls in WooCommerce shop_manager + product/order caps. Posts, products, plugins, and theme remain off-limits. Existing LMS Manager users gain page caps automatically on next plugin load. (TSK-18398)

## [0.7.15] - 2026-05-05

### Fixed

- **LMS Manager and Course Creator roles can now access wp-admin** when WooCommerce is active. WooCommerce's `woocommerce_prevent_admin_access` filter blocks any user without `edit_posts` or `manage_woocommerce` and redirects them to `/my-account/`, which locked LMS-only roles out of their dashboard. The plugin now exempts users with the `edit_scorm_courses` capability and keeps their frontend admin bar visible.

## [0.7.14] - 2026-05-04

### Added

- **`paginate="no"` attribute for `[scorm_courses_grid]` and `[scorm_groups_grid]` shortcodes.** When set to `"no"`, the entire pagination block is skipped even if `max_num_pages > 1`. Intended for featured-grid usage on homepages or sidebars (e.g. "top 6 courses" + "View all" link), where pagination doesn't make sense and would point at the full archive anyway. Default remains `"yes"` (backward compatible). Companion to TSK-18293. (TSK-18348)

### Changed

- **Astra-mapping default for `--scorm-button-text-color` is now palette index 4 (background/light) instead of 2 (heading/dark).** Index 2 is typically a dark heading color; mapped onto a button bg of palette[0] (typically primary brand color, often dark/colored), the result was dark-on-dark and unreadable. Index 4 (background/surface light) is the safer default. Existing installs with the option explicitly set keep their value. (TSK-18340)
- **Course access badges (`.scorm-access-badge` on archive cards and `.course-access-badge` on single-course/group heroes) now ship with subtle `box-shadow` + `text-shadow`** so labels stay legible over bright featured images, especially with pale `--scorm-accent-color` brand tints. Brand colors unchanged. (TSK-18156)

### Fixed

- **`.scorm-btn-primary:hover` and `.scorm-btn-secondary:hover` now darken via `color-mix(... 85%, #000)`** instead of swapping to `--scorm-secondary-color`. With Astra-mapping enabled, primary and secondary palette colors are often near-identical (both navy variants), so the previous hover was nearly invisible. Guaranteed 15% darken regardless of theme. (TSK-18339)
- **MentorKit attribution link in Astra footer-copyright now uses `color: currentColor`** so it stays visible regardless of footer background. Previously it inherited Astra's default link color (often a brand-specific navy) which became invisible on dark/colored footers. (TSK-18343)
- **Course/group post-type archives (`/courses/`, `/groups/`) now fill the full width of flex-based theme containers** (e.g. Astra's `.ast-container`) instead of collapsing to ~660 px. `.scorm-archive-page` gets `flex: 1 1 auto; width: 100%`. (TSK-18156)
- **Group Leader dashboard tabs (`.gld-tab`) no longer inherit theme button styling on `:focus`/`:active`.** Themes like Astra apply a primary-colored background to all `<button>` elements on click, which filled the last-clicked tab with brand color. Tabs now render as minimal underline-only tabs in every theme, and declare `type="button"` so they cannot accidentally submit an enclosing form. (TSK-18156)
- **`[scorm_courses_grid]` pagination on static front pages no longer 404s.** When the shortcode is rendered outside the courses CPT archive (e.g. on a static front page) the pagination base now uses the courses-archive URL instead of the current request URL, so "Page 2" links to `/courses/?paged=2` rather than `/page/2/` (which was the WordPress posts archive and 404'd). On the archive itself, default pretty-permalink behavior is preserved. (TSK-18293)
- **`[mentorkit_user_menu]` text now inherits header text color via `currentColor`.** `.mk-user-menu__name`, `.mk-user-menu__welcome` and the guest `.mk-user-menu__login` previously hardcoded dark fallback colors (`#1e293b`, `#64748b`, `#333`) which were invisible/barely readable on dark headers. The "Welcome back" line uses `opacity: 0.7` to keep the muted visual hierarchy on both light and dark backgrounds. (TSK-18078)

## [0.7.13] - 2026-04-29

### Fixed

- Version bump to 0.7.13 — previous commit accidentally rolled the plugin version back to 0.6.4.1.

## [0.7.12] - 2026-04-29

### Fixed

- **Paid-course access is now granted only on `completed` orders by default**. `processing` orders no longer unlock SCORM content, and pending invoice / manual-payment orders are kept in a non-access state unless the explicit offline-payment immediate-access flow sets `_scorm_offline_access_granted`.
- **Pending WooCommerce purchases now surface a dedicated LMS pending-access state across the learner journey**. My Profile shows the purchased course with a pending-access status, the single-course page and `/view/` screen show payment-pending messaging instead of a second purchase prompt, and the thank-you / order-details CTAs route learners to course details until access is actually granted.

## [0.7.11] - 2026-04-29

### Fixed

- **Setup wizard now suppresses core and third-party admin notices** on the captive onboarding screen so LMS setup stays distraction-free. Includes regression coverage for wizard-only notice suppression.
- **WooCommerce thank-you and order-details course access CTAs now stay hidden until access is actually granted**. Completed/processing orders still show the normal course links immediately, while offline-payment orders only show access once the `_scorm_offline_access_granted` flag has been set.

## [0.7.10] - 2026-04-28

### Added

- **Grant course access on offline payment (BACS / cheque / COD)** — opt-in setting that grants course access immediately while the order stays on-hold for admin reconciliation. Auto-login fires; revoke on cancel/refund/fail clears the idempotency flag. Filterable via `scorm_offline_payment_methods`. Beta. (#103)
- **Mine kjøp / My purchases on `[scorm_my_profile]`** — customers see their WC orders (completed, processing, on-hold, pending, refunded) on the LMS profile with brand-tinted status pills. (TSK-18255)
- **LMS-styled Faktura view** at `?scorm_invoice=1&order_id=NNN` — A4-print-friendly, theme-free invoice. Unpaid offline orders show kontonummer, KID, and forfall (pulled from BACS settings). Override via `templates/scorm-order-invoice.php`. (TSK-18255)
- **Invoice organisation number** setting under Settings → WooCommerce. Renders as "Org.nr … MVA" on the Faktura view. (TSK-18255)
- **Auto-complete paid SCORM orders** (on by default) — paid SCORM-only orders go to `completed` on payment_complete. Filterable via `woocommerce_payment_complete_order_status`. Disable for workflows that need manual admin sign-off. Does not affect offline orders. (#108, #109)
- **First-time password set for auto-created customers** — WC-checkout users get a `scorm_password_pending` flag, a "Set a password" CTA in the post-purchase notice + thankyou panel, and a one-shot path through `[scorm_my_profile]?edit=1#scorm-password-section` (current-password check bypassed once, cleared on save). Norwegian translations included.

### Fixed

- **`[scorm_register]` mirrors name/email to WC billing meta** (`billing_first_name`, `billing_last_name`, `billing_email`, `shipping_first_name`, `shipping_last_name`) so checkout pre-fills for users registered via the LMS auth shortcode. Covers both the standard submit and AJAX endpoints.
- **Paid courses grant access via direct user-meta** (`access_course_<id>`), not only `wc_customer_bought_product()`. Restores SCORM viewer access for BACS buyers and admin-granted users. Includes regression tests. (#103)
- **REST `/upload-status/<job_id>` declares `permission_callback`** (`manage_scorm_courses || manage_options`), removing `_doing_it_wrong()` warnings. (#106)

## [0.7.9] - 2026-04-27

### Added

- **Category filter + bulk-add for the group → courses meta box** — admins can now filter courses by `scorm_category` term and bulk-assign all matching courses to a group with a single click. Includes a dynamic Add-all label that describes the current scope (e.g. "Add all 2 in Onboarding" / "Add 1 matching 'safety' in Sikkerhet"), a Clear-all button, a live "X selected · Y visible" counter, category pills next to each course, and translatable strings with nb_NO included. Scoped to a new `.scorm-courses-picker` wrapper so the group-leaders picker is unaffected. (TSK-18177)

## [0.7.8] - 2026-04-24

### Added

- **In-dashboard plugin updates** via Plugin Update Checker (PUC). Client sites now see plugin updates in WP Admin → Plugins, and can opt into WP's auto-update flow per-site. Releases are published automatically from `master` to Kernl via GitHub Actions, which serves the update manifest and zip to client sites.

## [0.7.7] - 2026-04-24

### Fixed

- Show courses granted through group enrollment on `[scorm_my_profile]` / Mine kurs, including users who only have `enrolled_group_{id}` membership and no direct `access_course_{id}` meta. Group-provided courses are labelled with the source group and respect group date restrictions, sequential progression, and role requirements. (TSK-18189)

## [0.7.6] - 2026-04-24

### Changed

- Certificate builder text elements can now be edited directly on the canvas with a native caret after entering inline edit mode, while keeping the existing sidebar text controls and variable insertion workflow.
- Certificate builder font selection now supports Astra body/headings fonts from the active theme, including Google fonts outside the plugin's built-in registry, and surfaces those Astra theme fonts at the top of the dropdown while avoiding bogus fallback entries like selecting a font group label as the font family.

## [0.7.5] - 2026-04-23

### Fixed

- **My Profile and Group Leader Dashboard fonts now inherit from the active theme** — removed hardcoded Merriweather / Open Sans font stacks (`--scorm-profile-heading-font`, `--scorm-profile-body-font`) so headings and body text pick up the theme's font-family (e.g. Astra's 'Outfit') instead of overriding it. Course cards already inherited correctly; the profile page and group leader dashboard title now match.

## [0.7.4] - 2026-04-23

### Fixed

- Fix Astra palette dropdown being clipped on the Theming page by scoping `overflow: visible` to theming-page cards instead of the global `.scorm-email-card` rule

## [Unreleased]

### Added

- **Pre-release Playwright smoke harness** — new local-only test suite (`npm run smoke`) spins up an ephemeral WordPress via `@wordpress/env`, seeds admin/learner/instructor users and a sample SCORM course, then runs a Playwright smoke suite covering login (admin + learner + wrong-password), profile access, and course visibility. Complements PHPUnit CI; run manually before cutting a release. See `docs/superpowers/specs/2026-04-22-playwright-smoke-harness-design.md` for scope, planned v1.1 (setup wizard, admin course creation) and v1.2 (2FA) follow-ups.

## [0.7.3] - 2026-04-22

### Changed

- Complete redesign of the `[scorm_my_profile]` shortcode — new identity card with inline profile editor and group/role chips, 3-stat KPI row (courses completed / in progress / certificates), "Continue where you left off" resume hero, row-based course list with fagområde tag + status pill + progress bar, 2-col groups/certificates grid, and relative-date formatting throughout. Brand-adaptive via existing `--scorm-*` color tokens; inherits Astra theme colors when `scorm_use_astra_colors` is enabled. The shortcode now owns its `<h1>` and toggles a body class so host themes (Astra, block themes, Beaver Builder) suppress their page title without per-tenant CSS hacks (`page_title="keep"` opts out). See `docs/superpowers/specs/2026-04-21-my-profile-redesign-design.md`. (TSK-18156)
- **Archive-page architecture — CPT archives are now the canonical URL for `/<courses_slug>/` and `/<groups_slug>/`.** The earlier pattern of manually creating a "Courses" / "Groups" standalone page with just a grid shortcode caused URL collisions with the CPT archive and left two layers of truth (page slug + plugin slug option) that had to be kept in sync by hand. Archive templates now render an optional intro block above the grid, sourced from two new settings: **Courses Archive Intro** and **Groups Archive Intro** (WYSIWYG in Settings → URL Settings). Sites with an existing redundant `Courses`/`Groups` page see an inline notice on the settings page offering to move it to trash; the CPT archive URL keeps working afterwards. Pages with custom (non-shortcode-only) content are never auto-detected or touched. `archive-scorm_package.php` and `archive-scorm_group.php` prefer the new intro option and fall back to `the_archive_description()` when unset.

### Added

- Fagområde tag on the new profile course rows, sourced from the existing `scorm_category` taxonomy. Tenants without categorized courses simply see no tag (graceful fallback); assign a category in the course edit screen to surface it on the profile page.
- **Subtitle under the `[scorm_my_profile]` H1** — "Overview of courses, progress, and account settings" (Norwegian: "Oversikt over kurs, fremgang og kontoinnstillinger"). Anchors the heading so the title no longer floats alone above the identity card. Rendered only when the shortcode owns the page title (`page_title="replace"`, the default). Top padding halved (40px → 20px) and H1-to-card spacing tightened to 24px so the heading block reads as one unit with the card below.
- **Archive intro settings** — `scorm_courses_archive_intro` and `scorm_groups_archive_intro` (both WYSIWYG, `wp_kses_post`-sanitized). Rendered above the grid in the respective CPT archive templates.

### Removed

- Old profile DOM (`.scorm-profile-header`, `.scorm-my-courses-grid`, `.scorm-my-course-item` and their CSS). Fine-grained `scorm_before_my_*_item` hooks. Load-more pagination on the profile page (still present in dedicated course listings). Tenants with custom CSS or PHP targeting the old selectors/hooks need to migrate.
- **Dead "My Profile Page Slug" admin field and `scorm_my_profile_page_slug` option.** No runtime code read from this option — profile URLs have always been derived from `get_permalink($scorm_my_profile_page_id)`, which reflects the page's live `post_name`. The field created a confusing second source of truth that silently drifted when admins renamed the page. The option is deleted automatically on upgrade.

### Fixed

- **`?edit=1` on the profile page opened the Edit Profile panel but left the toggle out of sync, so the "Edit profile" button couldn't collapse it and subsequent clicks appeared dead.** The auto-opener set `fields.style.display = 'block'` and added a non-existent class (`scorm-btn-active`) without removing the `hidden` attribute that the real toggle reads to decide open-vs-closed. The auto-opener now dispatches a real click on the toggle so both the opener and subsequent user clicks go through the same state machine (`aria-expanded`, `is-open` class, `hidden` attribute all kept in sync).
- **LMS managers/admins mislabelled as "Group leader" on every group chip in `[scorm_my_profile]`.** The profile chip asked `scorm_user_is_group_leader()`, which has a `manage_scorm_courses` shortcut — correct for access-control gates (admins can manage any group), wrong for a display label. `scorm_get_user_group_role()` now checks the real relationship directly (group `post_author` match, or user listed in `_scorm_group_leaders` meta *and* holding the `mentorkit_group_leader` role), bypassing the capability shortcut. Dashboard access, AJAX gates, cert exports, and other access-control call sites are unchanged. Admins enrolled as plain members now render as "Member".
- **Profile page "Browse catalog" / "All" links 404 when archive slugs are localized.** The courses and groups section headers on `[scorm_my_profile]` fell back to `home_url('/courses/')` / `home_url('/groups/')` when the orphaned `scorm_courses_page_url` / `scorm_groups_page_url` options weren't set (they're no longer written since the archive refactor in 0.7.3). Both links now resolve through `get_post_type_archive_link('scorm_package'|'scorm_group')`, so sites with custom slugs like `/kurs/` or `/grupper/` get the right URL. Falls back to `home_url()` when the CPT archive is unavailable.
- **Underlined meta line on profile group rows.** The whole group row is an `<a>`, and its theme-level link styling overrode the plugin's `text-decoration`/`color` rules (equal specificity, theme loaded later). Bumped the selectors to `.scorm-my-profile a.scorm-profile-row` + explicit rules on `__title` / `__meta` children so the theme can't win the cascade, and gave the meta a hardcoded `#64748b` fallback for tenants without `--scorm-muted-text-color` defined.
- **Group leader dashboard heading now matches the profile page.** `/mine-grupper/` previously rendered two H1s — the theme's page title in sans-serif plus the dashboard's own `.gld-title` ("Your Groups") in a different sans-serif/size/weight — while `/min-profil/` rendered one unified Merriweather 32px/900 title. The dashboard now adopts the same pattern: a `scorm-leader-dashboard-hides-title` body class suppresses the host theme's page title (covers Astra, block themes, Beaver Builder), and `.gld-title` uses the same Merriweather 32px/900 typography as `.scorm-my-profile__title`. Replaces the fragile `:has()`-based theme-title suppression that missed Beaver Builder page headers.
- **Invite User admin page** — CSS and JS never loaded because the `admin_enqueue_scripts` guard hardcoded a stale hook prefix. WordPress derives the prefix from `sanitize_title($menu_title)`, which changed from `mentorkit-lms_page_` to `lms-pro_page_` when the top-level menu label was renamed to "LMS Pro". The guard now matches on the stable suffix `_page_scorm-player-invite-user`, so the page's CSS + JS load regardless of label changes. This also fixes the beta tooltip text rendering inline in the H1, the Invite/Invited Users tab switcher not working, and Select2 not initializing. The tab switcher now also honors the URL hash on load, so deep links like `?page=scorm-player-invite-user#invited-users` open the Invited Users tab directly. (TSK-18173)
- **Hardcoded `/my-profile/` fallbacks removed from runtime URL helpers.** `mentorkit_get_profile_url()` and three WooCommerce integration points previously fell back to `home_url('/my-profile/')` when the configured page was missing or when reading a never-set `scorm_my_profile_page_url` option. These fallbacks produced 404s on sites with a localized profile slug. The WooCommerce call sites now delegate to `mentorkit_get_profile_url()`, and the helper falls back to `home_url()` — no hardcoded slug.
- **My Groups page URL resolves dynamically** — the header dropdown "Group overview" link, the post-login redirect for pure group leaders, the `login_redirect` filter, and the `scorm-login` template no longer hardcode `/my-groups/`. They now go through a new `mentorkit_get_my_groups_url()` helper that resolves the stored page ID in `scorm_my_groups_page_id` via `get_permalink()` (falling back to `home_url()` — no slug guessing). Admins who rename the page keep working menus and login redirects. The activation and migration routines adopt an existing page matching `[scorm_group_leader_dashboard]` or the default `my-groups` slug, then backfill the ID option for sites upgraded before the resolver existed. A new **My Groups Page / Title** block in LMS Pro → Settings → URL Settings mirrors the existing My Profile controls, including a "Create New Page" AJAX button.

## [0.7.2] - 2026-04-17

### Added

- **Hide group from public listing (Beta)** — LMS Managers can now flag individual groups to be excluded from the public `/groups/` archive via a new **Visibility** checkbox on the group edit screen. Hidden groups remain fully accessible via direct URL and keep working for enrolment, invitations, and dashboards — useful for demo or stealth groups. Default off (backwards compatible).
- **Per-group email domain allowlist (Beta)** — LMS Managers can restrict a group's enrolment to one or more approved email domains via a new **Email Domain Allowlist** field on the group edit screen. The check is enforced on self-registration (form + AJAX), AJAX self-enrolment, LMS Manager invite, Group Leader dashboard (add + invite), and admin group-edit leader auto-enrolment. Empty list keeps existing groups open (backwards compatible). Matching is case-insensitive and tolerant of `@`-prefix, newlines, and commas; subdomains are not matched automatically. A `scorm_group_email_allowed` filter supports overrides. (TSK-17958)

### Changed

- Suppressed third-party admin notice nags (Astra Pro license banner, Wordfence upsell, WP Mail SMTP review prompt, Complianz wizard, Code Snippets, plus common offenders like Elementor/RankMath/Yoast/Woo/Jetpack) for non-super-admins so LMS Managers and other restricted roles see a clean admin. Denylist is filterable via `scorm_player_notice_denylist_prefixes`.

### Security

- Rejection log messages now record only the email domain, not the full address, to keep personally identifiable information out of log files and Sentry breadcrumbs.

### Fixed

- wp-login.php redirect now allows Two-Factor plugin actions (`validate_2fa`, `backup_2fa`, `revalidate_2fa`) so 2FA challenge submissions complete instead of being redirected to the frontend login page.

## [0.7.1] - 2026-04-15

### Added

- Optional `pre-commit` hook plus `bin/check-phpcs.sh` helper to lint staged or changed PHP files locally, with matching changed-file PHPCS validation in CI.

### Changed

- GitHub `Validate Plugin` now runs a fast `I18n Regression` check before `Build Plugin Package` for pull requests targeting `master`, so translation regressions fail before the full package build starts.
- Local `pre-push` hook remains translation-only and checks `.po` integrity against `origin/master`.

### Fixed

- Plugin release packages no longer include local-only hook assets such as `.githooks/` and `bin/check-i18n.sh`.
- `includes/admin/user-access.php` now conforms to WordPress coding standards and safely unslashes the `role` query parameter before sanitization.

## [0.7.0] - 2026-03-31

### Certificate Builder

- New Konva.js-based visual certificate builder replaces the Gutenberg block editor for certificate design
- Drag-and-drop canvas elements: text, images from the Media Library, and shapes (rectangle, circle, line) with full property editing
- Google Fonts support with grouped font selector plus template variables like `{{user_name}}`, `{{course_name}}`, and `{{completion_date}}`
- Konva-based certificate preview page for students with zoom controls and inline PDF download
- Client-side PDF export via Konva + jsPDF for pixel-identical output matching the canvas editor; TCPDF and legacy server-side PDF handling were removed
- Editing workflow upgrades: undo/redo history, multi-select, group drag/delete/duplicate, in-session copy/paste, and keyboard shortcuts for duplicate, paste, nudge, and delete
- Editor UI redesigned with a consolidated toolbar, compact color swatch popovers, async save status, and document-sidebar controls
- Removed duplicated certificate metaboxes and WordPress featured image support; certificate backgrounds and controls now live inside the builder
- Legacy certificates from the pre-canvas system now show a rebuild-required unavailable state instead of broken previews/downloads
- Norwegian localization added for certificate editor and preview strings
- Security hardening: the group certificate PDF data endpoint now requires an authenticated user before returning certificate rendering data

### AI Virtual Instructor Integration

- `mentorkit_scorm_package_processed` action hook now fires only after a SCORM package is fully processed and stable on disk, giving addons such as AI Virtual Instructor a reliable publish/update sync point
- `scorm_after_course_player` action hook adds a dedicated render target for addon UI while the learner is actively taking a course
- Shared course-access helpers `scorm_player_get_course_access_state()` and `scorm_player_get_course_enrollment_action()` provide a reusable source of truth for access checks and enrollment/checkout redirects across templates, REST endpoints, and addons
- SCORM package finalization is now centralized across upload, download, and update flows so package-processing hooks run only after files, metadata, and cleanup are complete
- Course detail, player, minimal-course, courses-grid, and certificate access checks now share the same access-state helper so AI Virtual Instructor integrations stay aligned with core LMS rules for open/free/paid/closed courses, group access, and date restrictions

### Access, Registration, and Roles

- Auto-enroll in group on registration so users who click "Create Account" from a group page are enrolled immediately after registering
- Registration page now shows group context with the group name, course count, and visible course list, while hiding role-restricted courses from that summary
- Username field removed from registration; usernames are auto-generated from the email address to reduce friction
- Role-based course filtering for group enrollment — courses can require a specific role (Group Leader / LMS Manager) via the "Required Role" dropdown on the course edit screen (TSK-17886)
- Automatic access sync when a user's WordPress role changes, including grant/revoke behavior for role-restricted courses
- Sequential progression for group courses can lock later courses until the previous required course is completed
- Role badge (★) and N/A indicators in the group leader dashboard for role-restricted courses
- CSV export now reflects role-based access with "N/A" for restricted courses
- Beta badge component added for new and experimental features
- Auto-enroll group leaders as group members when assigned — via Invite User page or admin group edit metabox — so leaders get course access and progress tracking

### Fixed

- Fix group report showing 0 members despite enrolled users by adding a database migration (`1.3.0-group-tracking-backfill`) that backfills missing `scorm_group_tracking` rows for users enrolled via usermeta
- Add defensive runtime initialization in analytics detail view and overview cards — missing tracking rows are created on-the-fly before queries, preventing future data drift

### Security

#### Cloudflare Turnstile

- Added Cloudflare Turnstile protection for frontend login, registration, lost-password, and reset-password flows, including AJAX submissions
- Added Turnstile settings for widget mode, validation failure mode, allowed hostnames, test mode, and a validation health check in MentorKit LMS settings
- Added shared frontend Turnstile helper support so plugin-owned auth forms can opt in without bespoke integrations
- Redirect public `wp-login.php` access to the branded MentorKit auth flow by default, with a break-glass constant available for recovery scenarios

#### Rate Limiting

- Added multi-signal auth rate limiting across login, registration, and password reset flows using IP, username/email, and optional first-party device identifiers
- Added dedicated security event and rate-limit tables with retention controls and logging verbosity settings
- Added proxy-aware IP handling, configurable lockout messaging, and developer threshold overrides for local and staging diagnostics

#### Two-Factor Authentication

- Added Two-Factor plugin support to the frontend `/login/` flow — when the Two-Factor plugin is active and the user has a provider configured, login routes through the standard 2FA challenge instead of dead-ending in a generic error (TSK-18044)
- Login error messages are now mapped from the underlying `WP_Error` code (invalid credentials, missing fields, rate-limited, etc.) instead of being swallowed into a single generic fallback

### Fixed

- Hide WordPress core update and maintenance nags from LMS Manager, Course Creator, Group Leader, Author and Editor roles — they cannot act on "WordPress X.Y.Z is here! Notify the site administrator." notices and should not see them (TSK-17995)

## [0.6.3] - 2026-03-27

### Added

- Billable Active Users card on analytics page — counts unique subscriber/group leader logins per calendar month, excludes admin roles (TSK-17846)
- Billing details tab on analytics page with period selector, enrolled user table, and CSV export for invoicing (TSK-17846)
- Support widget now visible for LMS Managers and Course Creators via `edit_scorm_courses` capability (TSK-17863)
- LMS Managers can now see other LMS Managers in wp-admin/users.php user list (TSK-17854)
- Enrollments tab in course/group editor — view enrolled users, inline enroll/unenroll via AJAX, Select2 user search, group access protection (TSK-17881)

### Changed

- Removed "Free"/"Gratis" badges and labels from all frontend course and group views (TSK-17852)
- "Enroll for Free" buttons now just say "Enroll", "Free Group" → "Group"
- "Review Course" translation improved from "Gjennomgangskurs" to "Se kurs på nytt"

## [0.6.2] - 2026-03-27

### Fixed

- Profile page translations: "Personal Information", "Update Password", "Saving...", "Updating...", and other strings now correctly translated to Norwegian (TSK-17864)
- "Course Creators" and "Assigned Creators" section on course edit page translated to Norwegian (TSK-17853)
- Added missing "Assign course creators who can edit this course and view its analytics." translation
- Complete Norwegian translations for billing tab, enrollments tab, and all new UI strings
- Regenerated `.l10n.php` with all missing translation entries — WP 6.5+ uses this as primary source
### Fixed

- Production packaging now rewrites the distributed plugin to `SCORM_ENV = 'live'` so setup wizard Creator links and Creator webhook callbacks do not fall back to localhost in release builds
- GitHub and Bitbucket release packaging now verify the built zip artifact contains the live SCORM environment before publishing

## [0.6.1] - 2026-03-25

### Added

- `user_invitation` email template with `{set_password_url}` placeholder (en/nb)
- Invite User admin page under MentorKit LMS menu with role-based course/group assignments
- Invited Users tab with status tracking (Pending/Active), resend invitation button
- Edit Profile section on My Profile page (name, email, password change)
- Current password verification required for password changes
- Auto-detect admin-created vs self-registered users for correct email dispatch
- `_scorm_invited_by` user meta to track invitation source
- Login tracking via `_scorm_last_login` user meta
- Course Creators assignment field on course edit page — LMS Managers can delegate course access to course creators
- `scorm_user_is_course_creator()` and `scorm_get_user_assigned_course_ids()` helper functions
- `pre_get_posts` filter so course creators see assigned courses in admin listing
- `map_meta_cap` filter so course creators can edit/delete assigned courses

### Changed

- Default WordPress new user email suppression now context-aware (admin vs self-registration)
- Course creators user picker now only shows users with `mentorkit_course_creator` role
- Group leaders user picker now only shows users with `mentorkit_group_leader` role

### Fixed

- Editor tab bar no longer overlaps the Gutenberg publish confirmation panel — lowered z-index and added top offset to `.editor-post-publish-panel`

### Security

- Managed-role check on resend invitation action

## [0.6.0] - 2026-03-24

### Added

- `mentorkit_lms_manager` role — full LMS access without WP content access
- `mentorkit_course_creator` role — own courses and certificates only
- `mentorkit_group_leader` role — frontend-only, assigned groups, enrollment, and group analytics
- Group leaders meta field (`_scorm_group_leaders`) for multiple leaders per group
- Group leader frontend dashboard with enrollment management, progress tracking, CSV export
- Course Creator analytics scoping — summary stats and reports filtered to own courses only
- Editor/Analytics tab bar in Gutenberg course and group editors
- Version-based capability resync (`scorm_player_cap_version`) with stale cap cleanup
- Chart.js bundled locally (removed CDN dependency)

### Changed

- Analytics Group Reports tab hidden for Course Creator role
- Bulk enrollment forms hidden from group leaders (security hardening)
- Migration uses direct usermeta manipulation instead of `WP_User::remove_role()` for deregistered roles
- CSV exporter validates group ownership and course authorship before export
- Editor tab bar CSS extracted to `assets/css/editor-tabs.css`

### Removed

- `scorm_course_editor` role (auto-migrated to `mentorkit_lms_manager`)
- `scorm_player_create_course_editor_role()` function
- All CDN references to Chart.js (6 occurrences replaced with local bundle)

## [0.5.9] - 2026-03-24

### Added

- Frontend password reset form with cookie-based key storage so users complete the entire reset flow without seeing wp-admin
- Password reset validation with proper error messages for empty, whitespace-only, too-short, and mismatched passwords
- `redirect_to` passthrough across all auth pages (login, register, lost-password) so users return to their original destination after authentication
- PHPUnit coverage for auth handler reset password validation and `WP_Error` stub in the test bootstrap

### Changed

- Replace `session_id()`-based transient error storage with in-memory `$form_errors` static property to avoid cross-user leakage on hosts without PHP sessions
- Use `wp_safe_redirect()` instead of `wp_redirect()` across all auth redirects for open-redirect protection
- Add `wp_unslash()` to all `$_POST` superglobal reads for correct handling of magic-quoted input
- Normalize redirect targets with same-origin and wp-admin blocking validation to prevent regular users from being sent into the dashboard
- Replace custom password reset email body with a regex substitution of the `wp-login.php` URL so translated and customized email content is preserved
- Use `esc_html_e()` instead of `_e()` in all auth templates for consistent output escaping
- Read `pagename` from `$wp_query->query_vars` instead of raw `$_GET` in the asset enqueue guard
- Set dynamic page titles for the password reset view ("Set New Password")

### Fixed

- Prevent open-redirect via unvalidated `redirect_to` parameters on login, register, and lost-password pages
- Eliminate dependency on PHP sessions for form error display, fixing blank error messages on stateless hosting environments

### Translations

- Add Norwegian Bokmål (nb_NO) translations for 327 new strings covering setup wizard, certificate expiration, auth pages, theming, analytics, help center, shortcode descriptions, and WooCommerce/Astra integration messages
- Regenerate POT template from full codebase scan to capture all translatable strings
- Rebuild `.mo` and `.l10n.php` files — Norwegian translation now at 100% coverage (1,280 strings)

## [0.5.8] - 2026-03-20

### Added

- GitHub Actions release workflow that builds the plugin on pushes to `master`, uploads the versioned zip as a workflow artifact, and publishes a GitHub Release tagged from the plugin version

### Changed

- Make the plugin packaging script portable between local macOS development and GitHub's Linux runners
- Validate that the plugin header version and `SCORM_PLAYER_VERSION` stay in sync before creating a release package

## [0.5.7] - 2026-03-19

### Added

- PHPUnit coverage for SCORM auth logout redirect handling and my-courses shortcode asset loading when no global post object is available

### Changed

- Redirect the default WordPress logout flow to the SCORM login page while keeping direct access to `wp-login.php` available for admin and staff logins

### Fixed

- Prevent a `post_content` warning on the SCORM login page when the my-courses shortcode asset loader runs without a valid global post object

## [0.5.6] - 2026-03-18

### Added

- Login / Register theming controls for auth page background and the text outside the auth card
- PHPUnit coverage for auth-specific theme variable output and auth page body class handling

### Changed

- Keep the login/register form card, inputs, links, and buttons inherited from the global SCORM theme while allowing auth-page background and outside text to diverge
- Add `scorm-auth-page` body scoping so auth-only theme variables apply without affecting the rest of the LMS UI

## [0.5.5] - 2026-03-16

### Added

- Restricted WordPress user management for `editor` and `scorm_course_editor` roles, limited to subscriber accounts in the core Users screen
- Safeguards that block restricted editors from managing users through the core REST API and XML-RPC endpoints
- PHPUnit coverage for restricted editor user-access rules and the setup wizard welcome-step skip flow

### Changed

- Grant `list_users`, `edit_users`, and `create_users` capabilities to the editor-based SCORM management roles while continuing to withhold promotion and deletion capabilities
- Limit the Users list table, available role filters, editable roles, and direct user edits to subscriber-only management for restricted editors

### Fixed

- Resolve the setup wizard welcome-step skip action to the MentorKit admin landing page while properly marking the wizard as skipped and clearing the pending redirect flag

## [0.5.4] - 2026-03-10

### Changed

- Rename plugin from "SCORM Player v4" to "MentorKit LMS" with updated author and description
- Replace hardcoded colors with CSS custom properties across auth, courses grid, my courses, single course, single group, certificates, WooCommerce, and minimal header/footer templates
- Add new theming options: surface color, border color, muted text color, header background, and header text color
- Add WCAG contrast utilities (relative luminance, contrast ratio) for automatic accessible text color calculation in the theming engine
- Refactor theming styles output to always emit CSS custom properties and simplify variable-based styling
- Replace inline styles with CSS classes in WooCommerce thank-you page and order item templates
- Move inline styles to `<style>` blocks in minimal header and footer templates
- Remove inline JS styles from single course notice component
- Use CSS variable for surface color in single group pricing features
- Remove stray `var_dump` call in certificates shortcode

## [0.5.2] - 2026-03-09

### Changed

- Bump plugin version to 0.5.2 and add PHP 8.4 requirement
- Remove `transform: translateY()` hover effects from buttons across admin styles, auth, single course, single group, my courses, enrollments, certificates, support widget, and WooCommerce templates
- Set auth wrapper background to transparent in theming styles output

## [0.5.1] - 2026-03-09

### Changed

- Reformat CSS multi-value properties (transition, box-shadow) for readability
- Remove hover transform effects (translateY, scale) from course cards, certificate items, status badges, and course images
- Remove `border-left` status indicators from course progress items and status-specific border styles in my courses
- Remove JavaScript hover animation handlers from courses grid
- Add `!important` to `text-decoration: none` on button links for consistency across themes
- Normalize hex color casing in admin styles

## [0.5.0] - 2026-03-06

### Added

- Sentry error tracking integration for improved error monitoring and diagnostics
- In-app support widget with Algolia-powered self-service search, contextual help links, and MentorKit-branded UI
- Setup wizard flow with ownership checks and AJAX functionality
- REST API endpoints for managing SCORM runtime data per user and per package
- Runtime data persistence using browser storage and server-side storage so users can resume progress across devices
- Tracking utilities, taxonomies, and migrations to support the new runtime data storage model
- **Group Reports** tab on the analytics page with group-level metrics (members, progress, completion rates)
- SCORM analytics support for both `scorm_package` and `scorm_group` post types, including dedicated group analytics views and AJAX handlers
- Analytics shortcode for frontend display with author-based filtering
- Search inputs on the analytics page to filter groups, courses, and users
- `scorm_v5_export_reports` capability to control access to SCORM report exports
- Admin submenu and export page for downloading SCORM reports in CSV format
- CSV export handlers and exporter class using the `league/csv` library
- Activity tracking module for daily active users on SCORM content
- Groups grid shortcode for displaying SCORM groups on the frontend
- Minimal SCORM course and group archive templates for streamlined course views
- Statistics section on the SCORM admin main page
- Polylang integration for multilingual SCORM content
- Comprehensive Norwegian (nb_NO) translations and i18n improvements
- Localized email template defaults and migration support
- Docker support and environment configuration for development
- AGENTS.md documentation for project onboarding
- Date-based access control for SCORM courses and groups (start/end date fields, frontend messaging, WooCommerce enforcement)
- Certificate expiration settings on courses and groups with admin UI, templates, and "Retake" functionality
- Certificate analytics and scheduled expiry email notifications (cron)
- UI tokens for MentorKit color palette in admin styles
- Admin styles for file inputs, buttons, and MentorKit-branded collapsible upload card
- Tests: `CertificateExpirationTest.php` covering expiration flows

### Changed

- Enhance the SCORM API emulator to inject the current user and load/save runtime data via the new endpoints
- Refine SCORM analytics UI and script loading for better handling of runtime and group analytics data
- Bump the database schema version to `1.2.0-runtime-data` to support runtime data features
- Update the post-upload notice to include direct **Edit** and **View** links for newly uploaded SCORM packages
- Refactor the analytics overview into a card-based layout with clearer enrollment metrics and status indicators
- Improve group analytics rendering, tab injection logic, and CSS for better presentation of group data
- Update CSS and theming for admin, analytics, and authentication pages for improved consistency and responsiveness
- Adjust AJAX handlers to accept both GET and POST requests for better Select2 compatibility
- Grant export capability to existing SCORM roles and update Composer dependencies to include `league/csv`
- Restrict group meta fields to only allow selecting published SCORM courses for improved data integrity
- Enhance the analytics page to include active user metrics and trends
- Update SCORM templates and CSS for more consistent styling across SCORM pages
- Enhance admin styles across Settings, Theming, and SCORM Packages pages
- Refactor main page layout and card designs for improved user experience
- Adjust permissions for the "Upload Courses" menu item for proper access control
- Remove the WP Admin bar for subscriber type users
- Refactor publishing workflow and download processor with improved job status handling (`success`/`error` states)
- Add `author` support to group post type
- Enhance user capabilities for SCORM groups and analytics
- Restrict user report counts to author's own courses
- Refactor SCORM course enrollment handling, access control, and user redirection logic
- Guard SCORM file deletion from trashing (only delete files on permanent delete)
- Enhance SCORM package upload and preview permissions
- Enhance SCORM player documentation and shortcode functionality
- Update build process and build script
- Refactor course access logic across templates to enforce date restrictions and improve user messaging
- Extend admin meta fields for courses/groups to include availability and expiration options
- Improve tracking utility: certificate validation and expiration handling
- Refactor expiry email flow for reliability and accuracy
- Remove obsolete SCORM courses page template

### Fixed

- Fix SCORM taxonomies not showing up in the admin menu
- Fix SCORM zip extraction error handling and upload directory permissions in Docker environments
- Fix code review issues and remove all fuzzy translation entries
- Fix frontend translation loading reliability
- Fix template availability messaging and enforcement for single package/group views
- Guard against missing Algolia SDK and stale search responses

## [0.4.4] - 2025-10-20

### Added

- Minor enhancements and cleanups across admin and frontend modules

### Changed

- Internal refactors for template routing and rewrite rules

### Fixed

- Stability fixes and small UI polish

## [0.4.3] - 2025-10-17

### Added

- AJAX unlink action for SCORM↔WooCommerce product associations
- Additional certificate capability checks

### Changed

- Improved product linking UX and validation

## [0.4.2] - 2025-10-17

### Added

- Use WooCommerce store currency when rendering course prices

### Changed

- Harden product link validation in SCORM meta fields

## [0.4.1] - 2025-10-15

### Added

- Improvements to certificate handling and display

## [0.4.0] - 2025-10-15

### Added

- SCORM admin analytics and certificate management pages
- Email management features and enhanced logging/queueing

### Changed

- Increased email queue threshold and refined logging

## [0.4.0-alpha] - 2025-10-13

### Added

- Email notifications for course and group events (with detailed logging)
- Enrollment notifications and custom user registration email handling

### Changed

- Centralized logging in `SCORM_Group_Handler` via `scorm_player_log`

## [0.3.6] - 2025-10-09

### Added

- `.scorm-group-info` class and related styling improvements

## [0.3.5] - 2025-10-09

### Added

- SCORM admin functionality enhancements and course styling improvements

## [0.3.4] - 2025-10-07

### Added

- Improved SCORM settings management and user profile experience

## [0.3.3] - 2025-10-06

### Changed

- Refactor admin menu structure and streamline settings management

## [0.3.2] - 2025-10-03

### Added

- Font customization options for certificates
- Breadcrumbs in the course template

## [0.3.1] - 2025-10-02

### Added

- Enhanced capabilities management for SCORM

### Changed

- Refactor admin menu to use "Mentorkit" as the main menu slug

## [0.3.0] - 2025-09-29

### Changed

- Finalize theming and environment configuration updates
- Layout tweaks (padding) for course pages

## [0.3.0-alpha] - 2025-09-22

### Added

- Refactored SCORM theming settings with improved frontend integration
- Enhanced color option handling and Astra palette integration

### Changed

- Sanitization improvements for color and settings inputs

## [0.2.0] - 2025-09-11

### Added

- Background processing for SCORM uploads (asynchronous processing)
- New REST API endpoints for uploads and background jobs
- Background download support and cover image handling

### Changed

- Repository housekeeping and `.gitignore` updates

## [0.2.0-alpha] - 2025-06-30

### Added

- Certificate editor and supporting dependencies
- Course finish signal dispatched from player

### Changed

- Deprecated bundled certificates functionality

## [0.1.20] - 2025-06-25

### Added

- Custom capabilities for SCORM course management and adjusted admin permissions
- Certificate Custom Post Type registration
- Custom handling for SCORM authentication pages (fix 404 and custom titles)

### Fixed

- WooCommerce dependency error during initialization

## [0.1.19] - 2025-06-13

### Added

- Theming options with customizable colors and a new admin submenu for SCORM theming

### Changed

- SCORM header/linking updates (exit button directs to current course page)
- Admin UX: "Add New" relabeled to "Upload SCORM Package" and redundant add buttons removed

## [0.1.18] - 2025-05-29

### Added

- "My Courses" feature (admin settings, page creation, shortcode, AJAX actions)
- Frontend templates and styles for course viewing (full-screen optimized)
- Certificate helpers for user/package lookups
- Enqueue JS/CSS for new My Courses UI and progress animations

### Changed

- Header template now resolves My Courses URL via options with fallback
- General style adjustments across course views

## [0.1.17] - 2025-05-14

### Fixed

- Virtual product handling in WooCommerce integration

## [0.1.16] - 2025-05-13

### Fixed

- Minor bug fixes and stability improvements

## [0.1.14] - 2025-05-09

### Added

- Initial WooCommerce integration
- Custom product type for SCORM courses
- Courses archive page and single course template (auto-created on plugin setup)
- Routing/rewrite rules and template inclusion for SCORM course pages

## [0.1.13] - 2025-04-25

### Added

Learndash integration

- Allows admins to enable progression for the course only if a SCORM module is completed (passed or other verbs)
- Allows admins to choose verbs for completion (passed, browsed, failed)
- Disabled & enables the mark complete button automatically
- Enabled the settings only for Learndash

### Changed

- The completion verbs to the correct SCORM ones (passed)
- Updated the tracking page to use the correct completion verb
- Bumped plugin version to 0.1.13

## [0.1.12] - 2025-04-19

### Added

- PHPUnit test suite covering core functionality and WP integration
- Bitbucket Pipelines configuration for running tests on each push
- Certificate issuance tests for individual (`sp_htmlcert_record`) and bundle (`sp_bundlecert_record`) certificates

### Changed

- Bumped plugin version to 0.1.12

## [0.1.11] - 2025-04-18

### Added

- Certificate styling settings: allow administrators to upload a logo and frame image via the SCORM Settings page.

### Changed

- Certificate template now renders the frame as a background <img> to ensure it prints correctly, and displays the logo when set.
- Default print orientation for certificates set to landscape via CSS @page rule.
- Bundled certificates now honor the same logo and frame styling as individual certificates for consistent look & print behavior.

## [0.1.10] - 2025-04-18

### Changed

- Internationalization: added text domain and domain path, loaded translations, and wrapped all user-facing strings for localization.
- Settings sanitization: enforced 'yes'/'no' values for certificate options via sanitize_callback.
- Security hardening: added zip-slip protection by validating ZIP entries before extraction.
- REST API improvements: switched to rest_ensure_response(), tightened permission callbacks to manage_options, and internationalized API error messages and responses.
- Coding standards scaffolding: included phpcs.xml.dist for WordPress Coding Standards.
- Localization cleanup: updated admin menu titles, page headings, buttons, table headers, and JS confirmations with esc_html**(), esc_attr**(), esc_url(), and esc_js().

## [0.1.9] - 2025-04-18

### Added

- Bundle certificate analytics: summary cards and detailed listing of users who have earned bundle certificates on the Certificates admin page

### Changed

- Bumped plugin version to 0.1.9

## [0.1.8] - 2024-05-25

### Added

- HTML Certificate generation feature
- Certificate issuance based on completion or attempt status
- Admin management page for certificates
- User shortcode `[user_certificates]` to display earned certificates
- Print-friendly certificate design (no PDF dependency)
- Export certificates to CSV
- Manual certificate issuance option for administrators

### Improved

- Domain-independence for certificate URLs (uses relative paths)
- Robust rewrite rule handling for certificate URLs
- Fallback URL handling for different server configurations

## [0.1.7] - 2024-05-15

### Fixed

- Fixed permission issues with debug logging by using WordPress uploads directory
- Improved error handling to prevent warnings in server logs
- Added security measures for log files to prevent unauthorized access

## [0.1.6] - 2024-05-12

### Fixed

- Gutenberg block now included in distribution package
- Fixed issue where Gutenberg block wasn't appearing on new installations
- Block files now pre-built and included in the plugin

## [0.1.5] - 2024-04-20

### Improved

- Modular architecture for better code organization
- Organized code into logical modules (core, admin, utils, api, blocks)
- Security improvements with nonce verification
- Enhanced .htaccess rules for SCORM directories
- Added index.php files to prevent directory listing
- JavaScript debugging improvements

## [0.1.4] - 2024-04-09

### Added

- SCORM file replacement functionality
- New "Replace" button in the Actions column
- Modal dialog for uploading replacement files
- Ability to maintain all existing references when replacing SCORM packages

### Improved

- UI for package management
- Code organization and documentation

## [0.1.3] - 2024-04-01

### Added

- Gutenberg block for easier SCORM package embedding
- Height adjustment settings for SCORM content
- Better responsive display for mobile devices

### Fixed

- Issue with MIME types causing 403 errors
- Safari compatibility problems

## [0.1.2] - 2024-03-15

### Added

- SCORM tracking functionality
- User progress tracking (attempted, failed, completed)
- Admin tracking dashboard

### Improved

- Error handling and logging

## [0.1.1] - 2024-02-20

### Fixed

- Path resolution issues on certain server configurations
- Issues with special characters in SCORM file names
- SCORM API compatibility improvements

## [0.1.0] - 2024-02-01

### Added

- Initial release
- Support for multiple SCORM packages
- Custom post type for package management
- Shortcode integration
- Automatic SCORM package processing
- Responsive iframe player
