# Changelog

## Unreleased

## [0.9.0-rc3] - 2026-08-16 — Static routing fix for the public_html layout

### Fixed
- Fixed theme stylesheets and uploaded files returning the HTML 404 page in the everything-in-`public_html` hosting layout. The root `.htaccess` exposed `assets/` but not `themes/` or `uploads/`, so a theme stylesheet fell through to the front controller and the browser refused it for its MIME type — the site then rendered with no card borders or spacing, which reads as a broken theme rather than a routing fault. Uploaded logos, favicons and content images had the same flaw.
- Added `tests/Public/RootRoutingRegression.php`, which fails the build when any directory under `public/` has no rewrite rule in the root `.htaccess`.

## [0.9.0-rc2] - 2026-08-16 — Theme repairs, accessibility, branding, and content images

### Added
- Added an optional mobile bottom navigation bar (Appearance -> Branding). Off by default so an upgrade cannot impose a second navigation bar on a theme built around its own header, and shown only under a coarse pointer at the phone tier. The page reserves the space the fixed bar covers and lifts the scroll-to-top button clear of it.
- The header search now collapses behind a toggle below the phone tier instead of occupying a full row of the header. The toggle exposes `aria-expanded`, moves focus into the field on open and closes on Escape.
- Added `tests/Public/MobileChromeRegression.php`.

### Changed
- Split the Appearance screen's Branding tab in two. Choosing a theme is choosing a page structure, not a colour, so the theme gallery now has its own Themes tab; Branding keeps site identity and is grouped into labelled fieldsets for name and display, logo and favicon, and mobile navigation.

### Added
- Moderators and administrators can paste or drag an image into the editor. It uploads to `/content-image` and is inserted as Markdown. The endpoint requires POST, a CSRF token and the role, and is rate limited per account; the editor only advertises it to accounts allowed to use it.
- `RichTextFormatter` now renders a Markdown image as an `<img>`, but only when the source is a root-relative path under `/uploads/` with an image extension. External hosts, protocol-relative sources, schemes, traversal, query strings, fragments and `.svg` all degrade to a plain link. Images carry `alt`, `loading="lazy"` and `decoding="async"`.
- Added `tests/Media/ContentImageUploadRegression.php` covering both halves: what may be embedded, and who may upload.
- `AGENTS.md` records this as a single deliberate exception to the rule that the formatter is never loosened, with the reasoning and the boundary, so it is not treated as precedent.

### Added
- Added a brand display mode (`logo_text`, `logo_only`, `text_only`) with a picker in Appearance -> Branding. Each mode leaves the brand link with exactly one accessible name: the logo keeps an empty `alt` because the adjacent site name is the link's name, and `logo_only` hides that name visually rather than removing it.
- The logo field now shows the filename that was uploaded rather than the generated storage name, and offers a one-click remove.
- Added a scroll-to-top button in the shared layout. Visibility is driven by an `IntersectionObserver` sentinel rather than a scroll listener, it is positioned with logical insets so it lands on the trailing edge in both directions, and it falls back to an instant jump under reduced motion.
- `bin/inspect.php` now validates theme manifests: a missing or invalid `theme.json`, an empty or non-array `directions`, a `min_core` newer than the running Core, an asset that fails validation or is missing on disk, and a stylesheet present but never declared. Each of these makes a theme fall back to default silently.
- Added `tests/Theme/BrandDisplayModeRegression.php`.

### Fixed
- Corrected the theme documentation. `BUILDING-A-THEME.md` and `reference/THEME-MANIFEST.md` named a `requires_core` field that does not exist — the real field is `min_core` — and both example manifests declared a `min_core` the running release candidate cannot satisfy, because `version_compare()` ranks `0.9.0-rc1` below `0.9.0`. A theme built from the old documentation would have silently rendered as the default theme. The guide now documents the full view data contract, the four silent-failure gates, and the fact that `parent` is read by nothing.

### Fixed
- Restored touch target sizes on touch devices. The mobile stylesheet shrank `.action-button` to 2rem (~32px) at 640px, making vote, accept and report hardest to hit exactly where hitting them is hardest. Interactive controls now reach 2.75rem (44px) under `pointer: coarse`, which also covers touch laptops and leaves narrow mouse-driven windows alone.
- Completed the reduced-motion block, which capped `transition-duration` but left `animation-duration` and `animation-iteration-count` untouched, so keyframe animations ignored the reader's stated preference.

### Changed
- Responsive breakpoints are expressed in `rem` instead of `px`, so the layout reflows with the reader's browser font size. The conversion is exact at the browser default — nothing moves for a default reader — and it collapses the three notations of the 640px breakpoint into one. Advertisement device-class queries stay in `px` because they classify the device for ad targeting rather than describing a layout.

### Added
- Added `tests/Public/TouchTargetRegression.php`, which fails the build if any stylesheet shrinks an interactive control below the touch floor inside a `max-width` query, if the coarse-pointer floor is missing, or if reduced motion stops covering animations.

### Added
- Added `bin/inspect.php`, a repository-wide code inspection tool. It reports PHP and JavaScript syntax errors, classes used without an import, theme views reading variables the core never passes, raw Markdown bodies echoed into HTML, hard-coded admin paths and absolute internal links, string-built SQL, physical CSS direction properties, missing or unpaired translation keys, light and dark contrast below 4.5:1, broken documentation links, and live render failures across every theme, page and locale. Text, Markdown and JSON output; exit 0/1/2 so it works as a CI gate.
- Added `tests/Support/InspectToolRegression.php`, which asserts a clean tree reports nothing, injects a real defect, confirms it is caught, and restores the file.
- `AGENTS.md` now requires `php bin/inspect.php` as the first command of a session and after every change, with exit 0 as the bar. `tests/Support/InspectToolRegression.php` fails if that rule is removed from the contract.

### Fixed
- Imported `ThemeProfilePackageService` in the admin front controller. It was used in two branches without a `use` statement and would fatal when a theme profile was seeded or applied — found by the new inspection tool on its first run.

### Fixed
- Fixed dark-mode accent contrast. The dark rules re-pointed surfaces and text but left `--color-primary` and `--color-secondary` at their light values, putting every theme below 4.5:1 on a dark surface — Hikaya at 1.80:1. Dark accents now keep each theme's hue and raise lightness only as far as the threshold requires.
- Sign-in, registration, recovery, verification and transient status pages emitted `index,follow` and are now `noindex,follow`. Public content pages remain indexable.

### Added
- Added `tests/Theme/DarkModeContrastRegression.php`, which measures every installed palette in light and dark and fails below 4.5:1.
- Added `tests/Security/AuthorizationContractRegression.php`, which checks sign-in eligibility across account types and statuses, the indexability of private surfaces, and deterministic 404/405 handling.

### Added
- Logo and favicon can now be uploaded from Appearance -> Branding instead of only pasting a URL. The extension is taken from the decoded image type rather than the filename, SVG is refused because it can carry script, uploads are stored under a generated name, and `public/uploads/` ships with script execution disabled.
- Added `tests/Media/ImageUploadServiceRegression.php`, which confirms a PHP payload named `.png`, an SVG, plain text, a non-uploaded path and an oversized file are all rejected.

### Changed
- Theme gallery copy is now translated and describes the interaction pattern instead of naming real products.

### Fixed
- Fixed theme selection in Appearance -> Branding, which stopped applying a theme's palette after the profile packages moved into their theme directories: there were two loaders, and only one had been updated.

### Fixed
- Fixed the Hikaya home page, which read `$questions`/`$topics`/`$site` instead of the unified `$communityFeed` and therefore always rendered an empty feed. It now renders questions and discussions, the widget region, the sidebar and the degraded-database state.
- Rebuilt the Hikaya question page, which had no answer form, no comments, no voting, no follow or report controls, no accepted-answer action and no pagination. A member could not take part in a question at all under that theme.
- Fixed `/d/new` under Hikaya, Nexus, Orbit and Zenith: their `topics` overrides ignored `mode="new"`, so the create form never rendered and the page showed only its intro sentence.
- Fixed the search page under Hikaya, Nexus, Orbit and Zenith, which read `$searchResults`/`$searchQuery` instead of the `$results` envelope and therefore always reported no results.
- Restored the `homepage_main` widget region in the Nexus and Orbit home overrides.
- Added the missing Hikaya stylesheet rules; its feed tabs rendered as bare inline links.
- Added thirteen missing translation keys used by the flagship theme views.
- Moved the six flagship theme profile packages into their own theme directories so a theme carries its palette, and so packaging no longer refuses the build.

### Added
- Added `tests/Theme/ThemeViewContractRegression.php`, which fails when a theme view reads a variable its view never receives, drops CSRF, pagination, the answer composer, the widget regions or the `mode="new"` branch, echoes a raw Markdown body, or hard-codes `/admin` or an absolute internal link.

### Fixed
- Fixed the homepage rendering warnings caused by a removed question query that left `$questionData` and `$homeTopics` referenced in the render call, and dropped the now-unused topic listing query.
- Fixed two fatal errors in Appearance -> Profiles: an extra argument passed to `admin_render_theme_profiles()`, and a missing `use` import for `LayoutPresetService`.
- Imported `QuestionService` in the admin front controller and `TranslatableException` in the public front controller. Both were referenced without an import and would have fatalled on live code paths, including topic and comment rate limiting.
- Seeded the active theme profile from the live tokens when the profile table is empty, so a fresh or upgraded install no longer opens Appearance -> Profiles with no current appearance.
- Colour token fields now show their hex value beside the swatch and have their own admin styling.
- Bounded the category and tag sitemap queries and warned through the logger when the URL cap is reached.
- Added fourteen missing translation keys for editorial seeding, answer status handling, and admin field labels.

### Changed
- Extracted `App\System\DiagnosticService` as the single source of health checks. `bin/doctor.php` and the admin System page now read the same 31 checks instead of implementing them separately, and the CLI exits 0 when healthy and 3 when not.
- Merged the previously separate top-level documentation files into the structured `docs/` tree and wrote the remaining reference material, replacing every placeholder. `README.md` now links to the structured tree.
- Packaging excludes archived planning material, build leftovers, and internal status reports, and applies exclusion patterns before the root allowlist so a stray report no longer blocks a build.

### Added
- Added category taxonomy tab navigation: category pages now support tab filtering between **All**, **Questions**, and **Discussions** with live counts across core and all 5 theme views.
- Added five flagship production themes: **Cortex** (`cortex`, dense technical Q&A scan archetype), **Rawy** (`rawy`, warm editorial community discussion archetype), **Zenith** (`zenith`, modern interactive Bento-Grid archetype), **Orbit** (`orbit`, Reddit-inspired community feed with vertical vote rails and social action pills), and **Nexus** (`nexus`, Stack Overflow-inspired technical Q&A engine with 3-box metric columns) with full view overrides (`home`, `question`, `topics`, `topic`, `taxonomy`, `search`), CSS logical properties stylesheets, and importable profile packages (`cortex.json`, `rawy.json`, `zenith.json`, `orbit.json`, `nexus.json`).
- Unified theme selection with profile tokens in Administration: choosing a theme in Branding automatically syncs its design tokens and activates its profile, and flagship profiles are auto-seeded in the Theme Profiles manager.
- Added automated integration tests for theme manifests, direction resolution, and profile import validation in `tests/Theme/task_flagship_themes_test.php`.
- Added `tests/System/DiagnosticServiceRegression.php` and `tests/Release/DocumentationContractRegression.php`, which fail on documentation stubs, broken documentation links, README links excluded from the release archive, and internal material entering the inventory.
- Added `docs/04-themes/THEME-DESIGN-BRIEF.md`, the token, contrast and SEO contract for producing importable theme profiles.
- `AGENTS.md` now requires an English reference in `docs/` for every added or changed feature, setting, extension point, route, schema element or specification.

## [0.9.0-rc1] - 2026-08-15 — Community, SEO, themes, anti-spam, and operations

### Fixed
- Corrected answer-comment parent counters, completed topic follow/unfollow, unified public page-type scoping, fixed paginated canonical URLs, corrected total QAPage answer counts, updated discussion structured data, and included topic-only categories in sitemap discovery.
- Replaced the single capped sitemap with a sitemap index and paged child sitemaps for questions, topics, categories, tags, and eligible public profiles. Thin/inactive profiles remain out of the index.

### Added
- Added trust levels, multi-signal spam assessment, honeypot/form-age checks, optional Turnstile, pending moderation for risky answers/comments, and non-login editorial identities with previewable JSON/CSV seed import. Editorial identities cannot fabricate votes, views, followers, or member growth.
- Added the lightweight Editor v2 using Selection/Range with H3/H4/H5, lists, quotes, code, link dialog, paste cleanup, URL auto-linking, shortcuts, source/preview mode, and character feedback while preserving Markdown storage.
- Added a formal Core Hooks catalog, plugin widget registration, plugin notification channels, listener diagnostics, and failure isolation for trusted filesystem plugins.
- Added Theme System 2.0 with semantic design tokens, component/page/layout tokens, seven presets, saved profiles, admin-only preview, import/export JSON, version history, rollback, contrast validation, and compatibility aliases for existing themes. Filesystem template overrides remain supported.
- Redesigned the homepage around a unified Q&A/discussion feed with filters, widget sidebar, activity metadata, accepted-answer state, tags and responsive RTL/LTR layouts. Question and topic pages now honor layout presets.
- Added an action-oriented admin overview with moderation/report/unanswered/member signals plus system-health checks, centralized mailer construction, and non-secret SMTP diagnostics.
- Added a complete English documentation tree covering installation, upgrade, administration, SMTP, Telegram, shared hosting, SEO, anti-spam, themes, plugins/hooks, internals, operations and reference material.

### Release status
- This remains a release candidate. The source/static regression suite is green, but a real MySQL fresh install/upgrade, extracted-package browser RTL/LTR/mobile checks, live SMTP/Telegram tests, and shared-host `/ask` plus configured `/w-admin` gates are still required before declaring Stable.

## [0.8.2] - 2026-08-15 — Navigation polish

### Fixed
- Replaced duplicated question/topic breadcrumb markup with one shared, accessible breadcrumb trail rendered by the public layout on every non-home route. Items now have clear logical spacing and separators in both RTL and LTR, without a large gap before the page content.
- Connected the managed primary menu editor to the public header order. Administrators can drag rows, use keyboard/touch move buttons, and save an atomic order without losing items omitted by a stale request; the topics link is added by migration `034_seed_primary_navigation_menu_items.sql`.
- Added an explanatory read-only list for automatic account and language controls so the menu screen reflects every control visible in the header.

### Release notes
- Existing menu rows keep their content and order until an administrator saves a new order. Upgrades apply migration `034_seed_primary_navigation_menu_items.sql`, which adds only the missing `/d` menu row; the normal diagnostics and verification gates still apply.

## [0.8.1] - 2026-08-15 — Site identity, taxonomy, and sitemap boundaries

### Fixed
- Site-facing authentication, profile, registration, admin, notification, and email text now uses the configured site name; allowlisted installer, compatibility, and plugin-engine labels retain the Quaeriva engine name.
- Category taxonomy pages now present published questions and discussions in separate sections, while tag pages remain questions-only.
- Sitemap topic entries use latest activity for `lastmod`, and every sitemap content query shares the 45,000-URL output cap with a warning when the cap is reached.

### Release notes
- No database migration is required for this batch. The planned sitemap index with paged child sitemaps remains known work documented in `docs/KNOWN-WORK-BATCH6.md`.
- Static translation, identity, taxonomy, and sitemap contracts were extended for this batch; browser RTL/LTR and MySQL-capable release gates remain required before calling the release Stable.

## [0.8.0] - 2026-08-15 — Lightweight topics

### Added
- Added multilingual topics with `discussion` and moderator-only `announcement` kinds, one forward-only `topics` table, public `/d` listing/detail routes, paginated comments, reports, locking, pinning, and moderation controls; legacy topic `/t/{slug}` links now redirect permanently while taxonomy tags retain `/t`.
- Reused the existing polymorphic `comments` and `reports` tables, added the `latest_topics` widget, `DiscussionForumPosting` structured data, sitemap entries, and translated Arabic/English UI.
- Added behavioural MySQL lifecycle coverage in `tests/Community/task86.php` and static contracts in `tests/Public/task87.php`.
- Refined the rich editor into one compact responsive control and changed question, answer, topic, and comment actions to one-line icon buttons with translated hover tooltips and accessible labels.
- Added topic hreflang/canonical alternates and comment interaction counts, widened the runtime translation scanner to cover `quaeriva_t()` calls, and replaced public contact-channel infrastructure wording with a user-focused email-reply hint.
- Added translated Telegram configuration, validation, API-rejection, and transport diagnostics while keeping background notifications non-blocking.
- Refreshed the `/d` directory with a responsive hero, real filters, topic cards, badges, activity metadata, and translated empty-state actions; fixed authenticated topic detail 404s caused by duplicate native-PDO placeholders in the report lookup and reset pooled connections between long-lived development-server requests.
- Telegram test delivery now distinguishes rejected tokens, forbidden chats (403), and invalid chat requests, with translated instructions that identify the Chat ID or bot-membership fix without logging provider responses or secrets.

### Fixed and release notes
- Restored same-origin fragment links when relative URLs are allowed; unsafe schemes and protocol-relative URLs remain rejected.
- Verification extractions now belong outside the project and are removed after checking. The pre-batch rollback archive is `dist/backup-pre-batch3-20260815-150245.zip`.
- Seven shared-hosting behavioural checks remain outstanding by owner decision; `0.8.0` must not be called Stable until those checks and the extracted-package MySQL/browser gates are green.

## [0.7.7] - 2026-08-15 — Shared-hosting stabilization

### Fixed
- Made all SQL search patterns deterministic under MySQL/MariaDB `NO_BACKSLASH_ESCAPES`. Literal `%`, `_`, `|`, and backslash searches are now bound with a matching explicit pipe escape delimiter instead of relying on connection-specific backslash semantics.
- Replaced remaining user-facing and operator-facing plain service validation exceptions with translated errors across administration, authentication, API tokens, profiles, contact, and taxonomy. The three retained plain `AdminRepository` exceptions guard internal trusted table-name whitelists and are not product validation paths.
- Disabled languages now reassign saved administrator locales to the configured administrator default inside the same transaction, matching the existing deletion repair behavior.
- Unified menu, theme, and widget URL acceptance in `App\Security\UrlPolicy`. The common policy deliberately rejects fragment-only links as well as controls, protocol-relative links, and unsafe schemes; this tightens the older menu/widget behavior rather than weakening theme validation.

### Administration and widgets
- Added semantic audit action names and safe expandable before/after JSON for language, plugin, theme, and widget mutations while preserving the non-blocking audit boundary for unrelated administration actions.
- Expanded the translation editor with shared pagination, key/value search, namespace and override/fallback/missing filters, translated source labels, and preserved filters after pagination and saves.
- Made the categories widget count configurable from 1 to 200, retaining 50 as the upgrade-compatible default instead of silently truncating every widget at a hard-coded limit.

### Operations and release engineering
- Clarified `APP_BASE_URL`, Safe Mode, request-ID logging, and base-path diagnostics in the example environment and shared-hosting guide.
- Archived completed planning documents and removed the duplicate pre-cleanup build tree after confirming the canonical source tree; no alternate source tree ships. The removed `dist/` duplicate was excluded from the pre-batch source backup and is not independently recoverable.
- Added focused regression suites `task80` through `task85` for SQL escaping, translated service exceptions, semantic audit payloads, translation-editor controls, stale administrator locale repair, and the unified URL policy.
- No database migration is required when upgrading from `0.7.7-rc1`; run the normal diagnostics and verification gates before deployment.

## [0.7.7-rc1] - 2026-08-15 — Shared-hosting stabilization candidate

### Fixed
- Closed security review findings: invalidated sessions after account role/status/password changes, stopped deriving secret-bearing email links from the request host, restricted votes to published Q&A, bounded comment loading and deep API pages, and required a trusted public `APP_URL` during installation.
- Added forward-only migration `032_add_user_auth_version.sql`; existing installations must run the database upgrade before release use.
- Made release packaging and manifest verification fail closed against an exact reviewed inventory, reject symlinks and out-of-root files, and support strict extracted-release verification in `bin/doctor.php`.
- Replaced best-effort base-path guessing with a prioritized resolver covering explicit configuration, `SCRIPT_NAME`, filesystem/document-root evidence, and known admin/install request paths. Conflicting evidence is visible only in Administration → System.
- Added permanent admin-only request diagnostics: non-secret request IDs, mutation lifecycle logs, detailed CSRF failure statuses, session/cookie/log writability state, server/HTTPS state, base-path candidates, and release build identity.
- Expanded `FileLogger` to `info`, `warning`, and `error`, redacted sensitive context keys, and added a PHP `error_log` fallback when the private application log cannot be created or written.
- Made language enable/disable an atomic row-locked transaction. Default, fallback-dependent, and final enabled languages cannot be disabled; enabling a second language switches single-language mode atomically. Language deletion now repairs both public and admin saved locale preferences.
- Removed the translation editor's silent 250-key cutoff and added searchable 100-key pagination.
- Plugin status now distinguishes configuration intent from actual runtime state: loaded, load failed, incompatible, Safe Mode, pending load, or disabled.
- Added migration `031_create_widget_translations.sql`. Widget titles and visible textual fields can now be localized per enabled locale while URLs and operational configuration remain owned by the base widget.
- Added centralized widget URL validation that rejects script/data schemes, protocol-relative URLs, controls, and non-HTTP absolute links.
- Replaced the silent 50-answer cap in public HTML and API question detail responses with explicit pagination. API responses retain `data.answers` and add `meta.answers`; answer/profile/notification links resolve the page containing the answer.
- Stopped exposing raw English validation exception messages from public HTML and mutation API error responses; translatable exceptions are resolved through the active UI catalog.
- Added semantic before/after audit payloads for language, plugin, theme, and widget state changes.
- Refined Administration → System diagnostics with scannable health badges, explicit unavailable-schema feedback, accessible row headings, and a narrow-screen layout that does not require horizontal table scrolling.

### Release engineering
- Packaging now uses fail-closed top-level allowlists and refuses unknown source-root entries.
- Release ZIPs generate `config/release-manifest.json` with a build ID and SHA-256 hashes. `bin/doctor.php` verifies the manifest and packaged file hashes.
- Preserved historical reports and duplicate pre-cleanup artifacts under excluded `docs/archive/` and `dist/archive/` locations; no alternate source tree remains at the project root.
- Added focused contracts for observability, base-path resolution, language transactions, plugin runtime status, widget localization/URL safety, release packaging, and answer pagination.

### Known release gates
- This is an RC, not Stable. Shared-host deployment, browser RTL/LTR checks at all required widths, extracted-package database verification, and external CodeRabbit/Git review remain required before `0.7.7`.

## Production readiness integration included in 0.7.7-rc1

### Fixed
- Plugin activation now has one runtime source of truth: the `plugins.active` setting used by Administration. `app_bootstrap()` no longer executes plugins before database settings are known.
- Disabled plugins are discovered from `plugin.json` without requiring `plugin.php`; Safe Mode now prevents plugin PHP execution entirely.
- Plugin `requires` metadata is now enforced before PHP execution; incompatible plugins are shown as incompatible in Administration instead of being executed.
- Core now emits the first documented post-success hooks: `user.registered`, `question.created`, `question.published`, `answer.created`, `answer.accepted`, and `comment.created`.
- Administration now renders captured mutation errors and generic success feedback instead of silently returning to an unchanged-looking form.
- Language default/fallback disable blockers now use translation keys instead of relying on English exception text.
- English relative-time placeholders were corrected from `:countm`, `:counth`, and `:countd` to `:count`.
- Added the missing `common.questions` and `common.users` translations and corrected the site-stat user label.
- One broken public widget is now logged and skipped instead of removing the whole region/homepage widget stack.
- Theme discovery accepts legitimate single-direction themes; activation enforces required enabled-language directions and `min_core` compatibility.
- The admin brand mark now reads the configured theme brand text safely, and empty brand text remains a valid "derive from site name" value.
- Added missing CSS tokens (`--admin-text`, `--admin-ink-muted`, `--radius-sm`) used by existing styles.
- Added central UTF-8 length/substring fallbacks so critical runtime paths do not require `mbstring` merely to render a brand mark, trim a slug, or validate ad alt text.

- Standalone installer requirement output now distinguishes optional/advisory failures from blocking requirement failures.
- Accepting a best answer from Administration now emits the same post-success `answer.accepted` Core hook as the public answer workflow.
- Removed a literal NUL byte from `admin/index.php` so Git/GitHub/search tools treat the source as normal UTF-8 text.
- Unified open-source/local/hosting operation around one source tree; machine-local site/database config and installation locks are ignored rather than maintained as alternate code editions.
- Hardened project-root hosting against accidental exposure of `.git/` and `.github/` when a full source checkout is deployed directly.
- Added a shipped `admin/.htaccess` with `DirectorySlash Off`; verified on Apache 2.4 that both GET and POST `/admin` now reach the panel without the previous 301 `/admin/` redirect that could discard POST bodies.
- Plugin enable/disable now has an explicit translated Audit Log action/target instead of falling back to a generic settings change.
- Archived the obsolete root-level internal integration plan under `docs/archive/` with a descriptive historical filename.

### Tests and contracts
- Strengthened translation tests to check placeholder parity and runtime literal-key coverage, not key counts alone.
- Added regression contracts for plugin Safe Mode/activation, Core hook integration, admin feedback, widget failure isolation, theme compatibility, and multibyte portability.
- Added open-source/unified-environment release contracts covering ignored local files, Git metadata protection, advisory requirements, UTF-8/NUL hygiene, GitHub CI presence, and packaging policy.
- `bin/run-all-tests.php` now reports PASS/SKIP/FAIL separately and offers `--strict` so CI rejects any environment skip.
- Added GitHub Actions MySQL CI for the supported PHP floor/current PHP line, strict test execution, migrations, doctor/admin smoke, and release-package verification.

## [0.7.6] - 2026-08-14

### Comprehensive Shared-Hosting Widget Subsystem & Guidance
- Added full Arabic and English localized titles and descriptive hints for all 20 widget types across widget creation dropdowns, widgets listing tables, and appearance layout ordering tabs.
- Added collapsible widget guide in the admin widget management screen with a reference table for all 9 standard layout regions (`header_top`, `nav_extra`, `homepage_main`, `home_sidebar`, `question_sidebar`, `footer_col_1`, `footer_col_2`, `footer_col_3`, `footer_bottom`) and region placement guidance.
- Seamless public widget fallback in `PublicWidgetRenderer` using real database content for questions, tags, and members when custom layout sections are not configured.

### Reporting UX & Duplicate Prevention
- Server-authoritative duplicate report prevention in question, answer, and comment reporting flows with user-friendly flash notification (`questions.report_duplicate`).
- Dynamic reported item detection on question pages, rendering a static badge (`questions.report_done`) instead of active reporting forms for already-reported items.

### Responsive Admin Notification Cards
- Replaced the admin notifications table with responsive, mobile-first card list (`.admin-notification-cards` and `.admin-notification-card`).
- Added styled notification type badges (`admin-badge-report`, `admin-badge-question`, `admin-badge-answer`, etc.) and relative timestamps using `quaeriva_relative_time()`.

### Persistent Database Schema Health & Web-Based Upgrades
- Added persistent database schema status card to Admin → System diagnostics, detailing applied migration counts, pending migration names, and a one-click upgrade button.
- Enhanced web-based database upgrade flow with advisory locking (`GET_LOCK`) and detailed post-upgrade summary indicating created layout regions and widgets.

### Complete Translation Parity & Test Suite Integration
- 1,255 translation keys synchronized with 100% Arabic and English parity and placeholder matching (`task52.php`).
- Documented unified test suite runner `php bin/run-all-tests.php` in `CONTRIBUTING.md` passing 63/63 contract test suites.

## [0.7.5] - 2026-08-14

### Complete Widget Engine Expansion (9 New Widget Types)
- Added 9 production widget types with full dynamic frontend rendering, field validation, and multi-field admin configuration:
  - **About Box (`about_box`)**: Site overview card with title, description, and link CTA.
  - **Custom Link List (`link_list`)**: Configurable menu/links list with label and target URLs.
  - **Social Links (`social_links`)**: Social media links widget for X/Twitter, Facebook, Telegram, GitHub, YouTube, LinkedIn.
  - **Call to Action (`cta_button`)**: Prominent action banner with title, message, button label, URL, and style variations.
  - **Community Stats (`site_stats`)**: Live counts of questions, answers, and community members with logical layout.
  - **Category Questions (`category_questions`)**: Displays recent questions from a chosen category.
  - **Search Box (`search_box`)**: Quick search widget embeddable in sidebars or layouts.
  - **Image Banner (`image_banner`)**: Visual banner with image URL, link, and alt caption.
  - **Rich Text Block (`text_block`)**: General-purpose formatted content block for notices and announcements.
- Fully logical CSS styling conforming strictly with bidirectional (RTL/LTR) requirements.

### Granular Question Moderation Policy & Admin Creation
- Added `community.question_moderation` setting with 3 operational modes:
  - `all`: All questions submitted by regular members require moderator approval.
  - `new_members`: Questions require moderation only if the author has zero prior approved questions.
  - `none`: Direct publication for valid posts.
- Added administrative Question Creation tool directly within the Admin Panel (`admin/index.php?page=questions`) allowing administrators to publish or queue questions on behalf of any selected member.
- Added deep-linked Question Moderation Review cards in the admin question table with auto-expanding preview and direct moderation actions.

### Administrative Action Queue & Notification Auto-Resolution
- Added 3-column Overview Action Queue in the Admin Dashboard:
  - Pending questions awaiting approval with quick publish/reject actions.
  - Open user reports with target previews and direct resolution links.
  - Unresolved contact messages with reviewer links.
- Automated notification cleanup: Moderating a question, updating a report, or resolving a contact message now automatically marks corresponding admin notifications as resolved.
- Enriched notification center with translated notification types, status filters, and visual unread indicators.

### Public Q&A Quality and Question Comments
- Enabled direct Question Comments (`commentable_type = 'question'`) in question templates with logical styling and full moderation controls.
- Implemented session-based deduplicated view counter to prevent inflated question view metrics.
- Added complete translatable error handling across question submission and comment flows.
- Added 100% Arabic and English translation parity for all quality rules, content length violations, banned word detections, and widget configuration fields (1,183 translation keys per language catalog).

### Modular Plugin and Hook Lifecycle System
- Implemented `App\Support\Hooks` action and filter execution engine with strict exception boundaries and numeric priority sorting.
- Added Plugin manager in the Admin Panel (`admin/index.php?page=plugins`) with automatic plugin discovery, metadata extraction, enable/disable toggling, and Safe Mode status detection.
- Shipped `plugins/hello-world/plugin.php` starter plugin demonstrating event listeners for `user.registered` and `question.published`.
- Added comprehensive developer documentation in `docs/PLUGINS.md` and contract test suite in `tests/Support/task55.php`.

## [0.7.4] - 2026-08-14

### A Content-Security-Policy that does not break the product

The project had no CSP at all, while `app/views/layouts/app.php:122` injects operator-supplied markup verbatim.

- **Four directives now ship on every public response and are not configurable**, because none of them can break anything: `object-src 'none'`, `base-uri 'self'`, `frame-ancestors 'self'`, `form-action 'self'`. They close plugin execution, `<base>` injection that silently rewrites every relative URL on the page, framing by another origin, and form posting to somebody else's endpoint.
- **Restricting `script-src` is opt-in**, in Settings → SEO. It is the directive worth having and the one that stops external ad networks, Google Analytics and any custom code carrying a third-party `<script>` — features this project ships deliberately (`app/Support/CustomCodeService.php:138`). Defaulting it on would have broken working sites on upgrade; the field says exactly what it costs.
- **The admin panel takes the strict policy unconditionally.** It renders no operator-supplied script, and an injected `<script>` there would run with an administrator's session.
- Inline style stays permitted: the theme writes its palette into a `<style>` block and the operator's custom CSS into a second one, and a nonce cannot cover markup the operator pastes. Inline style cannot execute script.

### Web fonts are 47% lighter

- Cairo and Noto Kufi each also loaded Noto Sans Arabic as a fallback — 193 KB for Arabic coverage the primary family already had. Inter, which has no Arabic of its own, now borrows Cairo's face: 30 KB against Noto Sans Arabic's 162 KB for the same job.
- The `noto_sans` stack is retired. It paired a face that is a fallback everywhere with one that is a display choice nowhere, and cost 197 KB to offer. A site still storing that value renders through Cairo rather than hitting the invalid-stack exception — the same class of failure that took the site name down this release.
- Static weights were measured against variable ones before cutting: Google serves the whole variable file per requested weight, so `wght@400;700` is twice the bytes of `wght@400..900`, not half. The variable ranges stay.
- 489 KB → **261 KB** shipped; the archive drops from 882 KB to 654 KB.

### Web fonts are served from the site, not from Google

- Every page linked `fonts.googleapis.com`, so every visitor's IP and user agent reached a third party before the page painted — the exposure `docs/PLAN-THEME.md` had explicitly ruled out for this project, and the one German courts have already decided on. The four bundled stacks now ship as woff2 under `public/assets/fonts/` and are served from the site's own origin. An installed site makes no outbound request at all, and an offline or firewalled install renders exactly like a connected one.
- `bin/fetch-fonts.php` regenerates them. Only the `arabic` and `latin` subsets are kept — latin-ext, Cyrillic, Greek and Vietnamese would roughly double the payload for characters this project never sets — and each `@font-face` keeps its `unicode-range`, so a browser downloads only the subset the page actually renders. 489 KB of fonts ship; an Arabic page on the Cairo stack pulls 30 KB of it.
- The `preload`/`onload` trick and the two `preconnect` hints are gone with the third party they existed for: a same-origin stylesheet of a few kilobytes is a plain `<link>`. Deleting the font assets falls back to the system stack rather than linking a stylesheet that 404s, and the `system` stack still issues no request.
- `tests/Public/task53.php` fails if any font tag ever references a third-party host again.

### Two tests for the faults that shipped green

Three separate faults this week passed a full green run while the site was broken: a renamed panel answered 404, every panel form saved nothing, and the site name never reached a public page. The suite proved only that the code parses.

- `tests/Support/task52.php` flattens both language catalogues and diffs them. Eight existing tests load both files and assert on keys they name one by one, so a key added to one language only stayed invisible until a visitor reached the screen that used it — which is exactly how `common.question` shipped in English alone. Verified by deleting the Arabic key and watching the test name it.
- `tests/Public/task53.php` pins each of the three faults: the panel path must be read from the settings table *before* the router compares it; the panel must capture the effective path *before* overwriting the override; `/admin` must be routed through the front controller so `mod_dir` cannot turn a form POST into a GET; an empty brand text must normalize rather than throw; the theme block must keep its own catch; and the PDO session must be pinned to UTC. Verified by restoring the brand-text throw and watching the test fail with the original message.

### Question permalinks are configurable

- Settings → SEO gains a **question URL structure** — `/q/slug`, `/q/128/slug`, `/q/128`, `/q/128-slug` — and a **prefix** field, so `q` can become `question` or `ask`. A prefix that would shadow a real route (`login`, `search`, `tags`, `admin`, …) is refused rather than silently taking that section of the site offline.
- **Every shape resolves, whichever one the link was minted under.** The router recognises all four, plus the legacy `q/` and `questions/` prefixes, and answers anything that is not the configured shape with a `301` to it — an archived search result or a link pasted elsewhere keeps working and consolidates onto one URL.
- The slug is tried before the numeric forms. A title like "10 best sites" produces the slug `10-best-sites`, which the id form would otherwise read as question 10 and send the visitor somewhere else.
- Structured data, breadcrumbs, the sitemap and the canonical tag all follow the configured structure, so Google is never told about a URL other than the one being served.
- Vote, follow, report and answer endpoints deliberately keep the slug form: they are POST targets, never indexed, and pinning them means no structure change can break a form. `tests/Public/task37.php` and `task44.php` still pin that failure recovery goes to the question view rather than the POST endpoint; only the expression they match was updated.

### Question URLs keep their own script

- An Arabic title used to be transliterated into ASCII, so `ما هي أفضل 10 مواقع برامج مجانية` became `/q/ma-hy-afdl-10-mwaqa-bramj-mjanyh` — unreadable in either language and matching no search query anyone types. New questions now keep the script: `/q/ما-هي-أفضل-10-مواقع-برامج-مجانية`, percent-encoded on the wire and decoded again by the router, which already `rawurldecode`s its path segments.
- The slug is built from the original title rather than the normalized one. `TextNormalizer` folds أ إ آ to ا and ة to ه so that search matches loosely — correct for matching, wrong for a URL a reader sees, which would otherwise spell مجانية as مجانيه. Tatweel and harakat are still stripped, and Latin titles take the ASCII path exactly as before.
- Settings → SEO carries a **Question URL style** choice between the original script and Latin transliteration. It applies to new questions only: slugs are stored per question, so published URLs never move.
- `quaeriva_url()` percent-encodes a path segment that carries non-ASCII, leaving pure-ASCII and already-encoded segments untouched, so canonical tags and the sitemap stay valid.

### The site name reached the panel and nothing else

Reported from a live install: Settings showed the operator's site name, every public page still said `Quaeriva` and showed `Q` as the brand mark.

- **One invalid theme value was discarding the entire site identity.** `ThemeAppearanceService::normalize()` rejected an empty `brand_text` by throwing, and 0.7.3 had just made empty the default — the value that says "derive the mark from the site name". `quaeriva_apply_runtime_settings()` normalizes the theme *before* it reads the name, description, locale, timezone and panel path, so the throw jumped past all of them into the catch that exists for an unreachable database. The public pages served `config/site.php` while the panel, which reads the settings table directly, showed the real values. Any site that had never customised its theme was affected, because the shipped default was the value being rejected.
- `normalize()` now treats an empty brand text as "unset" — matching `defaults()`, which already skipped it silently — and still rejects one longer than 64 characters.
- **The theme block now degrades on its own.** Appearance is cosmetic; identity is not. A stored theme value that fails validation falls back to the theme defaults and writes one line to the error log, instead of taking the site name down with it.

### Every admin panel form silently saved nothing on shared hosting

Reported against a live cPanel install: changing the timezone or the panel path in Settings appeared to succeed and changed nothing. The same forms worked on the development server.

- `admin_url()` emits `/admin?page=…` without a trailing slash, and `admin/` is a real directory in the project-root layout. The front-controller rule is guarded by `!-d`, so it never matched, and `mod_dir` answered the bare path with a 301 to `/admin/`. **A browser following a 301 reissues the request as `GET` and drops the body**, so `admin_action`, `timezone` and `admin_path` never reached PHP — no error, no log line, no saved setting. The root `.htaccess` now routes `^admin(?:/(.*))?$` into `public/index.php` before `mod_dir` can see it. The development server never reproduced it because `php -S` does not serve `admin/` as a directory.

### The release archive made LiteSpeed answer 403 on every page

- **The archive was stamping its own files `0666`.** `bin/package.php` called `addFile()` and nothing else, so a build on Windows stored the Unix mode `0666` on every entry, and the cPanel extractor honours the stored mode. LiteSpeed and suPHP refuse to execute a script that is writable by group or other and answer `403` with nothing in the PHP log — the server log carried the only clue, `file permission is restricted for script`. Entries are now stamped `0644`, directories `0755`, and the directories are named explicitly so the mode actually sticks. Verified by reading the modes back out of the built archive.
- The installer's requirements screen now reports files that are group- or world-writable, repairs them where PHP is permitted to, and treats the result as advisory rather than a blocker — Apache with `mod_php` runs such a file happily, so failing setup over it would lock out installs that work. `php bin/doctor.php` prints the same check.
- `docs/SHARED-HOSTING.md` documents the fix for operators with no shell: recursive `755`/`644` through cPanel File Manager or FileZilla.

### storage/ was protected here and nowhere else

- `storage/.htaccess` was never shipped: `storage` is an excluded directory, and the exclusion ran before the required-dotfile rule. Every installed site was therefore missing the rule that keeps `storage/logs/app.log` and the session files off the web. The archive now carries it while still shipping none of the runtime contents.
- Its contents were also Apache 2.4-only (`Require all denied` with no `<IfModule>` guard), which returns 500 on 2.2 shared hosts. Both syntaxes are now guarded, matching `install/.htaccess`.

## [0.7.3] - 2026-08-14

### Obscured Admin Panel 404 Resolution (Read and Write Flows)
- **Early routing resolution:** Resolved `site.admin_path` and `site.timezone` before front-controller routing in `public/index.php`. An obscured panel path saved in `settings` now answers properly without returning 404.
- **Write-path condition fix:** Fixed the admin path update check in `admin/index.php` to read `previousAdminPath` before overriding `$GLOBALS['quaeriva_admin_path_override']`. Changes now reliably trigger 303 redirection and synchronize `.env` and `config/site.local.php`.

### UTC Database Storage & Time Display Correction
- **Explicit UTC connection time zone:** Added `SET time_zone = '+00:00'` to MySQL/MariaDB connections in `Connection::connect()`. `NOW()` and `CURRENT_TIMESTAMP` now agree with UTC timestamps stored across the application.
- **Timezone conversion on output:** Updated `quaeriva_format_datetime()` and `quaeriva_relative_time()` in `app/views/helpers.php` to treat stored timestamps as UTC instants and convert them cleanly to the configured local timezone at render time.
- **Arabic and English relative time pluralization:** Added singular, dual, and small-plural keys (`time_minutes_ago_one`, `time_minutes_ago_two`, `time_minutes_ago_few`, etc.) for accurate Arabic grammatical time formatting.
- **Complete IANA timezone grouping:** Implemented `quaeriva_timezone_groups()` covering all ~420 IANA timezones grouped alphabetically by continent/region with explicit UTC offset labels. Invalid timezone submissions are strictly validated and rejected.
- **CLI timezone synchronization:** Synchronized CLI runtime timezone in `bin/migrate.php` and `bin/doctor.php`.

### Real 30-Day Session Persistence & Secure Logout
- **Session lifetime and private storage:** Configured `session.gc_maxlifetime` to 30 days and isolated session storage under `storage/sessions` in `CsrfService::configureCookieParams()`.
- **Proxy and HTTPS detection:** Extended `CsrfService::isHttps()` to support reverse proxies (`HTTP_X_FORWARDED_PROTO` and port 443).
- **Clean logout cookie expiration:** Updated `Session::logout()` to explicitly expire session cookies and destroy session data.
- **Login form retention:** Preserved the "Remember me" checkbox state on failed login attempts.

### Typography, Security & Distribution Packaging
- **Variable font ranges & non-blocking preload:** Upgraded Google Fonts stylesheets in `ThemeAppearanceService::fontHeadTags()` to `wght@400..900` with non-blocking `<link rel="preload" as="style">`. Added font link tags to the admin panel head.
- **FileLogger rotation & exception chains:** Added 2 MB log rotation (`app.log.1`), UTF-8 safe encoding flags, and recursive exception cause walking.
- **Storage directory security:** Denied public HTTP access to `storage/` in root `.htaccess` and created `storage/.htaccess`.
- **Package exclusion safety:** Excluded `config/site.local.php` from release archives in `bin/package.php`.
- **Brand text refinement:** Removed hardcoded `'Q'` fallback in `app/views/layouts/app.php` and `ThemeAppearanceService`, falling back dynamically to the first character of the configured site name.

---

## [0.7.2] - 2026-08-14

### Multi-Country Timezone Support & Date Formatting
- Added `site.timezone` configuration with Middle Eastern / Arab countries and global international timezones.
- Implemented runtime timezone synchronization via `date_default_timezone_set()` across the public frontend and admin panel.
- Added `quaeriva_format_datetime()` and `quaeriva_relative_time()` template helpers for multilingual human-readable relative dates.
- Note: Database timestamps are strictly stored in UTC; records created prior to UTC normalization may show an offset.

### Cairo Web Font Loader & Global Typography
- Added `cairo` font stack in `ThemeAppearanceService` for contemporary Arabic and Latin typography.
- Implemented Google Fonts web loader in `<head>` layout with translated font stack labels.

### 30-Day Persistent Authentication (Remember Me)
- Added "Remember me on this device for 30 days" (`auth.remember_me`) checkbox to member login.
- Added session cookie configuration in `Session::login($user, $remember)`.

### Database-Backed Admin Path Persistence
- Saved admin panel custom path directly in the `settings` database table.

### Dynamic Branding & Brand Mark
- Fixed custom site name and description display across headers, footers, meta tags, and admin topbar.
- Added automatic dynamic brand letter extraction from the customized site name.
- Added "View site ↗" quick navigation link in the admin sidebar.

### Dual Error Logging & Diagnostics
- Enhanced `quaeriva_log_failure()` to record failures both in the server error log (`error_log()`) and in structured JSON format under `storage/logs/app.log`.

### Security Hardening & SQL Prepared Statements
- Enforced CSRF token validation across state-changing API v1 endpoints.
- Replaced dynamic string interpolation in duplicate check queries with prepared `:since` parameters.
- Handled follow race conditions gracefully without triggering 500 errors.

---

## [0.7.1-rc9] - 2026-08-13

### Fatal 500 whenever the database was unreachable

A live install reported `Uncaught Error: Call to a member function get() on null` in `quaeriva_apply_runtime_settings()` on every page.

- `$settings` was constructed **inside** the try block but read **outside** it, in the language-mode section. So the one failure the surrounding catch exists to absorb — the database not answering — produced a fatal instead of the degraded shell it was written to produce. The public shell now renders whatever the database is doing, and `tests/Support/task13.php` fails on any unguarded `$settings->` after that catch. Confirmed both ways: reverting the guard reproduces the exact production error against a missing database, restoring it serves 200.
- **The reason was being thrown away.** The catch swallowed the exception silently, so an operator saw a blank shell or a 500 with nothing to act on. An installed site now writes one line to the server's PHP error log — `Quaeriva: settings unavailable ... | cause: SQLSTATE[HY000] [1049] Unknown database ...` — carrying the driver's own message. Nothing is added to the page, which is public; `Connection::connect` still reports only a generic message to callers.

### The home page answered 403 on a real subdirectory install

- **Fixed the 403.** A request for the site root is a *directory*, so the front-controller rule guarded by `!-d` never fired. Apache then looked for a DirectoryIndex, found no `index.php` in the project root, and `Options -Indexes` turned that into `403 Forbidden`. `/ask/install/` worked throughout because `install/` does contain an `index.php` — which is exactly the shape of the report. The root `.htaccess` now routes the bare root explicitly, and `tests/Public/task32.php` pins the rule.

### After installing, the installer said the install was already locked

- The success message was appended to the notice list, but the locked branch returned before notices were printed, so a completed install rendered the bare "This installation is already locked" — indistinguishable from a failure, with nowhere to go. Setup now ends on a success screen naming the site, linking to it and to the sign-in page, and telling the operator to delete `install/`.

### The panel can be moved off /admin

- `APP_ADMIN_PATH` in `.env` renames the admin segment. The router, the header link, `admin_url()`, the moderation notification links and `robots.txt` all follow it. An invalid value falls back to `admin` rather than locking the owner out, and a renamed panel refuses to answer on the literal `/admin` path that Apache would otherwise serve straight from the directory. Obscurity only — the password and role checks are unchanged.
- **Staff had no link to the panel at all** and had to know the URL. The header now shows one to administrators and moderators, absent from the markup entirely for everyone else.

### Configuration files

- `.env.example` and `config/database.local.example.php` carried development values (`127.0.0.1`, `quaeriva`, `root`) presented as if they were sensible defaults. Both now explain that the installer writes the real files, and state the shared-hosting facts that actually matter: `localhost`, account-prefixed database names, and never the MySQL root account.
- Installer base-path detection also strips a front controller from `SCRIPT_NAME`, so a server that reports `/ask/index.php` yields `/ask` rather than a path that routes nowhere.

### First clean-install rehearsal, and what it found

The installer had never once been run against an empty database in this environment. It has now been run end to end from the release archive into a fresh `quaeriva2`, twice, and the site served and signed into afterwards. Everything below was found by doing that rather than by reading code.

- **The installer now asks for the site name and description.** It previously collected database credentials, an administrator and a locale, so every new site was called Quaeriva until its owner found Settings. Both are written to `settings` during setup.
- **Settings → Site name never affected the public site.** The value was saved, loaded into the runtime site array, and then ignored by the layout, which read the `app.name` translation instead. Titles and `og:site_name` now use the configured name and fall back to the translation only when none is set; the meta description falls back through the site description before the generic intro. This bug predates the installer work and affected every site that ever renamed itself.
- **The chosen installer language did not become the site language.** `site.default_locale` was only ever written from the admin panel, so choosing English still produced an Arabic site. The installer now writes it.
- **The first administrator had no public handle.** Migration 020 backfills usernames, but migrations run before the account is created, so the owner was left with an empty `username` and a profile page that resolved to nothing. The installer now assigns one.
- The subdirectory path, `.env`, the database config, the lock file and the refusal of a second run were all confirmed against the extracted archive: `/ask/` serves, every generated link carries the prefix, and signing in reaches `/ask/admin`.

An omitted site name falls back to the default rather than blocking the install.

### Migration 020 failed on MySQL

The first install to get past the credentials screen died on `Migration 020_backfill_public_usernames.sql failed at statement 1: SQLSTATE[HY000]: General error: 1093 You can't specify target table 'u' for update in FROM clause`.

- The migration backfilled public handles with `UPDATE users u ... WHERE NOT EXISTS (SELECT 1 FROM users ...)`. **MariaDB accepts this; MySQL rejects it with error 1093.** Every machine this was written and tested on runs MariaDB, so the migration was broken for MySQL hosts from the day it shipped and nothing in this environment could have revealed it.
- The collision guard is now a `LEFT JOIN` against a derived table, nested twice so the optimiser cannot merge it back into the `UPDATE` and reintroduce the same error. Behaviour is unchanged and verified against a probe table: a free `member-{id}` handle is taken, one already claimed by another account falls through to the `u-{id}-{hash}` form, and existing handles are left alone.
- `tests/Database/task3.php` gains a check that scans every migration for an `UPDATE`/`DELETE` whose `SET` or `WHERE` contains a subquery reading the table being modified. It was confirmed to fail on the original statement and pass on the replacement, so this class of MySQL-only breakage cannot ship again from a MariaDB workstation.

Editing an applied migration is normally forbidden. It is safe here because the statement is idempotent and the file cannot re-run on an install that already recorded it — only fresh installs, which previously could not complete at all on MySQL, execute it.

### Installation fixed for real shared hosting

The first real installation attempt, into a subdirectory on live hosting, failed with `Installation could not be completed. Check the requirements and database details.` and nothing else. Four separate defects met at once.

- **The installer now says what actually failed.** The exception message was discarded unless an undocumented `debug` option was set, leaving the operator with a sentence naming two possible causes and no way to distinguish them. Driver errors are now mapped to instructions — a refused password, a database that does not exist, an unreachable host, a user without `CREATE` — with the underlying message retained in brackets. An installer is the one place where withholding detail from the server's owner helps nobody.
- **Database defaults now match shared hosting.** The form shipped `127.0.0.1`, `quaeriva` and `root`, which are local-development values: shared hosts almost universally want `localhost`, and prefix both the database name and the username with the account name. Host defaults to `localhost`; name and username now default to empty rather than to a plausible-looking wrong answer that submits successfully and fails obscurely.
- **The installer opens in English.** It defaulted to Arabic, which is the wrong first impression for an archive downloaded worldwide. Arabic remains one click away via `?lang=ar`, and the locale still governs the installed site.
- **Subdirectory installs work without hand-editing files.** The installer collected database credentials and nothing else, so a site uploaded to `/ask` installed and then generated every link against the domain root. It now detects the path from `SCRIPT_NAME`, offers it as a pre-filled editable *Site path* field, and writes `APP_BASE_URL` into `.env`, preserving any other keys already there. This closes the long-standing D2 gap.

`docs/INSTALL.md` gains the prefixed-name and `localhost` guidance where the credentials are entered, and four new rows in the troubleshooting table matching the new messages.

### Documentation

- Added `docs/INSTALL.md`, `docs/UPGRADE.md` and `docs/BACKUP.md`. There was previously no installation guide at all — only five lines buried in the README beneath the release notes for rc3.
- The install guide covers both hosting layouts and states plainly that with the document root at the project root, `.htaccess` is a security control rather than an optimisation, with four URLs that must be refused before the site goes public.
- Added `docs/THEMES.md` and `docs/ADVERTISING.md`, each ending with an explicit list of what the system deliberately does not do, so an operator can plan around the limits instead of discovering them.
- The README now leads with installation instead of release notes, and its documentation index is grouped by audience.
- Documented a tested fallback for hosts that cannot rewrite URLs: setting `APP_BASE_URL=/index.php` routes every generated link through the front controller explicitly, so the application runs on any server that runs PHP, with no code change. Verified by resolving `/index.php/q/slug` back to `/q/slug`.
- Documented three ways to point the document root at `public/` — the host's control panel, an SSH symlink, or falling back to the supported project-root layout — including the `FollowSymLinks` failure and what to do about it.

### Test suite is green

- **The suite now passes 58/58.** Two tests had failed on every correctly configured machine, and a permanently red suite trains people to ignore it. Neither assertion was weakened; both were testing valid intent through the wrong mechanism.
- `Bootstrap/task1` asserted the shipped database defaults against the *effective* configuration, which `config/database.php` deliberately overrides from `config/database.local.php`. It now asserts them only when no local override exists — and always asserts the `utf8mb4` charset, which is never overridable.
- `Support/task13` asserted that the API returns "service temporarily unavailable", so it passed only when the database was down. It now probes an unknown API route, which produces a translated error whatever the database is doing and exercises exactly the same locale resolution.
- Fixing the first uncovered a second, previously masked failure in the same file, which turned out to be output ordering in the test itself.

### Documentation cleanup

- Moved 23 superseded documents to `docs/archive/` — per-release upgrade and verification notes, completed implementation specifications, old audits, and `PLAN-MASTER.md`. `docs/archive/README.md` records why each is no longer current.
- `docs/` now holds only living documents: the five operator guides, the language and local-development guides, and the three active plans.
- The README leads with a release-candidate status notice and direct links to the guides, and its two long release-note sections are replaced by a feature summary plus an explicit "what it deliberately is not" — no plugin system, no browser theme upload, no build step, no ad tracking.

### Release hygiene

- Added `bin/package.php`, which builds the release archive from an explicit manifest and **refuses to build** when anything excluded would ship, or when any of the three `.htaccess` files is missing. A packaging checklist a person reads is not a control: a debug script that dumped API output survived a declared "final cleanup" and was found later only by inspection.
- Removed that debug script, and moved twelve release archives out of the project root. In the project-root document-root layout every one of them was downloadable.
- Migration `029` removes the ~45 settings rows superseded by the advertisements table, and `ads.head_code` superseded by custom code — stale copies of raw advertisement code that were re-read on every settings load. Live placement configuration is kept: the global switch, per-placement rotation, and the between-answers pacing.
- `agent.md` merged into `AGENTS.md`, now the single contract. Five superseded planning and handoff documents moved to `docs/archive/`, with a note recording why each is no longer current.
- Dropped the unused `AdminRepository` dependency from the admin overview page.

### Advertising

- **Device visibility is now decided on the server.** A placement hidden on a device previously still shipped its markup and provider script to that device and hid it with `display:none` — wasting the bandwidth of the visitors least able to afford it, and recording impressions the advertiser was billed for on screens where the ad was never visible. A non-matching placement now emits nothing at all. Verified: a mobile-only placement is absent from the desktop and tablet HTML source and present on mobile.
- Responses advertise `Vary: User-Agent` when any placement restricts itself by device, so a shared cache cannot serve one device's variant to another. The CSS classes remain as a second layer.
- Image advertisements accept explicit width and height, emitted on the `<img>` so the page does not shift as the creative loads.

### Custom code scoping

- Migration `027` adds page, language and audience scoping to custom code snippets. A snippet previously ran on every page for every visitor with no way to narrow it.
- Snippets can be limited to page types (`home`, `question`, `ask`, `search`, `taxonomy`, `profile`, `contact`, `auth`, `notifications`), to specific languages, and to signed-out or signed-in visitors. An out-of-scope snippet is **skipped entirely rather than emitted and disabled** — not shipping the code is the only form of scoping that means anything.
- Empty restrictions mean "everywhere", so existing snippets keep their behaviour across the upgrade.

### SEO and head metadata

- Added `og:site_name`, `og:locale` and an `og:locale:alternate` per enabled language. Open Graph requires a territory that a language tag does not carry; repeating the language is right for `ar_AR` and most others, so the handful where it is not (`en_US`, `ja_JP`, `zh_CN`, …) live in a small data map rather than as special cases in code.
- Added `theme-color` from the configured primary colour, and `apple-touch-icon` reusing the configured favicon — an operator who sets one icon expects it everywhere.
- **Public profiles are now in the sitemap.** They carry `ProfilePage` structured data and are linked from every question, so their absence was an oversight. Capped at 5000 and limited to active members with a public username.
- Corrected a misleading log message: sitemap generation failures were reported as "Homepage question loading failed".

### Renamed for accuracy

- The appearance tab called "Header and footer" is now "Page regions" — it lists nine regions including both sidebars and the home page body.
- Advertisement placements are named after the region ("Header", "Footer", "Home page") instead of "Header ad", which made the rotation checkboxes read as enable/disable switches for an advertisement.
- Advertisements can be enabled and disabled directly from the list, which is where an operator looks for it. The quick toggle runs the same placement-capacity guard as a full save.

### Advertising: several ads per placement

- Migration `028` moves advertisements out of the settings key/value table into `ad_creatives`. Each advertisement was previously a single string per placement, so a second one in the same placement was not a missing feature but structurally impossible.
- **One advertisement holds both an ad code and an image.** There is no type selector: the code is used when present, the image otherwise, so an operator can move a slot from a hosted image to an AdSense tag by pasting into one field.
- **Rotation, per placement.** With rotation off a placement runs one advertisement and enabling a second is refused with a message naming the one already running. With it on, eligible advertisements are drawn by weight — measured over 60 loads, weights 3 and 1 produced 44/16 against an expected 45/15.
- The rule is enforced twice on purpose: the admin guard explains it to a person, the renderer holds the invariant whatever wrote the row.
- Per-advertisement targeting by device, language and audience, plus optional start and end dates. An excluded advertisement is **absent from the HTML entirely** — never delivered and hidden.
- Existing settings-based configuration is migrated into rows, including the device flags, so nothing an operator set is lost.
- A placement with nothing eligible emits no wrapper element at all.
- Fixed a crash in the image renderer: the dimension attributes were passed to the escaping helper as integers, which threw a `TypeError` and — because ad rendering is wrapped in a `try`/`catch` — would have surfaced as an image advertisement silently failing to appear.

### Widget editing returns where it started

- Adding or editing a widget from the appearance layout tab used to leave the operator on the widgets screen after saving, with no way back to the region they were arranging except manual navigation. The originating screen is now carried through the form and the save returns to it.
- Only known destinations are honoured, so the parameter cannot be used to bounce an administrator to an arbitrary page. Saving from the widgets screen itself is unchanged.

### Widget ordering

- Widgets can be reordered inside a page region by dragging, with up/down buttons alongside because drag-and-drop alone is unusable by keyboard and awkward on touch. The order is held in hidden inputs and only saved on submit, so a mis-drop is undone by leaving the page.

### Appearance screen rebuilt

- The appearance screen is now tabbed — Branding, Colours, Type and layout, **Header and footer**, Custom CSS, Import and export — instead of one long form.
- **Added the header and footer manager.** Every region of the public page (header strip, navigation extras, home body, both sidebars, three footer columns, footer bottom bar) is listed in reading order with the widgets it currently holds, its enabled state, a direct link to edit each widget, and an add-widget link that pre-selects the region. An operator looking for "manage the header" no longer has to know these are widget sections and work it out from the widgets screen.
- A region with no widgets says so and keeps the built-in layout.
- **Saving one tab no longer resets the others.** The save handler previously rebuilt the whole appearance from the posted fields, so a partial submission would have snapped every absent value back to its default; keys the form did not post now keep their stored value.
- The colour picker and its hex text box stay in sync, so a brand colour can be pasted rather than eyeballed.

### Theme sidebars and translations

- **Wired up `home_sidebar` and `question_sidebar`.** Both were seeded as sections but rendered nowhere, so a widget placed in either silently vanished — the same dead-section defect the sections work was meant to remove. The home page and question page now render them, and fall back to the original full-width layout when the section is empty.
- A theme may ship `lang/<locale>.php`. Keys are forced into the `theme.` namespace so a theme can never redefine a core string, and database translation overrides still win, so an administrator can always correct a theme's wording. The locale name is validated before use, so a theme cannot reach outside its own directory.

### Theme direction support

- A theme may ship `assets/css/rtl.css` or `ltr.css`, loaded after its declared assets and only in that direction. A theme without one is unaffected.
- Added `tests/Public/task51.php`, which fails the build if any stylesheet — including a theme's — uses a physical CSS property (`margin-left`, `text-align: right`, `float`, bare `left:`/`right:`, physical border radii). The product ships RTL and LTR from one stylesheet, which only holds while every rule is logical; a single physical property is invisible in the language it was written for and silently wrong in the other.

### Widget configuration

- **Widget options are now real form fields.** The editor previously showed a raw JSON textarea, so configuring a widget required knowing the validator's key names and bounds — the worst piece of admin UX in the panel. Inputs are generated from a new `WidgetRegistry::fields()` descriptor, which sits beside `validate()` so a field cannot be added in one place and forgotten in the other.
- Every type's field group is rendered server-side and switched client-side, so changing the widget type needs no round trip. Inactive groups are disabled, so only the selected type's values are ever submitted.
- Number fields carry their real `min`/`max`, the category field is a proper category select, and a type with no options says so instead of showing an empty box.
- Validation is unchanged and still authoritative: an out-of-range limit and a `<script>` in an HTML block are both still rejected on save.

### Theme sections and managed menus

- Migration `026` seeds the page section keys the layout could never use before — `header_top`, `nav_extra`, `home_sidebar`, `question_sidebar`, `footer_col_1..3` and `footer_bottom`. Only `homepage_main` was ever read, so the header, footer and sidebars could not hold widgets at all.
- `PublicWidgetRenderer::renderSection()` renders the widgets in any one section. It returns nothing when a section is empty, which is what lets each region keep its built-in markup until an administrator actually configures it — an upgrade renders identically.
- The header top strip, footer columns and footer bottom are now widget-driven regions with that fallback.
- Navigation links moved out of the layout template into a managed `menu_items` table, seeded with the existing links so the rendered navigation is unchanged. Items carry either a literal label or a translation key, so seeded entries stay translated in every enabled language while administrator-authored ones use their own wording.
- Menu items support ordering, per-language visibility, new-tab links, and an audience rule (everyone / signed-out / signed-in / staff). A restricted item is absent from the HTML entirely rather than hidden with CSS.
- Menu URLs are validated with the same rules the rich-text formatter applies to user links: relative paths, fragments and http/https only — never `javascript:`, `data:` or `vbscript:`.
- New admin screen, Navigation menus, under Appearance. Admin-only, CSRF-protected, audit-logged against the menu item id.

### Administration sidebar

- Grouped the administration sidebar into four collapsible sections — Content, Community, Appearance, System — with Overview kept as a standalone link. Eighteen flat links were hard to scan; only one group is open at a time, the group holding the current page opens automatically, and the last opened group is remembered between pages. Built on native `<details>`, so the groups still open and close without JavaScript.
- A collapsed group surfaces the unread notification count on its header, so a notification is never hidden behind a closed section.
- Group membership is filtered against the viewer's allowed pages, so a moderator sees smaller groups rather than links that would be refused.
- Fixed `tests/Release/task27.php`, which was left pinned to `0.7.1-rc6` when `VERSION` moved to `0.7.1-rc7`, causing a failing test that was reported as environmental.

## [0.7.1-rc7] - 2026-08-13

### Custom code manager

- Added migration 025 with an idempotent `custom_code_snippets` table and a guarded import of the legacy `ads.head_code` setting without deleting the setting.
- Added admin-only Custom Code CRUD for `head`, `body_start`, and `body_end` snippets with CSRF, validation, prepared statements, audit resource ids, and raw trusted-code warnings.
- Decoupled custom snippets from advertising so disabling `ads.enabled` no longer removes analytics or other operator snippets from the page.

### Administration Questions UX

- Rebuilt the Questions management controls with category filtering, allow-listed sorting, page-size selection, active-filter reset, result-range summaries, and first/previous/next/last pagination links.
- Moved question filtering and pagination into prepared MariaDB queries so lists remain correct and bounded when the database contains hundreds of questions instead of stopping at the old 200-row query limit.
- Improved the Questions table hierarchy and responsive RTL/LTR layout for desktop, tablet, and mobile widths without changing moderation or SEO actions.

### Appearance and themes

- Added the admin-only Appearance manager with grouped branding, palette, shape, typography, layout, logo, favicon, and custom CSS controls; all values are validated again while rendering.
- Added offline-editable Quaeriva theme package export/import. Packages contain validated appearance settings and an installed theme name, while browser imports never extract PHP, JavaScript, templates, or executable files.
- Added the bundled `default` theme manifest and post-core theme asset loading with RTL/LTR manifest requirements, safe active-theme fallback, cache-versioned assets, and template override lookup.
- Added responsive admin styling and verified the Appearance manager and themed public shell in Arabic RTL and mobile/desktop layouts without horizontal overflow or browser console errors.

### Simplified advertising foundation

- Kept the initial advertising model intentionally small: one validated code or image creative per existing placement, responsive visibility switches, no new schema, no tracking, and no external ad-network dependency.
- Fixed between-answer placement so an ad is never emitted after the final answer, including when the interval is set to one.

### Languages and administration verification

- Fixed the single-language transition: enabling a second installed language now switches the site to multilingual mode in the same protected request instead of returning a generic failure.
- Replaced the language deletion placeholder with the exact blocker reasons: default language, fallback dependency, existing content reference, or last enabled language.
- Verified all 18 admin pages at desktop and mobile widths, including status responses, forms, responsive overflow, and browser console output; the full transactional admin write smoke test remains passing.

## [0.7.1-rc6] - 2026-08-13

### Behaviour change

- Inline escaped Markdown characters (`\\`, `*`, `` ` ``, `[` and `]`) now render as literal characters. Existing stored content is not migrated; only its rendered output changes when a backslash immediately precedes one of those characters.

### Editor and verification

- Replaced the Markdown-only question and answer controls with a zero-dependency WYSIWYG editor that keeps the submitted Markdown `textarea` as the server contract, supports full and comment toolbars, Markdown source mode, safe links, normalized paste, and RTL/LTR layout.
- Kept the editor submit hook compatible with browsers that do not implement optional chaining.
- Added `tests/Public/task50.php` for server-side render coverage and the required editor round-trip contract, including the Amendment A escape cases.
- Corrected the database and security contract tests to match migrations 023/024 and the actual safe `AuthService` return shape; no migration or stored-content rewrite was added for the editor.

## [0.7.1-rc5] - 2026-08-13

- Restricted language enable/disable and language-pack export actions to administrators, with CSRF protection on export links and translated missing-language errors.
- Added forward-only migration 023 for explicit language mode, layout-status repair, normalized question search columns, report-note compatibility, homepage widget reseeding, and status/query indexes.
- Added migration 024 for separate moderator report notes, preserving the reporter's original notes while allowing resolution context to be stored and reviewed.
- Added shared Unicode normalization for Arabic and related scripts, normalized search columns, multilingual duplicate detection, Unicode-safe truncation, and transliterated or encoded slugs.
- Removed locale-specific runtime forks in API, installer, and admin confirmation flows; installer language packs are now loaded from registered translation files with translated exception handling and fallback logging.
- Added resilient PHP-mail and dependency-free SMTP delivery, failure logging, verification-email resend flow, SMTP settings, and an administrator email delivery test.
- Completed administration workflows for pagination and list filters, question reopening/deletion, best-answer removal, user creation and verification, notification bulk actions, language deletion guards, nested-transaction-safe taxonomy writes, and safe taxonomy deletion.
- Added explicit API cookie CSRF/origin protection, scoped cookie-authenticated actors, public answer/comment pagination, wildcard-safe search, nonce-based rich-text previews, installer starter-category seeding, and documented trusted ad-header behavior.
- Fixed public header overflow at tablet-width desktop layouts and verified public and admin routes responsively in RTL mode.
- Translated audit action labels and redacted raw action identifiers from the administrator-facing audit details.
- Enforced same-locale category parent/child hierarchies and prevented parent locale changes that strand descendants.

## [0.7.1-rc4] - 2026-08-13

RC4 fixes the defects found while walking the running site as a visitor and as an administrator, and closes the shared-hosting gaps that stood between RC3 and a first-time installer being able to finish without SSH.

### Reported defects

- **Avatars no longer render at their intrinsic size on the question page.** `.author-chip` had no CSS rule in any stylesheet and the `<img>` carried no dimensions, so a member-supplied remote avatar rendered at its natural size — 2000×1200 in the demo data. Author avatars are now 36px on the question, 28px on answers and comments, with `object-fit: cover`, explicit `width`/`height`, and `loading="lazy"`.
- Added a global `img, svg, video { max-inline-size: 100% }` floor so no replaced element can overflow its container again, and gave the bundled default avatar SVGs explicit `width`/`height`.
- **Fixed the RC3 stylesheet variables that were never defined.** `--color-border`, `--color-muted`, `--color-primary`, `--color-surface` and `--color-surface-soft` were used across the community, widget and editor rules but declared nowhere, so every one of those `var()` calls silently fell back to `inherit`/`currentColor`. They are now aliased to the existing palette.
- **The answer and question editors now show a live preview.** The formatting toolbar still inserts Markdown, but the result is rendered beside the textarea as you type, so the formatting is visible instead of only its source. Side-by-side from 900px, Write/Preview tabs below that.
- The preview is rendered server-side by the same `RichTextFormatter` that renders published content, through a new authenticated, CSRF-protected `POST /preview` endpoint. There is deliberately no Markdown parser in JavaScript: the preview cannot drift from the published post, and no sanitiser is duplicated client-side.
- Pasting into an editor now inserts plain text only. Pasted page markup was the source of the stray navigation text visible in older demo content.
- **The question-language selector is hidden when only one language is enabled**, replaced by a hidden input so the submitted payload is unchanged, and the surrounding row collapses to a single column.

### Single-language mode

- Enabling exactly one language now reconfigures the whole site from that one decision: the visitor language switcher, browser-locale detection, the `?lang=` override and the per-question language choice all disappear, and the site locale collapses onto the enabled language. Previously these were four independent settings an administrator had to keep consistent by hand.
- The submitted `content_locale` is validated against the enabled languages on the server for both question and tag creation. The form is not a trust boundary: an absent, unknown or disabled value falls back to the site default.
- Administration → Languages explains single-language mode inline instead of leaving an administrator hunting for a switcher that is correctly hidden.
- Languages can be enabled and disabled directly from the table; previously the most common action required opening the full edit form.
- Language guard failures now say what is actually wrong — that a language is a default, or that another language still falls back to it — instead of a generic "operation failed". Exception text is still never rendered.

### Shared hosting and installation

- **Added a root `.htaccess`.** With the document root at the project root — the common cPanel layout, and one the README already presented as supported — `.env`, `*.sql`, `*.md` and the release archives were all downloadable, and every pretty URL and asset 404'd because the rewrite rules existed only in `public/.htaccess`. The new file denies the non-public paths and routes requests into `public/index.php`.
- **`.env` files are now actually read.** There was no dotenv parser anywhere in the codebase, so `.env.example` was decorative and `APP_BASE_URL`/`APP_LOCALE` were unreachable on any host that does not let you set process environment variables. Real environment values still take precedence.
- `config/site.php` resolves `APP_BASE_URL` through `app_env()` instead of calling `getenv()` directly, so it can no longer disagree with `app_base_path()`.
- `install/.htaccess` no longer emits Apache 2.4-only syntax unguarded, which returned 500 on every `/install` request on Apache 2.2 hosts, and its dotfile pattern is corrected.
- The installer now refuses to run when a requirement check fails. The checks were previously computed, displayed, and then ignored, so a host missing `pdo_mysql` could submit the form and receive an opaque exception.
- The installation lock always stays inside the project directory. It was previously written either to the parent directory — beside `public_html` on cPanel — or into `install/`, where only `.htaccess` protected it and nginx/LiteSpeed would serve it.
- Dropped the requirement check for `storage/`, a directory that does not ship and that no code path uses.

### Caching

- Static asset URLs are keyed on the release version **and** the file's modification time. Assets are served `immutable` for a year, so keying on `VERSION` alone meant any fix shipped without a version bump reached returning visitors as stale CSS/JS.

## [0.7.1-rc3] - 2026-08-13

RC3 completes the community-quality, database-recovery, profiles, discovery, advertising, editor and SEO pass requested after exercising RC1/RC2 against the real legacy/demo MariaDB schema. The historical items from `Quaeriva-CHANGELOG-conversation.md` were reconciled with the existing 0.6.x/0.7.0/RC1/RC2 sections so previously completed language, admin, routing, security and stabilization work is preserved without duplicating the same bullets.

### Database upgrades and recovery

- Separated **runtime compatibility** from strict migration health. A pending non-blocking migration no longer disables voting, following, reporting, tag creation or other public writes merely because `schema_migrations` is not fully current.
- Added one authoritative `DatabaseUpgradeService` shared by Administration and `bin/migrate.php`, protected with a MySQL/MariaDB advisory lock so two upgrades cannot run concurrently.
- Administration now shows a translated **Upgrade database now** action for compatible-but-outdated schemas; hard blocking remains only for missing tables/columns required by the running code.
- Added controlled reconciliation for deliberately idempotent stabilization migrations 012–019 when migration history and actual schema disagree after an interrupted/manual legacy upgrade. Repair never deletes community content and does not rewrite historical migration records.
- Preserved partial-migration recovery for MySQL/MariaDB prepared-statement cleanup, including harmless error 1243 on an already-released `DEALLOCATE PREPARE`.
- `bin/doctor.php` remains strict and reports every pending migration; `bin/migrate.php` applies/reconciles and then verifies schema integrity.
- Migration `019_locale_scoped_tag_names.sql` changes tag display-name uniqueness from global to `(content_locale, name)` while preserving globally unique public tag slugs.
- Migration `020_backfill_public_usernames.sql` gives legacy members stable public profile handles without overwriting existing usernames.
- Migration `021_seed_homepage_community_widgets.sql` adds the new community discovery widgets to existing published home layouts idempotently.
- Migration `022_seed_homepage_ad_widget.sql` adds an optional homepage advertising widget; advertising remains disabled until explicitly configured.

### Ask, categories and tags

- The Ask form renders the full category hierarchy instead of exposing only a small locale-filtered subset; selecting a category synchronizes the question content locale without silently hiding valid categories.
- Missing Unicode tags can be typed directly into the tag picker and created with **Enter**; exact existing tags are selected instead, the Ask form is never submitted by that Enter press, and the five-tag UI/server limit is retained.
- Tag creation is authenticated, CSRF-protected, rate-limited, locale-aware, Unicode-safe without requiring `mbstring`, duplicate-safe and rejects URL/control-character abuse.
- A pre-019 cross-locale tag-name collision returns a clear database-upgrade/locale-scoping validation message instead of a generic tag failure.
- Selected tags are rendered as crawlable public tag links on the question page.

### Voting and Q&A hierarchy

- Question owners are no longer offered self-voting controls.
- Repeating the same vote toggles it off; switching direction applies the correct atomic counter delta while keeping one vote row per user/target.
- Mutation failures return to the real GET question/content URL instead of POST-only paths such as `/vote`, preventing the misleading expired-form loop.
- Public discussion is answer-first: question-level comment composer/thread is removed from the public question flow, while short clarification comments remain attached to their answer cards. Legacy question comments remain available to moderation for compatibility.
- Accepted/best answers remain first, followed by other answers and the answer composer.

### Public member profiles

- Public author names/avatars link to `/u/{username}` when identity is public.
- Added deterministic local SVG fallback avatars for members without an uploaded image; no external avatar service is required.
- Profiles now expose join date, points, question count, answer count, accepted-answer count, received usefulness/upvotes, latest questions, latest answers and best/accepted answers.
- Legacy members receive stable public handles through migration 020 rather than disappearing from profile links.

### Posting & Quality

- Added centrally managed posting-quality rules for question title/body, answers and comments: minimum/maximum lengths, tag limits, new-tag limits, duplicate-content window, rate limits, banned words/domains and maximum external links.
- Browser and API writes share `PostingPolicyService` so limits cannot be bypassed by using another interface.
- Validation remains locale/Unicode aware and translation-driven rather than Arabic-specific.

### Homepage/community widgets

- Added optional/reorderable **Top contributors**, **Most viewed**, **Most useful**, **Most answered**, **Unanswered**, **Active tags** and bounded **Trending** widgets to the existing layout/widget engine.
- Existing published home layouts can receive the widgets through migration 021 without replacing administrator-created layout content.

### Advertising Manager

- Added centralized responsive Advertising management instead of hard-coded ad markup.
- Supported placements: header, footer, homepage widget, directly after the question, and between answers with configurable interval and maximum insertions.
- Each placement can be disabled, render trusted administrator-only AdSense/provider code, or render a responsive image with an optional safe link and required alternative text.
- Added a separate global provider/AdSense `<head>` code field so a network bootstrap script does not need to be repeated in every slot.
- Added per-placement desktop/tablet/mobile visibility controls while preventing horizontal overflow on small screens.
- Paid image destinations emit `rel="sponsored"` plus isolation attributes for external targets. Community content never enters the trusted raw-ad-code path; PHP tags are rejected from provider code.

### Safe lightweight editor and links

- Added a progressive, no-build-step editor for questions and answers with bold, italic, semantic H2/H3-style structure, ordered/unordered lists, blockquote, inline/code blocks, link insertion and undo/redo controls.
- Ordinary users cannot inject raw HTML, scripts, iframes, arbitrary font sizes/colors or extra page H1 headings; rendering is centralized through the safe rich-text formatter.
- Answer comments intentionally remain lightweight rather than becoming a second article editor.
- External user-generated links are limited by Posting & Quality policy and render with UGC/nofollow/isolation attributes; unsafe schemes such as `javascript:`/`data:` are rejected. Root-relative internal links remain normal and base-path aware.

### SEO and discovery

- Removed visible question-slug/reference noise that added no value to readers or search engines.
- Question meta descriptions fall back to useful text extracted from the question body rather than the generic site description.
- Expanded `QAPage` data with publication/modification dates, vote count, answer count, accepted/suggested answers and crawlable author profile URLs.
- Added `ProfilePage` structured data for public member pages and breadcrumb UI/`BreadcrumbList` where a category path exists.
- Kept canonical, robots/noindex, sitemap and base-path generation centralized; internal links remain domain/mount agnostic.

### Moderation, reports and administration

- Reports identify the reported question/answer/comment, show the linked question/context and reporter notes, and provide direct inspect/open actions instead of only numeric IDs.
- Reporter notes are preserved and are no longer overwritten by moderator status changes.
- Administration topbar includes recent unread notifications in addition to the sidebar inbox.
- Database-upgrade notices and actions are localized and noindex; administration continues to avoid exposing raw SQL/stack traces publicly.
- Database/bootstrap failures now log the underlying exception server-side with a short reference ID while the administration UI shows only the safe localized reference, preventing raw SQL/driver messages from leaking into recovery screens.

### UI, RTL/LTR and performance

- Forms, tag chips, editor controls, advertisements, profiles, widgets, tables and administration remain responsive across phone/tablet/desktop and use locale-driven RTL/LTR behavior.
- Versioned static assets and long-lived immutable caching/compression from RC1 remain in place.
- The supplied Lighthouse runs showed strong FCP/LCP and 100 scores for Accessibility/Best Practices/SEO in the tested runs; they also explicitly warned that Chrome extensions affected performance measurements, so extension-origin JavaScript is not treated as application code.

### Governance and verification

- Added root `AGENTS.md` plus `agent.md` entry guidance requiring multilingual/RTL/LTR behavior, portable links, SEO, security, forward-only migrations, responsive design, Q&A semantics and a `CHANGELOG.md` update in every functional change.
- Added regression contracts for database runtime compatibility/self-upgrade, interrupted migration recovery, category/tag behavior, vote recovery, profiles, Posting & Quality, community widgets, advertising, rich-text/link safety and SEO.
- PHP/JavaScript/database-independent verification is required on the packaged artifact. Real MySQL/MariaDB fresh-install, legacy-upgrade and write-flow verification remains mandatory before promoting RC3 to a Stable release.

## 0.7.1-rc3-tagfix — 2026-08-12

- Added create-on-Enter tags on the Ask page. Existing exact tags are selected; missing tags are created asynchronously and immediately selected without submitting the question.
- Added `TagCreationService` with authentication, enabled-locale checks, Unicode-safe length validation, URL/control-character rejection, duplicate reuse, and globally unique slug generation.
- Added CSRF-protected and rate-limited `POST /tags/create` JSON endpoint.
- Added migration `019_locale_scoped_tag_names.sql`: tag display names are unique per `content_locale`, while public slugs remain globally unique for `/t/{slug}` compatibility.
- The tag picker now renders even when the database has no tags, allowing creation of the first tag.
- Added schema-health checks for the new locale/name and slug indexes.
- Added regression coverage for no-submit Enter behavior and multilingual tag creation without mbstring.

## [0.7.1-rc2] - 2026-08-12

This release candidate fixes runtime and Q&A UX defects found while exercising rc1 against the user's upgraded MariaDB database.

### Runtime and forms

- Fixed `/settings/profile` fatal error caused by passing the `noIndex` boolean as the `structuredData` argument to the shared layout renderer.
- Hardened the Ask tag picker so Enter always prevents native form submission before selecting a tag.
- Public question pages now hydrate and render selected tags.
- Corrected content-direction handling for Arabic/RTL comment and answer composers.

### Q&A hierarchy and spam controls

- Reworked comments as subordinate discussion attached to the question or to a specific answer rather than a standalone content block competing with answers.
- Question comments are collapsed by default; answer comments remain in their answer cards; accepted answers continue to sort first.
- Added comment burst, hourly user, and hourly IP limits plus recent duplicate-comment rejection and a maximum of three links per comment.
- Added reporting for comments and duplicate-active-report suppression for question, answer, and comment reports.

### Moderation and notifications

- Enriched the Reports screen with the linked question title, reported content, reporter notes, a detail inspector, and a direct public target link for questions, answers, and comments.
- Report status changes no longer overwrite the reporter's original notes.
- Added a recent unread notification center to the administration topbar while retaining the sidebar inbox.
- API report creation now supports comment targets and suppresses duplicate active reports.

### Lighthouse follow-up

- Fixed the public brand accessible-name mismatch reported by Lighthouse.
- Retained rc1 versioned static-asset caching; the supplied Lighthouse runs explicitly warn that Chrome extensions affected load performance and the dominant unused JavaScript is extension code.

### Verification

- Added Q&A UX/moderation and report-consistency regression contracts.
- PHP/JavaScript static checks and database-independent suites pass in the build environment.
- MySQL/MariaDB-backed suites still require the user's PHP CLI with `pdo_mysql`; the build container has no PDO database drivers.

## [0.7.1-rc1] - 2026-08-12

This release candidate stabilizes Quaeriva 0.7 against legacy/demo MySQL schemas and hardens the Q&A write paths before a public 0.7.1 release.

### Database and runtime stabilization

- Added `SchemaHealthService` to compare the connected database with all bundled migrations and required runtime tables/columns.
- Administration now stops with a controlled **Database upgrade required** response instead of executing repositories against an outdated schema and producing SQL fatal errors.
- Public authenticated write paths and API mutations are schema-guarded before persistence.
- `bin/migrate.php` verifies schema integrity after applying migrations; `bin/doctor.php` reports pending migrations and missing schema state; `bin/admin-smoke.php` refuses an outdated schema before repository queries.
- Updated MySQL migration expectations through migration `018` and added a synthetic 001–011 → current upgrade/idempotency integration test.
- Migration execution errors now include the migration filename and statement number while preserving the original exception as the cause.

### Q&A fixes

- Generic error pages now preserve authenticated context: an already signed-in user receives a return action instead of a misleading Login button.
- Added a visible question action bar for Follow/Unfollow, Report, and Share; guests see the actions and are redirected through login with a return URL where authentication is required.
- Replaced raw multi-select tags with a progressive searchable chip picker supporting Enter-to-select, removable chips, locale filtering, and a five-tag UI limit without exposing labels such as `[ar]`. Server-side taxonomy/locale validation remains authoritative.
- Replaced broad OR-only duplicate suggestions with Unicode normalization, weighted similarity, thresholding, optional locale restriction, and category/tag boosts.
- Added character n-gram fallback so duplicate suggestions also work for languages that do not separate words with spaces; no category-specific or Arabic-only ranking rules are required.

### Performance

- Versioned public/admin CSS and JavaScript URLs with the central application version.
- Added long-lived static-asset caching and Apache compression directives, plus equivalent version-aware caching in the PHP development router.

### Verification status

- PHP syntax, JavaScript syntax, and database-independent regression/contract suites pass in the build environment.
- MySQL-backed suites remain mandatory before promoting this RC to stable; they require a PHP CLI with `pdo_mysql` and a reachable MySQL/MariaDB server.

## [0.7.0] - 2026-08-12

Quaeriva 0.7.0 completes the Core Q&A Plus release and turns the 0.6.x stabilization work into a usable multilingual open-source Q&A product.

### Q&A and community

- Activated authenticated question creation with category/tag/language validation, anonymous display option, duplicate suggestions, moderation status, CSRF, and rate limiting.
- Completed comments on questions and answers, question/answer reports, answer acceptance, vote protections, and moderation controls.
- Added question/tag/category follows, member notification preferences, notification inbox/read state, and taxonomy-follower notifications.
- Added dedicated filtered search for text, category, tag, answered state, date, member, and content language.
- Added public category/tag pages and richer public member profiles/activity.

### Administration and operations

- Reworked category, tag, user, report, language, message, layout, section, and widget management around compact tables and explicit actions.
- Completed hierarchical-category editing, including converting an existing root category to a child through the root checkbox/parent selector while rejecting cycles and mixed-locale ancestry.
- Split settings into General, Membership, Community, Maintenance, Mail, Notifications, and SEO tabs.
- Added registration enable/disable, optional email verification, maintenance mode, PHP mail settings, contact-message management, and testable administrator delivery through internal notifications/email/Telegram.
- Added administrator audit and moderation history backed by migration 018.
- Connected published `homepage_main` layouts/widgets to the public homepage renderer.

### Languages and RTL/LTR

- Preserved the single-URL interface-language model while supporting arbitrary registered language codes and RTL/LTR direction.
- Added AI/translator-friendly JSON language-pack import/export with stable keys and placeholder preservation.
- Hardened RTL search controls throughout public/admin interfaces while keeping user-content direction automatic and technical values direction-safe.
- Enforced question taxonomy/content-locale consistency in both the form and persistence layer.

### API

- Added scoped, hashed API tokens with one-time raw-token display, revocation, expiry, and last-use tracking.
- Implemented question, answer, comment, vote, accept-answer, follow, report, notifications, and `/me` API operations.
- Expanded question-list filters and allowed question detail lookup by slug or numeric id.
- Kept `/api/v1/auth/*` explicitly outside the 0.7.0 API contract.

### Security and deployment

- Wired rate limits into login, registration, password recovery/reset, Q&A writes, contact, duplicate suggestions, and API writes.
- Added baseline frame/content/referrer/permissions response headers and noindex administration markup; HTTPS responses add HSTS.
- Added audit logging for administrator mutations and member API-token creation/revocation without storing raw IP addresses.
- Kept root deployment as the default and arbitrary subdirectory deployment through one `APP_BASE_URL` value; no hard-coded `/ask` mount remains.
- Added/updated completion and hardening contract tests and GitHub Actions MySQL coverage.

### Upgrade notes

- Upgrade existing 0.6.x databases by backing up first, then running `php bin/migrate.php`.
- Do not re-import the demo database and do not edit migrations already recorded in `schema_migrations`.
- 0.7.0 applies forward migrations through `018_create_audit_moderation.sql`.

All notable changes to Quaeriva are documented here.

## [0.6.4] - 2026-08-12

This patch makes the application mount neutral by default and completes the administration collection-management UX pass requested before 0.7.0 feature work.

### Deployment paths

- Removed the historical `/ask` application-mount default. An unset `APP_BASE_URL` now means the web root.
- Arbitrary subdirectory deployments remain supported through one centralized setting such as `APP_BASE_URL=/forum` or `APP_BASE_URL=/community`.
- The public `/ask` route remains only the semantic **Ask a question** page; it is no longer a required prefix for the application.
- Updated the built-in-server, test, CI, and deployment documentation so no specific subdirectory name is assumed.

### Administration collections

- Reworked Categories into a compact management table with Create, Edit, Add Child, and Delete actions plus one contextual editor instead of one full form per record.
- Existing root categories can now be reassigned to a parent from Edit; descendants are excluded from the selector while repository cycle and same-locale validation remain authoritative.
- Reworked Tags, Users, Reports, Installed Languages, Layouts, Sections, and Widgets into management tables with row actions and single-record editors.
- Added user Edit, Activate, Suspend, Ban, and Delete operations with protection for the current administrator and the final active administrator.
- Added layout/section/widget edit and delete operations. The Widgets page now states explicitly that this subsystem is experimental and is not yet consumed by the public renderer.
- Questions and Answers now use dedicated management-table styling; the Questions status filter preserves the `page=questions` route on GET submission.

### Verification

- Added/expanded admin UX contract coverage for root-path defaults, category reassignment, user-management protections, and layout/widget CRUD contracts.
- Extended the MySQL-backed admin suite to cover converting an existing root category to a child, final-admin protection, user status/deletion, and layout/section/widget persistence.
- Database-independent Bootstrap, Support, Public, Admin, Release, SEO, and language regression suites pass in the review environment. MySQL-backed suites require PDO MySQL/MySQL and are left for CI or the user's local database.

## [0.6.3] - 2026-08-12

This patch turns the language hotfix into an administrable multilingual subsystem and strengthens the local/open-source administration workflow.

### Language and translation administration

- Added migration `012_create_language_management.sql` with the language registry, database translation overrides, and per-user admin locale preferences.
- Added **Administration → Languages** with separate public/admin default locales, public switcher visibility, browser-language detection, enabled languages, native names, RTL/LTR direction, fallback language, sort order, and translation editing.
- Bundled `resources/lang/ar.php` and `resources/lang/en.php` remain source packs; admin edits are stored separately so package upgrades do not overwrite local translations.
- Additional normalized language codes such as `de`, `fr`, `ur`, `pt-br`, and `zh-cn` may be registered without changing the routing model. Missing translations use the configured fallback instead of creating a second content URL.
- Admin UI locale is now independent from public UI locale and persists in `user_preferences.admin_locale` plus the admin locale cookie. Legacy admin `&lang=` parameters are removed from dashboard URLs.
- Regional browser locale detection now attempts an exact enabled locale before falling back to the base language, and `LocaleContext` normalizes underscore/case variants such as `PT_BR`.

### Category hierarchy

- Exposed the existing `categories.parent_id` schema in the administration UI instead of presenting categories as a flat list.
- Added parent selection, full hierarchy paths, descriptions, sort order, and same-locale parent filtering.
- Added server-side protection against self-parenting, hierarchy cycles, missing parents, mixed-locale parent/child chains, and parent locale changes that would strand children.

### Local development and upgrades

- Added `public/router.php` for PHP's built-in server so a mounted base path such as `/ask` serves `/ask/assets/...` correctly.
- Local development now starts from the project root with `php -S 127.0.0.1:8003 -t public public/router.php`.
- Moved machine-specific database credentials to untracked `config/database.local.php`; the installer now writes that local file instead of overwriting tracked `config/database.php`.
- Added `bin/migrate.php`, `bin/doctor.php`, and `bin/admin-smoke.php`; write-mode admin smoke checks run transactionally and roll back their test changes.
- Added a System page to expose version, project/base path, PHP/PDO MySQL state, connected database, migration status, and enabled languages without displaying credentials.

### Open-source verification

- Added database-backed regression coverage for language persistence/fallbacks and hierarchical category rules.
- Updated migration tests for the 0.6.0/0.6.2 eleven-migration upgrade path to apply only migration 012.
- Added `.github/workflows/ci.yml` to lint and run the test suite on PHP 8.1 and PHP 8.5 with MySQL 8, followed by the transactional admin smoke test.
- Added dedicated language-management, local-development, and roadmap-status documentation.

### Compatibility

- No existing migration 001–011 is modified. Existing 0.6.x databases upgrade forward by applying migration 012.
- 0.7.0 feature-schema work must therefore begin at migration 013 or later.

## [0.6.2] - 2026-08-12

This patch completes the language-switching hardening pass before 0.7.0 feature development.

### Complete Arabic/English switching

- The selected UI locale now survives refresh, navigation, search query strings, anchors, authentication flows, and browser back/forward cache restores.
- Public language controls now use a CSRF-protected POST action and redirect with HTTP 303 to the same language-agnostic URL.
- Legacy GET `/lang/{locale}` and `?lang=` behavior remains compatible but normalizes back to the clean shared URL.
- The switch route keeps a one-year first-party `quaeriva_locale` cookie scoped to the configured application base path.
- A logged-in language change updates both the current session and `users.preferred_locale`; a newer guest cookie is synchronized to the account after a successful login.
- When the administrator hides the language switcher, the configured primary site locale is enforced regardless of older visitor cookies or browser language.

### RTL/LTR and stale-page protection

- `LocaleContext` remains the single source of truth for UI language and `lang`/`dir` output.
- Search/query parameters are preserved when switching language, while canonical URLs remain query-clean and language-neutral.
- JavaScript preserves URL fragments before switching and detects stale back/forward-cache pages without becoming the server-side locale authority.
- Localized/auth-aware HTML now returns `Cache-Control: private, no-cache`, `Content-Language`, and `Vary: Accept-Language` to prevent shared-cache language leakage.
- Updated language controls retain their link-like styling in desktop, mobile, and footer layouts after conversion to POST forms.

### API locale consistency

- API locale negotiation now follows an explicit supported `lang` parameter, then supported `Accept-Language`, then the administrator/configured default locale.
- API startup reads the runtime default locale from Settings when the database is available.
- Question-not-found API errors now follow the resolved Arabic/English API locale.

### Verification

- Expanded `tests/Support/task13.php` with same-URL query handling, CSRF switch controls, session persistence, forced-primary mode, API locale negotiation, cache headers, and back/forward-cache contracts.
- Expanded `tests/Public/task6.php` with POST language switching, query/fragment preservation, CSRF rejection, and JavaScript fragment preservation.
- PHP syntax and all database-independent suites pass in the review environment. Database-dependent suites remain unexecutable here because no PDO MySQL driver/server is available; they are unchanged except for the new prepared user-locale update path.

## [0.6.1] - 2026-08-12

This hotfix stabilizes bilingual routing and protects runtime settings before the 0.7.0 feature work.

### Language and URL consistency

- Fixed locale persistence so a visitor-selected cookie is not overridden by browser `Accept-Language`.
- Added administrator settings for the primary public locale, language-switcher visibility, and optional browser-language detection.
- Browser-language detection is disabled by default; when the public switcher is disabled, the configured primary locale is enforced.
- Language switching now persists in a cookie and redirects to the same clean public URL instead of appending `?lang=`.
- Legacy `?lang=` requests normalize to the clean URL.

### Settings safety

- Split the destructive shared `settings_save` handler into `site_settings_save` and `seo_settings_save`.
- Saving site settings no longer clears SEO fields; saving SEO no longer resets the site locale/name/description.

### Duplicate URL cleanup

- `/questions` now permanently redirects to the single home question-list anchor.
- Removed the duplicate `/questions` sitemap entry and navigation link.
- Kept legacy `/questions/{slug}` compatibility redirects to `/q/{slug}`.

### Verification

- Added `tests/Support/task13.php` for locale persistence, clean language URLs, switcher visibility, settings separation, and duplicate-route normalization.
- Updated public and SEO tests for the clean shared-URL language policy.

## [0.6.0] - 2026-08-11

This release adds the first complete SEO administration layer for the bilingual Arabic/English Q&A application.

### 1. Canonical public URLs

- Added the short, readable `/q/{slug}` question namespace for public links, APIs, templates, and sitemaps.
- Added permanent `301` redirects from the legacy `/questions/{slug}` detail URL.
- Added duplicate-question `301` redirects to the published original question.
- Kept answer and vote POST routes compatible with both namespaces while new links use `/q/`.

### 2. Public SEO surfaces

- Added dynamic `robots.txt` with protected admin, installer, and API paths excluded.
- Added dynamic `sitemap.xml` containing only published, indexable, canonical question URLs.
- Added per-page `robots`, canonical, Open Graph, Twitter card, Google verification, and Bing verification metadata.
- Added schema.org `QAPage` JSON-LD only for published questions that have visible answers.
- Added centralized site description and default SEO title handling with Arabic/English direction preserved.

### 3. Advanced SEO administration

- Added global SEO settings for default title, site description, robots policy, sitemap enablement, Open Graph image, Twitter/X handle, and search-engine verification tokens.
- Added per-question SEO controls for meta title, meta description, canonical URL, and `inherit`/`index`/`noindex` policy.
- Added migration `011_add_question_seo_fields.sql` for the new question-level fields, written to be safe on an already migrated database.
- Added validation for SEO lengths, canonical URL format, indexing policy, and global robots values.

### 4. Verification and documentation

- Added `tests/Seo/task12.php` for canonical URL routing, robots/sitemap/layout contracts, and sitemap indexability rules.
- Fixed the PHP built-in-server router so CSS, JavaScript, fonts, and images are served as static assets instead of being swallowed by the front controller.
- Preserved the selected Arabic/English administration locale across sidebar navigation and post-save redirects.
- Preserved the selected public Arabic/English locale in the language-switch redirect URL as well as its cookie.
- Verified the local shared-host-style runtime at `http://127.0.0.1:8130` for the new question route, legacy redirect, robots file, sitemap, QAPage JSON-LD, settings page, and question SEO editor.
- Kept the single root `VERSION` file as the release source consumed by the public layout, administration, installer, and API response headers.

### Known boundaries for this release

- Hosted deployment, HTTPS, rewrite rules, and production mail transport still require verification on the target shared host.
- Reserved API write/auth routes remain structured `501` responses until their token and permission contract is introduced.
- The local/demo mailer remains `NullMailer`; production email delivery must be configured before enabling email-dependent workflows for real users.

## [0.5.0] - 2026-08-11

This release completes the local/shared-host administration and verification pass for the Arabic/English RTL/LTR Q&A application.

### 1. Central release identity

- Added the root `VERSION` file with the release value `0.5.0`.
- Added `app_version()` with semantic-version validation and a single-file source of truth.
- Connected the same value to `config/site.php`, the public footer, the administration panel, the installer, and the `X-Quaeriva-Version` API response header.
- Added a release contract test so a future template cannot silently drift to a different version.

### 2. Public application and localization

- Verified the responsive public home, question list, question detail, ask, login, registration, password recovery, email verification, profile, logout, answer, vote, and language-switch routes.
- Verified Arabic markup uses `lang="ar" dir="rtl"` and English markup uses `lang="en" dir="ltr"`.
- Kept shared-host compatibility: PHP/PDO/MySQL, no Composer, Node, Redis, or required external service.
- Kept `utf8mb4` database handling for Arabic and English content.

### 3. Administration and moderation

- Verified overview, questions, answers, categories, tags, users, reports, settings, and widgets pages.
- Verified question publish, close, duplicate, and rejection paths, including rejection-reason validation.
- Verified answer acceptance/deletion controls, category/tag create-edit-delete, user role/status/locale updates, report status updates, site name/default locale settings, and widget enable/disable plus safe HTML validation.
- Preserved the self-protection rule that prevents an administrator from removing their own active administrator access.

### 4. Security and deployment checks

- Verified CSRF-protected browser mutations, authenticated answer/vote flows, member/admin authorization boundaries, and HTML sanitization for widget configuration.
- Verified the installer lock and shared-host setup assumptions without modifying the existing `quaeriva` database.
- Added an mbstring-free answer preview fallback for hosts where `mbstring` is unavailable after installation.

### 5. Verification performed

- PHP lint completed for all application, installer, API, admin, and test files.
- Existing task suites completed for bootstrap, support, database, security, questions, public routes, admin, API, and installer behavior.
- Added and passed `tests/Release/task11.php` for the central version contract.
- Browser/runtime checks covered anonymous visitors, registered members, administrators, Arabic/English switching, RTL/LTR rendering, moderation, settings, widgets, and API responses against the local demo runtime.

### Known boundaries for this release

- The browser application is the supported write surface. Reserved API write/auth routes intentionally return structured `501` responses until a token and permission contract is introduced.
- The default local/demo authentication mailer is `NullMailer`; production email delivery must be connected to the host's mail transport before enabling email-dependent workflows for real users.

## [0.4.0] - 2026-08-10

- Added a dependency-free shared-host installer with Arabic and English setup screens.
- Added requirements checks for PHP, PDO MySQL, mbstring, JSON, sessions, writable paths, and database reachability.
- Added atomic installation locking, first-administrator creation, password hashing, and safe database configuration writing.
- Added open-source deployment, security, contribution, and upgrade guidance.
