Tripvance — Changelog
=====================

The recent releases in full, older ones in one line each. The current
release is also listed in readme.txt, which is what the plugin directory
shows.

= 2.8.10 =
Guided setup: an optional four-step wizard for a new site — default currency, tour and activity addresses, default designs, review. Updating an existing site changes nothing: no address, price, currency, design or setting is written, the wizard does not open, and no invitation is shown. Nothing is migrated.

* Where: Tripvance → Settings → Setup wizard (a tab of the Settings group; a submenu entry under Tours with the classic menu). No new top-level menu, no redirect, nothing blocked before or without it. Capability: manage_options, the same as the settings screen; every form carries a nonce.
* Invitation: "Welcome to Tripvance" with Start setup and Later, on the Plugins screen and Tripvance screens, only when every piece of evidence says the site is new: the install profile recorded at first activation is "new", there is no tour, activity, transfer or booking in any status, the settings screen has never been saved, the homepage has no configuration and the setup guide's welcome was never answered. The wizard's own marker is not evidence; its absence on an updated site means nothing. It replaces the setup guide's welcome notice, so a new site is greeted once. Reactivation writes nothing.
* Steps: progress indicator, Back, Continue and "Exit without saving". Choices live in a per-user draft (transient, 12 hours) and are validated on the server at each Continue and again, all together, at Apply settings. Opening the wizard from a link or the menu starts from the values the site uses now.
* Storage: no new settings system. default_currency, layout and activity_presentation are the settings screen's own keys; tour_base and activity_base are two new keys on the same wptm_settings option, read by rewrite_slugs() through chosen_bases(), and carried over by sanitize_settings() so a later settings save keeps them. Absent on every existing site, so rewrite_slugs() returns what it always did.
* Apply: one merge into wptm_settings through the new merge_settings() (a single update_option, so all or nothing), re-read and compared before "Tripvance setup is complete" is shown; on failure the stored array is put back and the review says so with the choices kept. A lock option and a one-time token make repeated presses safe: a second press while the first runs waits, one after it lands on the result.
* Currency: EUR, USD, GBP, MAD (default_currencies(), now also what the settings screen lists), with a price example from Pricing\format_decimal(). No price is converted. Read-only while a tour or activity without wptm_price_currency, a transfer without its own currency, or a booking without _wptm_price_currency exists, since each is read in the default.
* Addresses: tour (single, list of all tours, and the destination/type/category/departure/duration bases under it) and activity (single, and the list when that archive is on). Editable only with pretty permalinks, addresses per language off, no code changing the segments through wptm_rewrite_slugs, a "new" install profile, no content in any status, no terms in the tour or activity taxonomies and no menu link to them; otherwise shown read-only with the reason. Refused with a message beside the field: wrong format (2–40 lowercase letters, digits, single hyphens — nothing is rewritten silently), the same word for both, WordPress reserved terms and query vars, rewrite bases, Tripvance's other routes and legacy bases, a language code, an existing page or post, another post type's or taxonomy's base, any existing rewrite rule that is not one of the moving tour and activity rules. Post type and taxonomy names, labels, existing slugs, WordPress permalink settings, Polylang settings and SEO plugin data are not touched. After a change, wptm_rules_stale is set and the rules are rebuilt once on the next admin request, after init has registered the new bases; the result screen checks the stored rules and offers Rebuild permalinks if they are missing. No redirect system is added.
* Design: tour layouts from Layouts\picker_items() (legacy ones only when in use) and the three activity layouts, as radio cards with the existing CSS miniatures. The review counts tours without wptm_layout and activities without wptm_presentation that will follow the new default.
* Assets: assets/css/setup-wizard.css and assets/js/setup-wizard.js (ES5, no dependencies), loaded on the wizard screen only. Logical properties, RTL, keyboard (native radios and buttons), phones.
* Fix: the setup guide's "Use this design" saved the settings array through the settings sanitiser, which on a site whose settings were never saved wrote every other switch as off — activities among them. It now merges.

Setup wizard and menu icon corrections (same version)
* Eligibility: SEO\new_install() is a historical flag and no longer decides anything in the wizard (its meaning is unchanged for the rest of the plugin). A new option, wptm_setup_eligible, is written once by the activation hook, before anything else in it: 'yes' when the database holds no option, post or term of Tripvance or of the build it was renamed from (wptm_*, mtt_*), otherwise 'no'; an existing record is never rewritten. Updates write nothing, so an updated site — including one whose content was deleted — never qualifies. Completing the wizard sets it to 'used', so reopening it shows the addresses read-only. tour_base / activity_base already stored are read as before. The content, term, menu and conflict checks stay, and every step is validated again at Apply settings.
* Step indicator: steps already reached are submit buttons of the wizard form (form attribute), so the open step's fields reach the draft first, as with Back and Continue; going back keeps what was typed without checking it, going forward checks the open step, and nothing beyond the furthest step reached is offered. Works without JavaScript; a hidden first submit makes Enter in a field mean Continue (it used to mean Back).
* Drafts across tabs: every form carries the draft id and its revision; handle() refuses a form from another draft or an earlier revision without touching the draft and shows the latest state with an explanation. Expiry and the repeated Apply press keep their handling. Opening the wizard from the menu keeps a draft with unapplied choices (with a notice and "Start again from the current settings") instead of discarding it silently.
* Concurrent changes: the draft records the wizard values the site used when it began. At Apply, a value changed elsewhere since then that the draft would overwrite stops the apply; the review lists the value now and the choice made, with "Keep the current values". Settings the wizard does not write are never a conflict and are kept by the merge.
* Currency: PricingEngine::code() is the engine's test (trimmed, upper-cased, three letters), now used by PricingEngine::currency() itself and by the wizard. Tours, activities and transfers are judged by their distinct stored values (one query each) and counted with a meta query; bookings are those Payments\expected() accounts in the default (no snapshot, accepted quote or revision), read a page of ids at a time. No price or currency is changed.
* Failure reporting: "nothing was changed" is said only when the stored settings are unchanged or the undo was read back.
* Menu icon: it was an SVG data URI with a black fill that WordPress's svg-painter script recolours after the page has loaded. menu_icon() now returns 'none' and assets/css/admin-menu.css (every admin screen, in the head) draws the same "T" as a CSS mask painted with currentColor, so normal, hover, focus and current follow the admin colour scheme from the first paint, expanded or folded.

Draft concurrency, booking currency check and the Operations board (same version)
* Draft locking: handle() takes a per-user lock (SetupWizard\with_draft_lock(): one INSERT IGNORE row, 15 s time to live, waited for up to 5 s, released in finally) around reading the draft, checking its id and revision, changing it and saving it; Back, Continue, the step indicator, Keep the current values and Apply settings all go through it. A request that cannot get the lock changes nothing and says so. The Apply lock uses the same atomic claim instead of add_option(), which is an INSERT … ON DUPLICATE KEY UPDATE and let two callers both "win". The draft is read past this request's option cache. screen() no longer writes the draft when a step is only viewed; it writes only to start a draft, to restart one, or to replace one with nothing unsaved whose starting values no longer match the site — under the same lock.
* Booking currency: SetupWizard\booking_currency_source() follows Payments\expected()'s order — a revision newer than the accepted quote, the accepted quote, a valid snapshot, the default — without calling it (expected() reads a damaged snapshot's currency directly and would raise a notice). A snapshot is valid only as an array with a total key and a currency PricingEngine::code() accepts; missing, empty, a:0:{}, damaged serialisation, no currency, invalid currency all count as the default. A revision or quote with a valid currency wins over an old snapshot; one whose currency cannot be read locks the step as "cannot confirm". Every booking is scanned two hundred ids at a time with meta primed, with one query per page for which have an accepted quote; an exception locks the step.
* Operations board (includes/operations/board.php, assets/css/operations.css on that screen only): title, description and manifest buttons in one header; "On this page" after it (flex order), wrapping on a wide screen and scrolling inside itself below 783px; headings land below the toolbar and the sticky bar. admin.js marks the current section with the bar's real height when it is stuck and wraps (unchanged on screens where it is one row), marks the section jumped to at the foot of the page, and sets aria-current. Trips show trip and tour, date, customer and party, pick-up, crew (guide, driver, vehicle) and payment state, with amber badges for "Pick-up not set", "Nobody assigned" and "Awaiting verification", as labelled cards on phones (the table no longer carries .wp-list-table, whose phone layout hides cells). Empty states are a calm line per section ("No trips today.", "No trips tomorrow.", …). Queries, statuses and links are unchanged.


= 2.8.9 =
Site pages and the page builder, reworked around what an owner does with them: find a page, make one in the right language, edit it without losing anything, and publish it on purpose. No page is deleted, recreated, published, added to a menu or made the front page by this update; content, IDs, addresses, templates, translations and SEO fields of existing pages are not touched. No option, table or meta key is migrated.

Tour editor: title and Inclusions (same version)
No meta key, field name, stored format, save rule or front-end output changes; nothing is migrated.
* The title is above every section instead of inside Basics: WordPress's own title block (the same #title input, permalink box and nonce) is moved, as markup, before the page is sent (output buffer between edit_form_top and edit_form_after_title in Experience\Editor), under a "Tour title" / "Activity title" label. Nothing is copied or moved by script, so switching sections never hides, re-creates or resets it; it scrolls with the page (no sticky). If WordPress's markup is not where expected, it is printed unchanged.
* Inclusions in three groups: Highlights with its design under it; "What’s included" (✓) and "What’s not included" (−) in two equal columns with the same help line; What to bring on its own row. Groups come from a `group` key the native panel renderer reads (Experience\Schema\inclusions_layout()); only widths, rows, the two labels and help change.
* What to bring: eight everyday suggestions, the rest behind "Show all suggestions" / "Show fewer suggestions" (aria-expanded; without JavaScript all are shown). A suggestion already in the list — its key after a "|" or its exact name, ignoring spacing and case — is marked added (aria-disabled, a tick, screen-reader text) and pressing it again changes nothing; marks follow typing, pasting and deleting. A new line is only ever appended; focus stays on the button pressed.
* Fix: switching sections left keyboard focus inside the hidden description editor (WordPress resizes it on the resize the section switch announces, and TinyMCE focuses an editor for every command); focus taken that way is handed back.
* Fix: a section without side boxes kept WordPress's 300px side margin (the rule was weaker than WordPress's own), so the boxes were narrower than the title above them.

Bookings list page (same version)
No data, meta key, hook, capability, price, payment or booking status changes; nothing is changed by searching, filtering or viewing.
* Header: Bookings and Add booking (the one primary button) on one line, out of the status views; one short waiting notice with "Show new requests"; status views in their own wrapping row with clear counts and the current one marked. WordPress's post-status views (Mine, Published, Pending, Draft) and post-status labels ("Pending" beside a completed booking: a leftover of the save bug fixed in this version) are no longer shown on this list; the posts themselves are untouched and every booking stays under All.
* One filter panel: search (reference, name, e-mail or phone), the daily shortcuts, then Status, Trip, Travel dates (one from–to control) and Payment with visible labels; Booking type, Travel month, Source and Request received (WordPress's own "All dates", which is the date the request arrived, now named) under Advanced filters, open whenever one of them is in use. The quick filter is carried by the form, so applying a filter or a search keeps it; sorting and quick filters keep the search and filters; Clear filters keeps the sort. Travel-date filters narrow each other, and a combination that leaves no day is said so. Bulk actions and the count sit against the table.
* Table: shorter rows, the e-mail and country on one line, wider date and money columns, badges of one size (status filled with a dot, payment outlined, source neutral), words wrapping inside their cell on tablets, filters shown on phones (WordPress hides them). Money: the total and its source ("Estimate", "Agreed", "Revised") or "No price yet", then the ledger's state with "Paid …", "Due …" or "Overpaid by …" from Payments\summary().
* Filter panel tightened: on a wide screen the search and the quick filters share one row, the four main filters and Filter the next, then one line with Advanced filters, what is applied and Clear filters. Advanced filters is a button (type="button", aria-expanded, aria-controls; it never submits) with an arrow that turns, showing "(n)" only for advanced filters actually applied and valid; it opens on load when one is applied and closes without clearing. Without JavaScript the button is hidden and the panel stays open. Clear filters appears with any search, filter, quick filter, status view or the Bin, and links to the plain list (sort and view mode kept, first page, All).
* Fix: Screen Options on the bookings list answered only clicks on its top edge — the list's positioned page wrapper was painted over WordPress's floated tab — so it seemed to open once and lag the next time. The tab is lifted above the wrapper; every visible control on the booking screens is now checked to receive its own click.
* Bookings filter panel: the Advanced filters arrow is now drawn (SVG) and centred beside its label instead of a font icon that could sit lower on some systems; every button, field and chip in the panel shares one height, radius and focus ring (36px on desktop, 44px touch size from 782px down), the panel is white, field labels line up, and Clear filters carries a small × and turns red on hover.
* Laid out with CSS grid placement and display: contents over WordPress's own markup — nothing moved by script — and scoped to body.edit-php.post-type-wptm_booking.

Admin bar labels (same version)
* The admin bar said "View Posts" on the Tours and Activities lists and "View Post" on a tour, because those post types had no view labels. Tours, Activities and Transfers now carry their own (View Tours / View Tour, View Activities / View Activity, View Transfers / View Transfer) as registration labels: WordPress's admin bar reads them and links to get_post_type_archive_link() or the item's permalink, as before. No slug, rewrite, archive setting or ID changes; Posts, Pages and other post types are untouched; with an archive switched off WordPress still shows no archive link.

Bookings dashboard, list and booking screen (same version)
No table, option, meta key, price, payment, seat, status rule, message or document behaviour changes. Viewing, filtering and Quick view read only; nothing is sent or changed by them.
* Dashboard in working order: search (reference, name, e-mail or phone), Add booking, a More actions menu for adding tours, activities, guides, drivers, vehicles and departures; four figures — Requests to answer (status New), Follow-ups due (date today or earlier, open bookings), Trips today and Trips tomorrow (not cancelled or declined) — each counted by the same query the bookings list runs for its link (Booking\list_query_vars()), so a figure and its rows cannot disagree; Needs attention; Upcoming trips (the list's Next 7 days); the calendar; and the week's figures, request sources and most requested trips in a folded section. The old Today box, which repeated the figures, is gone; the pipeline counts now come from status_counts(), the same as the list's status links.
* Needs attention: rules keyed and ranked (attention_reasons(), no translated text compared): unconfirmed_soon (new or contacted, travelling within 7 days; today or tomorrow ranks highest), crew_missing (a confirmed transfer within 7 days without a driver or fleet vehicle, asked only when the agency keeps them — tours and activities are never asked for a guide or a driver), payment_claim, followup_due, no_reply (new for 2 days or more), balance_open (a confirmed trip within 7 days with a balance on an agreed total — open, never "overdue": there is no due date). Sorted by rank, then travel date, then follow-up date; one action per row opening the right panel; further reasons in a disclosure; View all opens the list filtered to exactly these bookings. The previous "Trip team not assigned" for every confirmed trip is removed.
* Quick view on the list (row action) and the dashboard: admin-ajax GET with a nonce and edit_post on that booking, rendered on the server from get_booking(), context(), Payments\summary(), the Guest Pass meeting point, the attention rules and next_steps(). The panel is emptied before each request and late answers are dropped; failure shows Try again; Escape, a close button and the backdrop close it; focus is kept inside and returned to the button; the page behind does not scroll. Without JavaScript the button is a link to the booking.
* Bookings list: quick filters (Today, Tomorrow, Next 7 days, Follow-up due, Needs attention) that combine with every other filter, sort and page; booking type now also Tours, Activities and Custom trips (Transfers and Tours and activities kept); payment state from the ledger's index (_wptm_payment_state, written by Payments\settle(), which now also runs when a customer reports a payment or accepts a quote); the applied filters, each removable, and Reset filters; search also matches WhatsApp numbers and phone numbers typed with other spacing; a payment tag drawn unlike the status pill; on phones each booking is a short card with name, trip, date, status, Open booking and Quick view. "All bookings" on a booking returns to the list as it was filtered (kept per tab; only the list's own arguments, checked again by the server).
* Booking screen: "Save changes" everywhere it saves the page, with descriptions that say exactly what it saves. After saving, one notice reports each part — status, trip details, follow-up, internal notes — and names a part that was refused, with a link to its panel, instead of WordPress's generic line and separate notices. Changed fields are marked until saved (in the existing change listener; the duplicate one is removed). Action buttons say what they do: Record payment, Issue PDF / Issue revised PDF, Preview draft PDF, Save assignments, Mark as sent, Retry sending, Email the guest link, Create quote draft.
* Styles scoped to the plugin's classes; the dashboard's share bars use classes instead of style attributes; logical properties throughout, no letter-spacing on right-to-left headings.

Site pages: Move to Trash (same version)
* Each page in Tripvance → Pages has a ⋮ menu with "Move to Trash", so a page no longer has to be opened to be removed. A confirmation names the page and says it can be restored; Cancel or Escape changes nothing. On success the row leaves the list, the count follows (and an emptied page of the list says so), and a notice offers Undo, which puts the page back with the status it had (published, draft…), not WordPress's default of Draft.
* WordPress's own wp_trash_post() and wp_untrash_post() only: no permanent deletion, no SQL; content, builder settings, SEO meta, images and Polylang translation links stay. Translations are never trashed with a page.
* Checked on the server at every request: the nonce, delete_post for this page, that it is a page, listed here, not already in the Trash. The front page and the posts page of Settings → Reading (and their Polylang translations, when Polylang is active) are refused with a note to choose another one first; with the Trash turned off (EMPTY_TRASH_DAYS = 0) the action is refused rather than becoming a permanent delete.

Builder start (same version)
* Fix: opening the Homepage or a site page in the builder first showed the plain form — the Order boxes, the tick boxes and "Adding and removing sections needs JavaScript" — then swapped it for the builder. Cause, measured: the builder's scripts are footer scripts behind the media library's, so the browser painted the parsed form first (about 0.1 s on a fast line, over 2 s on a slow one); it never waited on images, the preview frame or the page's load event. Now a small inline script marks the builder as starting the moment its layout opens, and the stylesheet keeps the form laid out but out of sight behind an outline of the builder (aria-busy, "Loading the builder…"). builder-canvas.js ends it the moment it has drawn itself. No fixed delay.
* If a builder script cannot be loaded or throws while starting, or the page finishes loading without the builder, the plain form is shown under a notice with Try again and Use the simple form. Without JavaScript none of this runs and the plain form is there as before. Fields, saving and page data are not touched.

Builder refinement (same version)
Four small tools in both builders (Homepage and Site Pages); nothing changes on the site until a page is saved, and a page saved before looks the same after.
* A name for each section, for the owner only ("Desert trips", "Marrakech activities"), typed in the section's header. Empty means the section is called by its type, and the type stays visible under a name. Stored where each screen already keeps a section's state: a homepage row's `label` (custom HTML keeps its existing Name field), a page block's `metadata.name` — WordPress's own block name, which the block editor's Rename writes and shows, and which nothing prints. Never on the page, in SEO or in schema; capped at 80 characters, cleaned on save. A form that does not send the field (an older open tab) keeps the stored name. Carried in exports, saved templates and imports; a file made before keeps the homepage's names as they are.
* A one-line summary on every section card, read from its own fields only — no request, no query: the type (when the section has a name), its design, "shows up to N" (what is asked for) or "N chosen" / "N items" (what is actually filled in, never a guess at results), and whether its photograph and focal point are set. Hidden sections keep their badge.
* Focal point picked on the photograph: click it, move it with the arrow keys (Shift for steps of 10), or type Across and Down in percent; "Back to the default" changes nothing else. Stored in the existing Focal point field as "x% y%" (whole numbers, 0–100, physical so never mirrored in RTL) and printed as object-position; the file is never changed and the crop and shape are not touched. Named choices (top, bottom, start, end) and every saved value are kept. Where: Luxury hero, Our purpose and Full-bleed story on the homepage; the Luxury page sections that already had a focal point; and, new, the standard homepage hero's cover photograph (`hero_focus`) and the Site Pages hero section (`focus`), both empty by default. Destination and tour images are not affected.
* Copy style / Paste style: copies only the choices on a section's Design and Spacing tabs (and on the homepage its spacing and background, plus its own layout choices such as columns and heading position). Never words, photographs, links, chosen tours or terms, code, the name, the place, the hidden state or the focal point. Pasted on a section of another kind, only the shared settings whose value fits are applied, and its design or layout family is never carried over; a homepage setting shared by two sections is never changed. Nothing fits: a message and no change. One undo step; kept in the editor's memory only, never the system clipboard.
* Fix: the Undo, Redo and Preview buttons of the light top bar were white on white once enabled.
Assets keep version 2.8.9; their cache stamps follow the files, so the new scripts load at once.

Builder stability and Our purpose (same version)
Causes proven with a long browser session using human clicks (the mouse moves a few pixels while pressed), each fixed at its source:
* Clicks lost to dragging. Every section row and library card was draggable as a whole, so a click with about 5 px of movement became a drag and the click never arrived — select, move, hide, duplicate, remove and the library's Add all "did nothing" now and then. Only the handle (rows) and the picture-and-words area (library cards) drag now; buttons only click. Before the fix the soak test failed 11 of 17 steps on the homepage and 8 of 10 on a site page; after it, none.
* Buttons replaced under the pointer. The list was redrawn 60–250 ms after every edit and redraw removed and re-created every "Add a section here" gap — and, on site pages, every row. A press that started just after typing lost its click, and a keyboard user lost focus after Enter on a row. The list now only moves the nodes that are out of place; gaps are created once and kept.
* Two settings panels at once. A removed section's panel kept its "current" mark; brought back by Undo it showed beside the selected one. Which panel shows is now derived from the selection every time the inspector is drawn.
Hardening (not observed failing, closed anyway): dialog answers that arrive after the dialog was closed or moved on are dropped; the import field is emptied after each choice so choosing the same file again works; error lines no longer pile up; a hidden section's duplicate is one undo step; the save request gives the button back after 90 seconds without an answer, with a message that says the save may or may not have been stored (the conflict token keeps a retry safe); the library drawer (narrower screens) closes on a press outside and on Escape; the notice has a close button; fields reached with Tab stop below the sticky toolbar; an empty repeating table no longer shows its column headings on phones, and a filled one scrolls inside its box with readable headings.
max_input_vars: verified, not changed — a plain save of a long homepage to a server with max_input_vars=120 is refused whole (nothing stored) and the JSON save to the same server stores everything.
Our purpose (Luxury homepage and page block)
* Commitment columns get equal room: the space between columns is a gap with the rule drawn in its middle. Before, padding on all but the first column gave the first one about 23 px more for its words.
* Titles start on one line and sentences on another in every row (subgrid, unchanged), checked with titles of one to four lines, two, three and five commitments, at 1024 and 1440 px and in RTL; stacked on tablets and phones with no spare space.
* "Larger" photograph: about half the width (506 px against 364 px at 1440) at the same 4:5 shape and crop; the commitments become rows beside it — title, then its sentence, aligned in two columns — instead of three columns squeezed to two or three words a line. "Current size" stays the default; no saved page changes size.
* The photograph's `sizes` now match the width it is shown at in each arrangement, so a sharp file is chosen without an oversized one.
* Not done, waiting for the client: the Royal Mansour photograph (not supplied — choose it in the Photograph field) and which icon to change (the section prints no icon on the site).

Section builder (homepage and Site Pages)
The homepage and the page builder are now composed from a section library instead of a fixed list of sections to show or hide. Nothing changes on the front end until a page is saved from the new builder; an unsaved homepage renders exactly as before.
* Library on the left (Introduction, Tours & activities, Destinations, Content, Trust, Contact), each card with a short description and a small drawing of the section, a search, and Add or drag into place. Settings of sections that are not on the page are not shown.
* The canvas lists only the sections on the page, in their real order: drag to reorder (with an insertion line), "Add a section here" between sections, and for the keyboard the move buttons and Alt+Arrow keys. Selecting a section opens its settings in Content, Design and Spacing tabs, beside a preview that follows the same order.
* Section actions: move up/down, duplicate (custom HTML on the homepage; on site pages every section the page may hold more than once), hide for now, remove. A removal can be undone from the notice; Undo/Redo cover add, remove, reorder, hide and settings, 30 steps, for the current visit to the screen.
* Homepage storage: a `layout` list in wptm_home, one entry per section {id, type, on, space, ground} plus `layout_version`. The old `sections` table is still written, derived from the layout, so code that reads it keeps working; `layout_sig` (a fingerprint of that table) lets the builder notice when something else changed the table — a preset, the Luxury switch — and take those changes in instead of overwriting them. Until the first save from the builder the layout is derived from the old table on read, without writing anything.
* Custom HTML is no longer limited to five slots. Each block has its own id, optional admin label and code under wptm_home[blocks]. The five old slots become blocks that keep their keys (custom_html_N), content, order, visibility and translations; their code is stored again only when it is edited. Code is filtered as before for users without unfiltered_html, and unchanged existing code is never re-filtered.
* Before the first save in the new format the previous wptm_home is copied to wptm_home_legacy_backup (not autoloaded, never deleted, also kept on uninstall). If the copy cannot be written the save is refused and nothing changes.
* Per-section spacing (compact, normal, spacious) and background (keep, white, paper) on the homepage; spacing for every Tripvance block on site pages. Hiding a block on a site page sets `wptmHidden`; the block stays in the content and is skipped on the front end.
* Design: three call to action layouts (band — the existing one —, split with an image, card), alongside the existing hero, featured, destination, team, testimonial, why and gallery styles. A "Page design" panel sets accent and secondary colour, background, content width, spacing, button and image corners for the homepage (wptm_home[design]) or one site page (`_wptm_page_design`); empty values inherit the global settings and the theme's fonts; a section's own choice wins over the page's.
* Starter layouts — private tours, luxury, desert & adventure, local guide & day activities, transfers — set the order and styles of sections. Applying one shows what it will do (the new order, sections taken off, kept custom HTML, styles changed) and can be cancelled; it changes the form only, and nothing is stored until Save. No reviews, numbers, bookings or pages are created.
* Save as template, export and import as JSON (format "tripvance-builder", version 1). Exports leave out bookings, customers, keys and site settings; image, tour and term references are listed so the import can say which do not exist on this site. Imports are checked for format, version, size (512 KB), types and allowed values, never fetch a URL, fill the form only, and keep the page's own texts unless asked to replace them.

Fixes
* Starter pages were stored with damaged block settings. wp_insert_post() unslashes what it is given; the starter's markup — JSON attributes with " escapes, a term description with quotes, a typed backslash — was passed unslashed, so `"` came out as `u0022` and backslashes vanished. Creation now slashes title, content and meta once. The Luxury demo pages had the same fault and get the same fix. The page builder's SEO title, description and keyword are slashed for update_post_meta() too.
* Builder saves with JavaScript failed every time. builder-admin.js sent the JSON save to `form.action`, and the form holds an input named "action" (admin-post's), which shadows that property: the request went to "[object HTMLInputElement]" and the screen said the server could not be reached. It now reads the attribute. This affected the page builder, the homepage builder and the car hire builder.
* Templates are kept. The layout radios used to write `default` whenever "Full width" was not ticked, so saving a page that had a theme template or the landing template moved it to the default one. The template now changes only on an explicit choice; a custom one is shown by name with "Keep" selected. The save and status calls also pass an empty page_template, so wp_update_post() no longer re-validates the stored template after writing — which answered "Invalid page template" (reported as a failed save that had in fact happened) for any template the active theme does not list, and without the error flag reset it to the default.
* Status changes are checked. Only draft/pending → publish, scheduled → publish now (date set to now, or WordPress would schedule it again), private → publish and publish → draft are accepted, each with the post's own capability (`publish_post`), a check that the status shown is still the status stored, and the conflict token; the final status is read back, and a draft with a future date that WordPress scheduled is reported as scheduled, not published.

Creating a page
* The language comes first, and the starter's title, headings and notes are generated inside `Lang\in_language()` for that language, then the language is assigned. A typed title is never translated, and pages are never linked as translations on their own.
* Destinations and tour types are searched as you type (`wp_ajax_wptm_pages_terms`, twenty at a time, the whole taxonomy reachable, Polylang's `lang` argument), replacing the first-200 select. Without JavaScript the field takes an exact name or slug, resolved in the page's language. A term in another language is refused on the server (`term_lang`).
* Changing the language clears a choice made in the other one, and says so.
* A page already made from the same starter, for the same term and language, is pointed out with a link, before creating (`wp_ajax_wptm_pages_existing`) and on the builder's "created" screen. Only plugin metadata counts: `_wptm_starter`, and for new pages `_wptm_starter_term` and `_wptm_starter_taxonomy`. Titles are never compared, and older pages get no inferred metadata.
* A refused attempt keeps the typed values for the same user for ten minutes (transient `wptm_pages_retry_{user}`) and reopens the form filled in. The button locks after the first press, and a per-form token makes a repeated POST open the page already made instead of making a second.
* Starter cards show the page's sections in order, read from the starter's own content. The title is suggested from the destination or type and never overwrites a typed title.
* The starter FAQ answers and the call to action's sentence are now bracketed notes to replace; nothing promises flexible itineraries or a payment policy. Pages already made keep their words.

The list
* The list is the screen, at full width, with "Create a page" opening the form on request. Pages come from the sources that own them: the homepage and the car hire page with every Polylang translation, the index pages, the landing pages and the starter pages, each ID once.
* Search by title, kind, language and status are applied by the database before the page of results is cut; 20 per page with the count; last modified; the language and links to the real Polylang translations (missing ones marked); Edit opens the editor that owns the page (homepage and car hire builders, the WordPress editor for index and landing pages, the page builder otherwise), plus View/Preview and WordPress settings; actions the user may not take are not shown.
* "Other pages with Tripvance sections" finds ordinary pages holding Tripvance blocks (filtered in PHP, no NOT IN); "Add to Site pages" writes only the marker.
* Empty states, and set-up buttons for the homepage, car hire and index pages when they do not exist yet. On a phone each page is a card.
* The screen's script and styles moved out of admin.js/admin.css into site-pages.js/site-pages.css, loaded on this screen only.

The page builder
* Content and Page settings & SEO are two tabs over the same form (every field is still saved); a panel opened from the navigation, the preview, an invalid field or a returning save brings its tab forward. Arrow keys move between tabs, mirrored in right-to-left.
* Closed sections show the first words in them. Each block with an Add template gets Duplicate in its header (a new identity; uid, src and sig of the original are never copied). The move arrows keep the keyboard on the moved section and announce its position. Panels are numbered within their tab.
* The status card shows the real status — Draft, Pending review, Private, Scheduled for {date}, Published — and one action that fits it. Save buttons say "Save draft", "Update the published page", "Update the private page" or "Update the scheduled page", with a sentence on what that means for visitors and that every save stores the whole page.
* Publishing with unsaved changes saves first and publishes on the server only after the save was stored whole; a conflict, a truncated request, a failed write or an invalid field publishes nothing.
* "Before publishing" lists, from the saved page: bracketed notes left in Tripvance sections, headings and text; addresses a visitor cannot follow; buttons with text and no address; WhatsApp or contact buttons with no number, telephone or e-mail in Settings; more than one H1 among the plugin's sections and headings, or one H1 on the theme layout. It says what it does not read (theme header, footer, menus, other plugins' blocks) and never blocks publishing.
* Full-width pages: an optional visible page title (`_wptm_show_title`) when no section is the main heading; off by default, so existing pages look as before. Ordinary blocks between sections share the sections' width, gutters and vertical rhythm.
* Rank Math keeps sole ownership of its title, description and focus keyword; nothing is written to them or duplicated.
* Phones: the builder's bars are not sticky and never cover content; the section strip and the section search no longer widen the screen; the preview mode is chosen by a media query, not innerWidth.

Builder polish (same 2.8.9 build number)
* Fix: the homepage builder never used the protected JSON save. Its search-field boxes are named `search_fields[]`, which the save's form-to-JSON fold refused, so every homepage save fell back to a plain POST: a failed request left the screen, and no recovery snapshot was ever kept. The fold now appends `name[]` values as PHP does; the homepage now saves, refuses and recovers like the page builder.
* Fix: Enter in the new "Find a section or a setting" box searches; it never submits (and saves) the form.
* Fix: moving a section with the arrows in the layout table kept no keyboard focus; focus stays on the arrow and the new position is announced. A move now updates the bar's "Unsaved changes" at once (`wptm:dirty`).
* Navigation: grouped into General settings, Page sections (in the order the page prints them, re-sorted as sections move), Appearance (the presets) and Translations (the language tabs); hidden and unsaved sections say so in words. On a phone it folds into one button.
* Find a section or a setting: titles, field labels and sub-headings, including inside closed panels and Advanced folds; fields of a layout not currently chosen are not offered. Going there opens the panel and the fold and focuses the field below the sticky bars (`scroll-margin`). Nothing typed is changed.
* Closed panels show the first words in them; adding a hidden section opens it with a visible note; section offers carry a local dashicon.
* Preview: says whether it shows your unsaved changes (only you) or the saved version; desktop, tablet and phone are laid out at 1280, 768 and 390 px and scaled to fit, so the page meets the same breakpoints as on the device.
* Translation tabs: each box is marked "Translated" or "Not translated — the page shows the source text".
* Phones: panel Save rows are not sticky, the layout table drops its description column, the navigator's rows no longer break words letter by letter.
* Our purpose: the commitments use a CSS subgrid, so titles share a line and sentences share a line in every row, whatever the title length, font, language or width; browsers without subgrid keep the previous layout. New "Photograph size": Current size (default for every saved homepage and page section) or Larger, same crop and shape (`lux_purpose_media`, section arg `media`).
* Moroccan Editorial Carousel: the corner geometry motif and the drawn flourish are removed, with their CSS. The palm and kasbah drawing stays. The "Which" choice is gone; a stored value is carried unchanged in a hidden field, and a band set to one of the removed drawings now draws nothing (said on the screen) rather than being switched to the palm.

Code: new assets/css/site-pages.css and assets/js/site-pages.js; functions `SitePages\outline()`, `existing_pages()`, `managed()`, `query()`, `PageBuilder\readiness()`, `change_status()`, `notices()`, `save_label()`; constant `SitePage\SHOW_TITLE`. Minified copies rebuilt.

= 2.8.8 =
Booking workspace: the edit-booking screen says where a booking stands and what to do next, groups its panels into five sections, and protects unsaved changes. No table, option, meta key, price, status rule, message or document behaviour is changed; nothing is migrated or repriced.

* Summary: reference and saved status, customer with the email, phone and WhatsApp the booking holds (the preferred one marked), the tour, activity, transfer or trip, date and start time, travellers. The price is named after its source — "Agreed total (accepted quote)", "Agreed total" for a revision, "Price at request" for the snapshot, "On request" — and payment state, received and balance appear only when the payment ledger exists. The latest quote and its state are shown; accepting a quote is not shown as paid or confirmed. Everything is read from saved data, so the summary changes only after saving. An old booking with no party shows "Not set", not "0 adults".
* Next action: one step worked out from the saved status and data by a short list of rules (`Booking\next_steps()`, a plain function of `Booking\workspace_facts()`): review a new request; follow up a sent, expired, declined or draft quote; review an accepted quote before confirming; set the travel date; mark a past trip completed; add the meeting time or place the guest will see (read through the Guest Pass, including the tour's meeting point and a transfer's pickup); assign a driver or fleet vehicle — on a transfer only, and only when the agency keeps drivers or vehicles. Up to three short notes follow: a requested date that has passed, a guide, driver or vehicle assigned elsewhere at the same time, a balance due, an email not delivered. Cancelled, declined and completed bookings are never asked to prepare anything; with no rule matching, a neutral sentence. Its button only opens the panel. Filters: `wptm_booking_workspace_facts`, `wptm_booking_workspace_sections`.
* Sections: Customer & request, Price & quote, Trip preparation, Messages, documents & Guest Pass, History & internal notes, and All panels. One section is shown at a time, starting with the one the next action points to; a section that needs attention is marked with "!" and a screen-reader label. Status, Publish and any panel from another plugin stay visible in every section. The section is kept across Save booking; an action that returns to a panel, or prints its result inside one, opens that panel's section. A required field in a hidden section or closed panel is opened and focused instead of blocking the save silently. Without JavaScript every panel is on the page and the section links jump to them.
* Unsaved changes: "Unsaved changes" beside Save booking once a field is edited; leaving the page, following a link or running an action asks first. Save booking warns when a panel with its own button (Payments, Quote, crew…) holds typed values it will not save. Buttons that send, issue or record carry a bar on their leading edge and are locked with "Working…" after the first click, so a double click sends one request; previews in a new tab and document downloads are not locked. Server-side duplicate checks are unchanged.
* History: one timeline, newest first, in the main column — the booking's log (status, trip edits, quotes, documents, payments, crew, Guest Pass), the emails in the outbox with their delivery state, and WhatsApp messages marked as sent, each labelled with its source. Times in the log are converted from site time correctly. Nothing is reconstructed for older bookings; internal notes stay in their own panel.
* Phones: the section links scroll in one row; the header's document buttons are two to a row again (a 2.8.6 rule was overridden).
* Code: the booking panel navigation moved from admin.js to booking-admin.js with the workspace; new file `includes/booking/workspace.php`. Minified copies rebuilt.

= 2.8.7 =
Settings screen reorganised, and stability fixes found by a full pass over the admin screens, the front end in every tour layout, both experiences and right-to-left. No setting, option, table, price or booking behaviour is changed.

* Settings screen reorganised: one section navigation instead of two — a column beside the settings on a wide screen, grouped under Core, Booking, Content & pages, Public & localization, Setup & integration and Advanced, with "Find a setting" at its top; on a tablet or phone the same list opens from the section button in the toolbar. The sections follow that order on the page, and the content uses the width the navigation used to leave empty. One Save button, in a slim toolbar that stays in reach without covering the section headings it scrolls to. Nothing about the settings themselves changed: the same fields, names, values, defaults and save.
* Fix: Settings printed a PHP warning ("Undefined array key 0") in Section pages when a hierarchical section had terms without tours. The example link now uses the first term returned, whatever its key.
* Fix: System status was laid out as one row, because its wrapper shared the class of the status badge; the diagnostic report was squeezed beside the cards and the page scrolled sideways. The report now sits below the cards at full width.
* Phones: the Site pages list and the Section pages table in Settings scroll inside their own region instead of widening the screen, and the Site pages actions wrap.
* Code: the WPML hooks used for translation are marked as WPML's own for Plugin Check, a file-scope loop variable carries the plugin prefix, and the rate-table cache key is built with wp_json_encode() instead of serialize(). The booking screen's stylesheet and script now ship with minified copies like every other asset.
* readme: the 2.8.3 upgrade notice is shortened to fit the 300-character limit.

= 2.8.6 =
Visual polish of the booking screens, the Guest Pass and the PDF documents. No change to how bookings, prices, seats, notifications, documents or the Guest Pass work; no data, option or table is changed.

* Add booking: the agreed total and its currency sit together in one highlighted panel, the amount larger and the currency a narrow code; status and "Send the usual emails" are each a tile, the ticked one marked; more space between rows and their descriptions; the button fills the width on a phone. The error list is indented on the correct side in right-to-left admin.
* Booking screen on a phone: the page no longer scrolls sideways. The Guest Pass map-pin pair and the Trip content image and itinerary fieldsets now shrink to the screen.
* Money fields: "Total to offer" and "Deposit asked for" (Quote) and "Amount" (Payments) are wide enough to show the full figure, with the currency beside them; the balance in Payments is larger; the invoice card shows "Not issued yet" / "Issued as …" as a small label.
* Right to left: no letter-spacing on Arabic small-capital headings in the booking screens, or on the Guest Pass reference label, status pill and payment terms.
* Trip programme PDF: the "Itinerary" heading is kept on the same page as the first day instead of being left alone at the foot of a page.
* "On this page" bar (Bookings dashboard, Operations, Settings): it now sticks directly under the WordPress toolbar at the toolbar's real height (32px, 46px on narrow screens) instead of a fixed 46px that left a strip of content showing above it, and it stays one row that scrolls sideways instead of wrapping into a two- or three-row bar (up to 99px) over the content. Headings it links to land below it. On phones it is not sticky.
* Edit booking, shorter: for anyone who has not arranged the screen themselves, the panels follow the work — request, trip, reception, crew, price, payments, quote, documents, trip content, messages, notes — and the less used ones (Guest Pass details, crew, quote, trip content, messages, WhatsApp, referral, voucher, history) start closed. A closed panel shows a one-line summary of what it holds in its heading, marked when something was typed in it before closing. Fields in a closed panel are still saved; the jump links open the panel they point to. Opening or closing any panel keeps that person's own choice, as WordPress always does. The header adds the received amount and the balance beside the agreed total; on a phone its document buttons sit two to a row.
* Booking screen: a screen-reader label inside the documents table no longer widens the page on a phone; the internal notes panel is tinted and edged so it reads apart from what the guest sees.

= 2.8.5 =
Booking documents: a price set for one booking (revisions with a reason, never over the original), the booking's own trip content, and real PDF quotes, invoices and trip programmes — numbered at issue, frozen, shareable on the Guest Pass. Manual bookings for tours, activities, transfers and custom trips. Schema 6 adds two tables; nothing existing is migrated or repriced.

* Price & services (booking screen): a line editor — service, quantity, unit price, line total — with a discount (amount or percentage), an optional tax line and the currency. Totals are worked out on the server in integer minor units; the browser's running total is a preview. A price of zero and a price on request are kept apart. Each save is a revision stored with the booking (`_wptm_price_revision`, one meta row each, append-only) carrying who, when, the reason (required), the previous figure, its source and the difference; the snapshot taken when the request arrived is never touched, and the product, its rates and its group prices are never written. Payments already recorded fix the currency: a revision in another currency is refused. A revision records no payment, moves no status and sends nothing.
* One money authority: `Payments\expected()` now reads the latest agreement — a revision, or a quote the customer accepted after it — before the snapshot, so the list, the header, the payments panel, the Guest Pass, quotes and invoices print the same figure. A total below what was paid shows "paid in excess" instead of hiding it; nothing is refunded automatically. A quote drafted after a revision starts from the revised lines (`wptm_quote_pricing_context`, priority 5).
* Imported prices never double: an engine line is read as quantity × unit only where that reproduces its own total. A group price ("Private tour, 4 travellers, 660") imports as 1 × 660. The basis (per person, per group, per vehicle) is shown.
* Exact parsing: `Payments\to_minor()` converts the decimal on its digits (`Payments\decimal_to_minor()`), not through a float. What it accepts is unchanged.
* Trip content for this booking: an explicit "Make an editable copy" stores title, description, chosen images, itinerary (keeping days, timeline or route), included / not included, meeting point, time and instructions on the booking (`_wptm_trip_copy`). Opening a booking never copies anything; editing the copy never writes to the product; editing the product never reaches the copy. The Guest Pass prints the copy's title and itinerary when there is one.
* Documents: quotes (from Quotes versions), invoices and trip programmes, previewed as DRAFT without a number and issued with one. New table `wptm_documents` (schema 6): the issued content is frozen as JSON — company, customer, lines, totals and the words in the booking's language — with its SHA-256; an altered row is never served. Numbers are allocated at issue only, from the rows (MAX + 1 per type and series), under a UNIQUE (type, series, number, revision) key that refuses a duplicate under concurrent requests and retries. A correction is a revision with the same number; the previous one is kept, marked superseded. Issuing confirms nothing and records no payment. The PDF is rendered once before a number is taken: a layout that cannot be produced issues nothing and uses no number.
* PDF: real PDF files from the bundled TCPDF 6.11.4 (LGPL-3.0-or-later, `vendor/tcpdf`, trimmed to the engine and DejaVu Sans; loaded only when a PDF is produced; no Composer, no external service). Arabic is shaped and laid out right to left; references, amounts, telephone numbers and Latin-only lines keep their direction. Images come only from the media library, checked to be inside the uploads folder; WebP and other formats are converted to a temporary JPEG through WordPress's image editor. Page numbers, a running header, the company footer, and headings kept with what follows them. Every text is escaped before layout.
* Access: admin downloads need the booking capability and a nonce. A customer can download a document only when it is issued, current, intact and shared, with their Guest Pass key (revoked or regenerated keys stop working at once) or, for a quote, that quote's own key. The Guest Pass lists shared documents; the quote page offers its shared PDF. No file is written to the uploads folder and no address works without a key.
* Company details (Settings): trading and legal name, logo, address, contacts, free identifier rows (ICE, RC, IF, VAT…), footer and short terms per language, numbering (prefixes, year, digits, starting numbers), defaults for payment instructions and programme prices, and an optional tax line (off by default; rate in basis points). Empty fields fall back to the values the site already has (Guest Pass name and logo, support telephone and e-mail, WhatsApp, site address). Payment details stay on the Payments screen and are printed only when chosen.
* Add booking: a tour or activity, a transfer (vehicle and trip type, priced per vehicle) or a custom trip with no product (holds no seats). The product's price for the party is stored as the booking's snapshot, as a request from the site stores it; an optional agreed total is recorded as a revision. Source "Other" needs a description. Values are kept when a field is refused.
* Fix: a booking entered by hand never held its seats. Its ledger key was 43 characters for a 40-character column, so `$wpdb` refused the row; it is now 39. Capacity is respected again for manual bookings with a date.
* Fix: the Guest Pass showed only the old typed payment fields to guests: the ledger summary was registered on wp-admin requests only. It now loads with the pass, and "paid" is what is held (received less refunded).
* Bookings list: Trip, Source and "Total · paid · due" columns (Guests and Submitted are under Screen Options), filters by source and travel-date range, a custom trip's own title, and an empty state that says what to do.
* Booking screen: the header carries the Quote, Invoice and Programme PDF buttons (the current issued file, or the Documents panel), the jump links follow the order a booking is handled, and the default panel order matches it until the user rearranges. The payments form is one readable column.
* Front-end request form: a summary line at the top when a field below was refused.
* Debug notice: the Luxury section registry is attached on `init` instead of at load, so WordPress 6.7+ no longer reports a translation read too early.
* Erasure keeps issued invoices (accounting records) and removes quote and programme documents of the erased booking.
* Issued PDFs are kept. After the number is allocated the document is rendered with it and the file stored in `wptm_document_files` (database only, base64 with a SHA-256 checked on every read; nothing in uploads or the media library). The row stays "pending" — not current, not shared, not shown to the customer — until the file is read back intact; only then is it issued and the revision it replaces marked superseded. Every download, admin or customer (same capability, nonce and key checks), is that file, so later changes to images, logo, company details or the product do not reach it. A render or storage failure leaves the numbered row "Not issued — PDF not stored" with "Finish issuing", which reuses the row and the number; issuing again meanwhile is refused. Deleting a booking or erasing it removes the files with the documents. A document issued by an earlier 2.8.5 build has no stored file: it is drawn again from its frozen text with the pictures as they are now (said so in the list) and its row is not changed.
* Fix (Add booking): an agreed total that could not be saved no longer lets the booking be confirmed or announced. The booking is kept, stays pending, queues nothing, and quotes and invoices are refused until the price is saved; the booking screen shows the amount entered and "Save the agreed price and finish". The retry runs on the same booking under a lock and reads the table first, so a revision whose write reported a failure but held is not written twice. 0 is a price. A booking finished later by recovery follows the same rule. The confirmation is checked in the table, not taken from `set_status()`'s answer: if it did not hold, nothing is queued, what is left stays on the booking and the retry resumes there; a booking already confirmed is not transitioned again, and one a person moved elsewhere (cancelled, declined…) keeps that decision. In recovery, a booking that is saved but not finished is not marked done: its entry is kept and the next run resumes after the effects, without binning it or releasing its seats.
* Fix (Add booking): "Notify" now covers the status change too. Unchecked, confirming queues nothing (`Mail\held_back()`: one booking, this request only, released in `finally`; every other status listener still runs). Checked, the messages are queued once, after the booking and its price are saved. Later changes made on the booking behave as before.
* Fix (Add booking): with seat limits enforced, any failure to hold the seats refuses the booking, not only "full", as the website form does. Nothing is written and what was typed is kept. Without enforcement, transfers and custom trips are unchanged.
* Fix (recovery): `settle()` compares the saved values with those submitted; a missing row is incomplete as before, a different value is unknown (kept for the administrator notice, not binned). A record that stopped mid-save with nothing to compare against is unknown instead of complete. Seats are checked on the ledger row. The settling lock (`wptm_booking_settling_{id}`) lapses after five minutes and only its owner releases it; the 2.8.3 claim that could stall a booking for ever is ignored. Progress after the save is recorded (`_wptm_settle_stage`), so a stop after "complete" still queues the messages; a stop inside the `wptm_booking_created` listeners does not run them again (logged). Waiting bookings are one option row each (`wptm_booking_unverified_{id}`), so concurrent writers lose none; the 2.8.3 list is moved on first read.
* Fix (prices): `Pricing\snapshot_state()` no longer accepts an existing snapshot on presence. It must be one readable row with its total and currency agreeing with the expected snapshot; otherwise the new answer `invalid` (unknown, never complete or binned). Missing or wrong report copies (`_wptm_price_total_minor`, `_wptm_price_currency`) are written again from the stored snapshot, never from current rates; the snapshot is never replaced.

= 2.8.3 =
Stabilisation: bookings, prices and notifications are confirmed as stored before success is announced; Guest Pass map priority. No schema, URL, token, QR, permission or pricing-formula change; nothing is migrated or repriced.

* Bookings: `Booking\create()` reads the required values back from the table (uncached) and compares them with what was written — reference, customer name, e-mail and phone, product, date and start time, departure, party, status, the price snapshot when one was worked out, and the reservation link — instead of trusting `wp_insert_post()` or `update_post_meta()`'s return. Three outcomes are kept apart. Complete: the booking carries `_wptm_save_state = complete` (itself verified), the ledger is joined and `wptm_booking_created` fires. Confirmed incomplete: the post is moved to the bin and the move is confirmed in the table before `create()` returns `wptm_booking_incomplete`; only then do tour, activity, transfer and manual callers release the seats; if the move cannot be confirmed the outcome is unknown. Unknown (the verification read failed): `create()` returns `wptm_booking_unverified`; nothing is binned, deleted or released, the ledger row and the reservation link stay with the request so a resend waits for this booking instead of creating another, the visitor sees the translated "still being recorded" message with their answers kept (the manual form says to check the Bookings list first), and the booking is recorded in `wptm_booking_unverified`. The queue runner (loop back, cron, hourly sweep, admin fallback) settles it with `Booking\recover_unverified()`: complete → the save state, ledger join, history, `wptm_booking_created` and the notifications, once (an atomic claim row guards them); confirmed incomplete → binned, the move confirmed, then the seats released; still unreadable → kept, and after ten minutes administrators see a count with a "Check now" button. A request that stopped mid-save (state still "pending" after ten minutes) is settled the same way. Age alone never decides an outcome. Luxury's transaction is unchanged: the same checks run inside it, and any failure rolls it back.
* Repeated requests: a repeated copy of a request (`await_booking()`, `resolve_duplicate()`, and the fingerprint shortcut for tours, activities and transfers) is answered with a booking only once `Booking\completion_state()` says it is complete. A ledger row naming a booking, or a booking naming its reservation, is no longer enough; a failed read is never complete. Until then the copy waits up to the existing limit and gets the "still being recorded" message. Bookings made before 2.8.3 carry no save state and count as complete when they have a reference and a status. Reconcile no longer joins a booking whose save is still pending. Uninstall removes the two new operational options.
* Prices: `Pricing\store_snapshot()` keeps its signature and returns true only when the snapshot (and, for a priced booking, its total and currency) read back intact; `Pricing\snapshot_state()` gives the full answer (stored / exists / failed / unknown). An existing snapshot is never replaced, checked in the table. Tour, activity and transfer requests pass the server-side snapshot to `create()`, which stores it before the booking is announced. A zero total is stored as a price; a price on request stores the snapshot without a total; no quote stores nothing, as before. The calculation is unchanged and uses the same inputs, just before the post rather than after it; listeners of `wptm_booking_created` (the payment state) now see the snapshot. Recovery stores the snapshot worked out at request time if it is missing; nothing is repriced.
* Notifications: `Mail\queue_state()` tells queued / exists / skipped / failed apart (`queue()` still returns the row id). Customer mail switched off, a missing address or a refused duplicate is not a failure. A required request or confirmation message that could not be added to the outbox flags the booking with the existing `_wptm_mail_queued` key; the queue runner adds what is still owed — dedupe keys make repeated or concurrent runs write one row — and removes the flag only when nothing owed is missing. What is owed follows the booking's current status (`Mail\still_owed()`): "request received" only while the request is open (pending, contacted), the confirmation only while confirmed, the agency's new-request message not once cancelled, declined or completed. The same test runs again when a row is composed for sending; a message that no longer fits is cancelled as "No longer applicable", not failed. Retries are spaced by a one-minute gate; a retry that still fails shows administrators a count and "Try again now" (capability and nonce checked). No personal data in notices or the log. A failed notification never cancels or recreates a booking; "sending"/"uncertain" rows are untouched and never resent automatically.
* Guest Pass map: the booking's own map link, then the booking's own coordinates (a complete, in-range pair; 0 is valid), then the tour's meeting map link, then a search for the place and address. Before, the tour's link outranked a pin set for the booking.
* Luxury: the private-enquiry storage check is shown under Settings → "How journeys are sold" when the luxury experience is on, so a non-InnoDB or unreadable-engine database is visible before the first enquiry is refused. Nothing is converted and nothing is saved unsafely.
* Changelog: 2.8.2 said sites with non-InnoDB tables "keep the previous behaviour and log a warning"; in fact the enquiry is refused, the reason logged and an administrator notice shown. Corrected below.

= 2.8.2 =
Luxury enquiry concurrency fix and Guest Pass visual polish. No schema, URL, token, QR or permission change.

* Luxury enquiries: "sent" is answered only when the enquiry is stored. The booking carries its submission key (`_wptm_enquiry_once`, written with the record), so a retry after a crash between saving and finishing resolves to it instead of filing a duplicate. A copy that arrives while the first is still writing waits up to 6 s (`wptm_luxury_once_wait`) and then gets a translated, recoverable message with its answers kept. A failed save releases the claim at once. Claims are taken with INSERT IGNORE, expired claims (120 s) are taken over with a compare-and-swap UPDATE, and releases delete only the releasing request's own token, so a stale worker can neither overwrite nor release a newer claim. The hourly sweep deletes only the exact expired values it read.
* Luxury enquiries, storage: the booking, every meta row (the submission key included), the enquiry fields, the outbox rows and the end of the claim are one InnoDB transaction (`store_enquiry()`). It opens by locking the claim row (SELECT … FOR UPDATE) and checking the owner token, so a worker whose lease was taken over writes nothing and queues no mail, and no takeover can happen while the owner is inside it. A request that dies before COMMIT leaves nothing (the server rolls back on disconnect); a silent reconnect mid-transaction is detected by connection id and its fragments removed; a failed COMMIT is resolved by reading. Notifications are queued inside the transaction and only started after COMMIT (`queue_mail()`, `notify()` unchanged for callers); `wptm_luxury_enquiry_created` fires after COMMIT. Lock waits are bounded to 3 s for this connection. Sites whose posts, postmeta, options or outbox tables are not InnoDB (or whose engines cannot be read) store no enquiry: the visitor gets the translated retry message, the reason is logged and an administrator notice explains it (see Safeguards).
* Luxury enquiries, safeguards: no enquiry is stored without a transaction. If posts, postmeta, options or the outbox table is not InnoDB ('unsupported') or the engines cannot be read ('check_failed'), the visitor gets the translated retry message with their answers kept, only this request's claim is released, the reason is logged and an administrator notice explains it (clearing itself once the check passes). Before COMMIT every required value (customer, tour, party, notes, status, enquiry fields, reference, request key) is read back and compared with the validated submission, and the owed outbox rows are checked against the booking; `queue_mail()` now reports queued / exists / skipped / failed, so switched-off customer mail or a missing address never blocks an enquiry while a failed insert rolls it back. A failed COMMIT, or one answered on a replacement connection, is never taken at its word: the transaction is ended with ROLLBACK and the durable state is read afresh (`committed_state()`): complete is success, absent is a retryable failure, anything else is "confirmation pending" with nothing recreated or deleted. The lock-wait setting is restored at shutdown.
* Guest Pass: header reads agency, pass title with reference and status, trip, then the facts; the facts grid has no empty cells; a reset rule that outranked every spacing rule now carries no weight (`:where()`), restoring the intended spacing; long names and addresses wrap inside their cards; pickup details and guest actions are grouped under hairlines; plain buttons get the primary style instead of the browser's; the QR sits on plain white with quiet space; the phone bar colours WhatsApp by destination; print shows black text (the header's white text vanished on paper), hides controls and WhatsApp links, keeps the QR block and key groups whole.

= 2.8.1 =
Stabilisation pass before 3.0. No schema change, no new dependency, no change to pricing formulas, URLs, hooks or stored data. Existing bookings, price snapshots, settings and layouts are read exactly as before.

* Booking idempotency (includes/booking/handler.php, transfers/booking.php, luxury/enquiry.php). The ledger key was sha256( product | wptm_request_id ), and the identifier is printed into the page, so behind a page cache every visitor shared it: the second visitor to book matched the first visitor's reservation and was shown the first visitor's reference, date and pick-up. The key is now `Booking\idempotency_key()`: scope, product, request identifier, the nonce the form carried, and `Booking\request_payload()` (normalised identity plus every booking distinction). A genuine retry repeats all of it; two visitors cannot. `Booking\request_id_field()` prints the field and queues a tiny inline script that replaces the printed value per page view; without JavaScript the payload alone keeps visitors apart.
* Concurrent copies. A copy that found the ledger row with no booking on it yet carried on and created a second booking on the same reservation (six simultaneous posts made six bookings). `Booking\resolve_duplicate()` now waits up to PENDING_WAIT (8 s, filter `wptm_booking_pending_wait`) for the first copy via `Reservations\owner()` and the booking's `_wptm_reservation_id`, then answers with that booking, or with a recoverable message if it is still being written or was released.
* Duplicate detection. `duplicate_key()` hashes every identifier given plus date, start time, departure, party, extras, pick-up, guide language, notes and, for transfers, vehicle, trip, drop-off, return, luggage and flight. `normalise_number()` follows the WhatsApp module's rules (international digits kept whole, a trunk zero gets the agency's country code) instead of keeping the last nine digits.
* Expired nonce. Booking and transfer handlers used to redirect silently to the home page; the luxury form ended on wp_die(). `Booking\expired_nonce()` sends the visitor back to the product with `err_expired` and the sanitised answers in the usual one-use state transient (metered, honeypot-checked, nothing booked). New strings: err_expired, err_in_progress (en, fr, es, ar).
* Luxury enquiries: the once-key includes the payload and nonce and is claimed with add_option() before anything is stored (released on validation or storage failure, swept hourly). The wptm_enq answer page now sends DONOTCACHEPAGE, no-store headers and Rank Math noindex.
* Outbox (mail/outbox.php, mail/admin.php). `recover_stale()` moves rows left "sending" for STALE_AFTER (900 s, filter `wptm_mail_stale_after`) to the new status "uncertain" in one conditional UPDATE; run() never picks them up. Messages panel: "Mark as delivered" (`mark_delivered()`) and "Send again anyway"; `resend()` refuses a row in flight and resets only the exact row the operator saw (`row_version()`), so a double click sends once. compose() exceptions are recorded as failures. fail() counted attempts one high, so the first retry waited 10 minutes instead of 2. counts() gains `uncertain`, shown on Settings and the Operations board.
* Pricing: extras keys are sanitize_key( sanitize_title() ), the form the booking filter and the preview already used; an extra with an Arabic name was dropped from both. Latin keys are unchanged, so stored snapshots are unaffected.
* Editor (fields/native.php, acf-integration.php, tour/itinerary-details.php). `Fields\inferred_value()` shows the value a tour actually uses for pricing_model, departure_mode and price_currency when none is stored, so Update on an untouched legacy tour no longer rewrites a per-group price as per traveller, a scheduled tour as flexible, or pins EUR on a MAD site. Number boxes show "45,5" / "1 250,50" / "€99.90" as the same plain amount (the browser emptied them before, and the save wiped the price); unreadable text is kept in a text box. The itinerary save no longer resets the 2.5.0 re-check stamp from 2 to 1.
* Front end: `.wptm-route__heading` no longer forces the body face; the route end label is never squeezed (it overflowed the viewport by up to 21px at 360px on Sara and Medina); Atlas's route row spans the band. `.wptm-cta__form` (620px) and `.wptm-cta__lead` (50ch) are restated after the `.wptm-tour *` guard, which had lifted them (Qamar's closing form ran 1020px wide). tour.js reserves the bar's height at the end of the document, so a theme footer is no longer under the phone bar at the bottom of the page.
* Admin help: the price and Group Rates fields say whether the amount is per traveller or per group; the seasons help no longer claims amounts are accepted (they are percentages).

= 2.8.0 =
Tripvance 2.8 is a curated tour layout release. The tour page is offered as seven layouts with different purposes instead of a long list of overlapping designs, and the architecture tells a layout (the page structure) apart from a style variant (its visual language) and a gallery variant (how it presents the photographs). Nothing is migrated and no tour needs to be reopened or saved: tours, activities, bookings, pricing, Group Rates, itineraries, FAQ, reviews, galleries, highlights, trust items, translations, settings and saved layout values are read exactly as before.

* Layout registry (includes/layouts.php): one list with each layout's family, purpose, recommended use, style variants, gallery variants and legacy aliases. New helpers: `Layouts\families()`, `Layouts\offered()`, `Layouts\is_legacy()`, `Layouts\resolve( Tour )` (layout, family, variant, classes, variant stylesheet), `Layouts\root_classes()`, `Layouts\picker_items()`, `Layouts\variant_choices()`. Filters: `wptm_layout_legacy_aliases`, `wptm_layout_resolved`; `wptm_layouts` is unchanged. Slugs are unchanged (SalmaDesign, SaraDesign, AtlasDesign, SaharaDesign, MedinaDesign, QamarDesign, classic).
* Variants are stored per tour in two new optional fields, `wptm_layout_style` and `wptm_layout_gallery` (shown only for Atlas and Sahara), with site defaults in Settings (`layout_style`, `layout_gallery`). Empty means the site default, then the family default (Clean, Cinematic). A variant saved for another family is ignored, not erased. Only the resolved variant's stylesheet is loaded (atlas-zellige.css, sahara-immersive.css, sahara-mosaic.css).
* Legacy layouts: Immersive, Editorial, Mosaic, Adventure and ZelligeDesign stay registered under the same slugs with the same templates and stylesheets, so a tour that saved one renders exactly as before (checked by comparing the 2.7.0 and 2.8.0 HTML of every fixture tour). They are described in the picker by their successor (Editorial → Atlas, ZelligeDesign → Atlas with the Zellige style, Immersive and Mosaic → Sahara with that gallery, Adventure → Sahara) and are shown only to the tour or site that has one saved. The older aliases (split, sidebar, wide, stacked) map as before.
* Salma — unchanged: gallery beside the two-step booking panel. Gains the family class only.
* Sara — marketplace product page: title, rating and place across the top; a product gallery (one large photograph, three beside it, the count on the last); the facts in one ruled row (a two-column grid on phones); trust; then the reading beside the sticky price card. Salma opens with booking; Sara opens with the product.
* Atlas — itinerary first: after the introduction beside the booking card, the route and the days run across the page on a tinted band with a day index beside them (sticky on wide screens, a row of chips on phones); the practical part follows in the order Good to know, Included, What to bring, Rates, FAQ, Reviews (Included and What to bring used to sit beside the card, before the days). The navigation follows the new order. Zellige style: star motif on headings, mounted opening photograph, two-tone rules, drawn in CSS only.
* Sahara — photo-first with no sidebar: the opening per gallery variant (Cinematic: title over a full-width photograph; Immersive: taller edge-to-edge photograph, title beneath; Mosaic: title, then one large photograph and four beside it); facts and an offer line (price and button) on one band; highlights across the page on a sand band; one open column with photo breaks from the tour's own gallery (decorative repeats, empty alt; only when there are enough photographs); the meeting point and help cards; the booking band with the form; then questions and reviews.
* Medina — compact: the meeting point (address and time) joins the facts above the fold; the short practical sections (Included, What to bring, Good to know, Rates) pair up two to a row on wide screens; on phones the order is photograph, title, facts, meeting point, booking, then the notes.
* Qamar — premium storytelling (rebuilt; its 2.7 arrangement overlapped Medina's): full-width opening photograph, centred title, the lead set large with the facts as one quiet line, highlights on a paper band, the description beside a photograph, the journey (route and days) in one measure, the notes beside a photograph on the other side, inclusions and rates, one invitation band (price, booking form, meeting point and help), questions and reviews side by side, related tours. No sidebar.
* Classic — unchanged.
* New shared parts in templates/parts/: itinerary-index.php (the day index, from Tour::itinerary(), Itinerary\mode() and ItineraryView\day_title() — no parsing of its own) and offer-line.php (Tour::price() and Tour::cta(), the same link the phone bar uses). `Tripvance\part()` renders a part; theme override tripvance/parts/<name>.php.
* The booking band draws the form itself on Sahara and Qamar (`book-now` with `inline` off), since those layouts have no ticket to carry it; the band keeps id="wptm-book-now", so every booking button has its target.
* Gallery: a horizontal swipe on the main photograph now turns it (it already did in the viewer), direction-aware for right-to-left pages. Same script, bound once.
* Layout picker: Settings, the tour's Layout field and the setup wizard show the seven layouts with their purpose, a description, when to use them and a miniature of the real arrangement.
* One list for every layout control (`Layouts\admin_choices()`): the picker cards and the select under them offer the curated layouts plus the tour's (or site's) own legacy value only; saving without a change keeps a legacy value, and validation still accepts every registered slug. The tour's Sidebar field is shown only under layouts that read it (the shared template: Classic and the legacy presets; `Layouts\supports_sidebar()`), hidden rather than cleared, so a stored value is kept; the Settings description says so.
* Fixed: the setup wizard could not set any of the *Design layouts as the default (the slug was lowercased before it was checked against the registry).
* No schema, no new dependency, no new post type, no change to pricing, Group Rates, booking calculations, validation, meta keys of existing fields, URLs or slugs.
* Final polish (same version):
  - Sara: the booking column sticks as one stack (sara.css, and tour.js now measures `.wptm-sara__aside`). Only the price card was sticky, so the meeting point and "Need help?" cards that follow it in the same column slid underneath it while scrolling and the WhatsApp button at their foot was painted over or showed as a cut strip. The stack is pinned by its bottom edge when taller than the screen, so its last card is always reached.
  - WhatsApp mark: `icon( 'whatsapp' )` is now the filled WhatsApp glyph (currentColor, even-odd) instead of two thin stroked outlines that blurred into a scribble at button size. Inline SVG, no font, no request. Used on every WhatsApp button, including the contact section, the archive sidebar, landing pages and the term index, which drew a generic chat bubble.
  - "Need help?" and meeting point cards (tour.css): heading centred on its badge, the WhatsApp button on the card's foot when the card is stretched beside a neighbour, a clear step above the button, long labels wrap with the mark kept whole; on phones the badge stays beside the heading (the help card's badge used to fall under the button).
  - Atlas route at a glance: heading at the plan's title scale, the sketch capped to a sketch's height, the chain as a ruled row of stops (labels above the two ends, arrows trailing each stop so a wrapped line never starts with an arrow; route.css, Atlas and Qamar only, RTL mirrored, Arabic untracked).
  - Medina: the questions and reviews no longer sit beside an empty half page. With notes, they close the body across both tracks (side by side when there are two, a heading/content split when there is one); with no notes, they take the notes' track beside the reading; with nothing to read, the notes take the first track. Same section output and source order, so phones read as before. The meeting point and help cards share one height; the closing call to action lost its doubled rule.
  - Qamar: stacked sections in a narrative band are spaced again (a reset outweighed the spacing rule), short narratives sit on the photograph's centre line, the summary reads as a standfirst, the route heading matches the journey's scale, the highlights band and the related tours lost a stray second rule, and the concierge cards share one height.

= 2.7.0 =
Tripvance 2.7 is a tour display stabilization and consistency release. Every tour layout keeps its look; underneath, the layouts now share the same components and the same data, so a configured section no longer goes missing on one design or behaves differently on another. Nothing is migrated and no tour needs to be reopened or saved: tours, activities, bookings, pricing, Group Rates, translations, settings and saved meta keys are read exactly as before.

* Shared layout contract: layouts decide where a shared section sits, never what it does. New helpers `Tripvance\will_render()` (does a section have something to draw, and is it not switched off in the Section Manager) and `Tripvance\layout_order()` (the section keys in the order a design really draws them).
* Trust section on SalmaDesign (after the tour, before the related tours) and QamarDesign (a band of its own before the closing call to action), from the same shared section every other design already used. No trust items in Settings, no band. Trust rows with no title are no longer printed as empty lines.
* One tour card data layer (`Tripvance\Card\data()`) and one renderer (`Tripvance\Card\render()`) with three variants: standard, archive and related. The archive's `.wptm-card2` and the related-tour cards moved out of their templates with their markup and classes unchanged; prices, "From" wording, basis and the Group Rates notice all come from the tour's authoritative pricing methods. The related cards now name places by the same rule as every other card (at most two destinations, the tour type when there is none), and their images and meta are fetched in one query instead of one per card.
* Section navigation follows the page: AtlasDesign (route and itinerary lifted out of the column), ZelligeDesign (route under the facts), Sara, Qamar, Sahara and Medina pass the order they actually draw, so the links read top to bottom in page order and never point at a section the design does not draw. Public anchor ids are unchanged.
* Booking confirmation: exactly one confirmation panel on every design. Sara, Qamar, Atlas, Sahara, Medina and Zellige drew it twice (top of the page and closing band); the classic layouts kept the booking ticket beside it. Both carried `id="wptm-book-now"`; now one element does.
* Booking dialog: decided by the dialog on every design (no dialog without a booking form, none on a confirmation page). Tours with booking switched off no longer carry a hidden dialog holding an empty link, and SalmaDesign's ticket no longer shows an empty link under a perforation.
* The booking form's nonce input no longer repeats `id="_wptm_nonce"` when the form is drawn twice on a page (band and dialog). Same action, name and value: the request handler is unchanged.
* The two-step booking form is a shared component, not part of SalmaDesign. Its rules moved from salma.css into the new booking-steps.css (handle `wptm-booking-steps`), loaded wherever the form can appear; salma.css (handle `wptm-salma`, unchanged) now holds SalmaDesign's layout only, is loaded on SalmaDesign only and depends on the form's stylesheet. The split moved rules without editing one, and was checked for cascade-order changes. The form's markup moved to templates/parts/booking-steps.php; the old path still loads it. The script keeps its file, handle and wptmSalma settings.
* Empty-state safety: the facts strip, the design bands that wrap it, the related-tours bands and the FAQ (entries with no question) draw nothing when there is nothing to show, instead of a styled empty band or a heading over nothing.
* Itinerary: a day title typed as "Day 1: ..." under a card that already reads "Day 01" and "Day 1 of 14" is printed without the repeated number. Display only; the stored title is untouched, and a title whose number differs prints as written.
* Highlights: one shared item style on every layout — a fixed 34px icon centred on the first line of text, the list's gap as the only spacing (18px, 28px between columns), two columns where the section is at least 680px wide and one below, 15–16px text at weight 600 with a 1.5 line height, no tracking on Arabic. SalmaDesign keeps its smaller text size and drops its own item overrides. A highlights line with no title ("| detail") no longer makes an empty Highlights section or a navigation link to one.
* Empty states, second pass: the booking card beside the column (Sara, Atlas), the information cards under the gallery (Qamar, Medina) and the closing band that holds the booking call to action and the related tours (Qamar, Medina) are drawn only when something inside them will be; the sidebar prints nothing, not even its hidden heading, when it has neither a ticket nor a card.
* Tour::has() had two 'facts' cases; the unreachable second one is removed (behaviour unchanged).
* Phone booking bar: with a long translated basis or Group Rates note the price column could take the whole bar and squeeze the button to one letter per line. The price keeps its intended cap; short prices look exactly as before.
* One documented layering scale (`--wptm-z-sticky`, `--wptm-z-float`, `--wptm-z-modal`, `--wptm-z-lightbox`, with reserved dropdown and overlay levels) used by the section navigation, the phone bar, the booking dialog and the photo viewer. The values are the ones already in use, so nothing moves relative to a theme or a chat widget.
* Breakpoints: paired `max-width: Npx` / `min-width: N+1px` queries now meet at N.98px, closing the gap a zoomed or fractional-width screen could fall into. Integer widths behave exactly as before; layout-specific breakpoints are kept.
* JavaScript: the tour, two-step form, booking form, card and itinerary scripts bind once per element, so a script loaded twice by an optimisation plugin no longer doubles every handler.
* No schema, no new dependency, no pricing formula, booking calculation, meta key, URL or text-domain change.

= 2.5.1 =
* Hotfix: Improved stability and responsive layout of the Homepage FAQ section.
* Fixed inconsistent FAQ card heights and layout shifting when displaying answers.

= 2.5.0 =
Tripvance 2.5 focuses on administrator workflow, editor stability, UI consistency, draft/publish actions, validation feedback and overall admin polish. A refinement release: tours, activities, transfers, bookings, pricing, group rates, quotes and the Guest Pass work as before, product pages keep their design with a finishing pass on the gallery, spacing and review form. One conservative migration runs on upgrade: a migrated itinerary still stored as a single untouched collapsed day is rebuilt from its original text; edited cards are never replaced and the original text is kept.

* Editor layout drawn by the server and CSS from the first paint: section navigation in the left column, the opening section chosen before the page is drawn, no rearranging after load. Reopens on the section being edited after a save. Works without JavaScript.
* Status-aware action bar: Save draft / Publish for new and draft posts, Update for published and scheduled ones, Save as pending and Submit for review where permissions require. Preview, Duplicate, View bookings, Move to draft and Move to trash in a More menu, shown only where they apply. The buttons press WordPress's own controls and work without JavaScript.
* Human status labels (New tour, Draft, Pending review, Scheduled, Published, Private); "auto-draft" is never shown.
* Save state: Not saved yet, Unsaved changes, Saving…, Saved just now / N minutes ago. The leave-page warning respects saves and previews. Ctrl/Cmd+S saves a draft as a draft.
* Duplicate for activities as well as tours; always a draft; the Polylang translation group and import/restore history are not copied.
* One completion definition (essentials only), an actionable "items need attention" list, and one set of section marks: complete, needs attention, incomplete, must be fixed.
* Drafts are never blocked. Publish and Update stop only for a missing title or a malformed number or address, explained under the bar and on the section. A first publish without a title is kept as a draft on the server.
* One icon policy, one button shape and hierarchy, one chip style, a shared spacing scale and one accent colour across the editor, bookings, operations and transfers.
* Same-named terms in the editor's term lists are shown with their language, parent or slug; nothing is merged or deleted.
* Keyboard support for the new menus, lists and field finder; visible focus; no state told by colour alone; RTL-safe logical properties; no horizontal scrolling at narrow widths.
* Tour galleries: every photograph is in the thumbnail strip, which scrolls sideways with the next picture showing at its edge and previous/next arrows once there is more to see; the selected thumbnail stays clear and in view. Small sets (two to six photographs) fill the row instead of leaving a gap, with no arrows when everything fits. Activity galleries mark the last frame "+N" when the viewer holds more photographs than the grid shows.
* Product pages: no empty "related tours" block (and its reserved blank space) when there is nothing to suggest; the classic layouts get a content width and gutter on block themes; the "prices checked" date wraps inside a narrow booking card.
* Reviews: the logged-in line reads as one sentence, the rating select is sized to its options, the submit button uses the plugin's primary style, and the summary and form line up with the review list in every layout (full width on tablets and phones).
* Itinerary and FAQ: text pasted from Word, Google Docs, AI tools or other plugins no longer collapses into one day or one question. Numbered days or questions run together in one paragraph ("2- Day two" or "2-Day two") are separated when the numbering is unambiguous, a day recovered that way as "Title / Description" keeps a short title, and an ordered list keeps its sequence; ranges, times, dates and prices never open a day, and bullet lists stay inside their answer. Content that already read correctly is read exactly as before. Migrated tours whose single collapsed day was never edited are rebuilt from their original text once, on upgrade; edited cards are never replaced and the original text is kept.
* Structured data: Tripvance adds at most one Product to a Tour or Activity page (name, URL, image and short description where shown; the bookable price as one Offer at the public "From" price — the fixed price or the lowest valid Group Rate; the aggregate of approved on-site reviews where shown). Prices come from the page's own pricing code; tours on request, incomplete rate tables and seasonal discounts get no price. With Rank Math the data enriches Rank Math's own Product for the page or adds one Product to its graph; without it, one JSON-LD Product is printed. Nothing is added elsewhere.
* Removed: the homepage graph (TravelAgency, WebSite, WebPage, FAQPage, ItemList) and its setting, and the ratings and reviews previously added to Rank Math Service, TouristTrip or Trip entities. Legacy settings are preserved. The old rating_schema value is read only to preserve an existing rating opt-out until the new product_rating preference is saved. "Stars in search results" is now "Search result product data", on by default, with its own rating option; a site that had switched the old rating option off keeps ratings off; search engines decide what is shown.
* Rank Math: the sitemap empty-terms switch is no longer forced for every taxonomy (only empty Tripvance terms are left out), and the page builder no longer writes or clears Rank Math's SEO title, description and focus keyword.

= 2.1.0 =
Transfers with reusable Vehicle Types, Fleet links in Operations, transfer-aware booking documents, and an optional Privacy & Cookies module. Tours, activities, group rates and their pricing are unchanged, and nothing is migrated: existing bookings and settings keep working.

**Transfers**

* New Transfers post type (`wptm_transfer`, /transfers/{slug}/): route, pickup and drop-off types, estimated duration, distance, operating hours, minimum booking notice, pickup and meeting instructions, inclusions, important information, cancellation and gallery. An optional transfer archive (Settings), off by default. No schema is printed; the SEO plugin owns the markup.
* Vehicle Types (`wptm_vehicle_profile`): reusable customer-facing vehicle options with category, class, passenger and luggage capacity, features and a picture. Private data, not public pages.
* Transfer pricing: each transfer sets its own price per Vehicle Type. A round trip is two journeys at the same price; there are no distance, surcharge or discount rules. One pricing service feeds the page, the server-side check and the price stored with the booking.
* Transfer booking form: Vehicle Type cards, one way or round trip, pickup date and time, passengers, luggage, optional flight details and addresses. Passenger capacity is checked in the browser and on the server; minimum notice is enforced on the server.
* A transfer request is an ordinary booking record (same statuses, history, reservation ledger, messages and confirmation), marked `_wptm_booking_type = transfer`, with its facts under `_wptm_trf_*` and its price snapshot stored once.
* The price snapshot stored with a transfer booking is worded in the booking's language (one way, round trip); amounts are unchanged and earlier bookings are not rewritten.
* The WhatsApp follow-up after a transfer request carries the transfer's facts — reference, transfer, route, pickup and drop-off, pickup date and time, passengers, Vehicle Type and return — to the site's WhatsApp number. Tours and activities keep their message.
* Homepage: a new Transfers section, off until ticked in Page layout. Latest transfers or transfers chosen and ordered by hand, a heading and a line, and cards with the route, Vehicle Types, capacity and the "from" price per vehicle from the transfer pricing service, linking to each transfer page. Existing homepages render unchanged.

**Operations and Booking Admin**

* Operations → Vehicles is now labelled Fleet, and Transfers → Vehicle Types holds the customer-facing options. Post types and meta keys are unchanged.
* A fleet vehicle can be linked to a Vehicle Type, whose capacity and class it then reads (an existing seat figure still wins). Optional brand, model and operational status (Available, Assigned, Unavailable, Maintenance).
* Assigning a fleet vehicle to a transfer: a warning when it does not match the requested Vehicle Type or is Unavailable or in Maintenance; refused when it cannot carry the passengers. The requested Vehicle Type is never overwritten.
* Transfer bookings show a Transportation block (route, requested Vehicle Type, passengers, luggage, assigned fleet vehicle, driver), a booking type filter in the list, and Trip details with pickup date, pickup time and one Passengers field kept equal to the reservation ledger.
* Quotes, the Guest Pass, the voucher, check-in, the operations board and the personal-data export read transfer bookings in transfer terms, and quotes start from the price stored with the booking rather than tour rates. The Guest Pass and quotes no longer read a transfer as a tour.

**Privacy & Cookies**

* Optional consent banner, off by default and loading nothing while off: Tripvance → Settings → Privacy & Cookies. Three layouts, four categories (Necessary, Analytics, Marketing, Preferences), editable wording, RTL and keyboard support.
* Scripts placed in the Analytics, Marketing and Preferences boxes, or tagged `type="text/plain" data-wptm-consent="…"`, run only after their category is accepted. Consent is kept in one first-party cookie, `wptm_consent`.
* Campaign attribution (`wptm_attr`) now honours the Analytics choice made in Privacy & Cookies. When a consent manager runs the WordPress Consent API in opt-in mode, its statistics answer is final and a refusal there is never overridden by Privacy & Cookies; the `wptm_attribution_consent` filter still has the last word. Without consent it stays off.
* The module helps collect and honour consent; it does not by itself make a site legally compliant.

**Display**

* Homepage: the hero no longer jumps under the theme header, spacing in rails and lists is restored, and search fields have a 44px touch target.
* Tour and activity pages: the price, its basis and the group-rate note read as one phrase; phone bar and quick-fact layout fixes. Display only.

= 2.0.2 =
Centralized pricing and group-rate integrity validation; scoped archive facets,
normalized duration labels, clean reviews and graceful form fallbacks.

= 2.0.1 =
Visual refinement of the tour page and the booking screens. No change to the pricing engine, the booking workflow, the stored data or the meta keys.

* Tour header: title, place, rating and the starting price read as one opening; the quick facts sit under the gallery as one strip.
* Gallery: the picture takes the reading column on a desktop, the thumbnails are an even strip, and a "View all photos" button on the picture opens the viewer, which now shows where you are in the set ("3 / 8") and follows the reading direction.
* Booking panel: reads in the order of the decision — From price, date and travellers, the rate for that party, extras, the estimated total, then the button — with the site's own reassurances and reply time under the button. Every figure still comes from the server.
* Fix: optional extras in the two-step form were drawn as large empty boxes with upper-case labels; they are real checkboxes with their price.
* Fix: a form reopened at step two after an error did not fill its summary.
* Phone bar: more compact, safe-area aware, and hidden while a dialog or the photo viewer is open.
* Sections: one spacing and type scale for Overview, Itinerary (multi-day thread and timed day timeline), Included / Not included, Important information, questions, reviews and related tours.
* Reviews: a summary card with the average as stars, and each review as a card with the name, date and stars.
* Fix: a single related tour grew to the width of the page; the rate table on SalmaDesign split its header on phones; sections below the booking panel carried a double gutter on phones; the page ran edge to edge on block themes.
* Bookings list: a Total column (accepted quote, else the price worked out with the request), status views with counts, a "Move to" menu for a safe status change from the list (same transitions, history and hooks as the booking screen), and a readable layout on phones.
* Booking screen: the request is set out as Booking, Trip, Customer, Pricing (line by line from the stored price snapshot) and Notes, each reachable from the header.
* Fix: group-rate input uses one decimal parser for dot/comma decimals and grouped thousands; displayed tiers retain cents.
* Admin: non-blocking warnings for group-rate gaps, overlaps, reversed ranges and invalid prices; saved values and engine selection rules are unchanged.
* Reviews: full, true half and empty SVG stars use the existing icon set, with RTL and accessible rating text.

= 2.0.0 — The Experience Builder =

Tours and Activities share one Experience Core and one section-based
editor, and stay two post types with their own URLs and page designs.
No meta key moves and nothing is migrated; see readme.txt for the list.

Pricing clarity
---------------

Presentation only: no stored price, tier, total or formula changes.

* Group-size prices say "Price varies by group size" beside the "From"
  price; the stored "From" price is shown unchanged.
* Booking summary reads "Rate for 2 travellers · €495 / person" instead of
  "Base rate", and updates as the party changes.
* "View group rates" lists the configured tiers from the engine's own bands.
* Children priced below the adult rate are said to be.
* Admin notice when the "From" price differs from the lowest group rate.

Tailor-Made Requests
--------------------

A "plan your own trip" wizard beside the booking form, not inside it.

* Trip Builder: nine steps (dates, length, interests, destinations,
  travel style, accommodation, travellers, budget, contact), one at a
  time, with progress, Previous / Continue and a summary before Send.
  Values stay in the form while moving between steps; nothing reaches
  the server before Send. Without JavaScript every step shows on one page
  and the form still posts. `[tripvance_trip_builder]`, a Trip Builder
  block, or an assigned page, whose Polylang translations show it too.
* Settings: steps on/off, required/optional and reordered; answers for
  interests, destinations, travel styles, accommodation levels and budget
  ranges, each with label, optional Media Library image, active state and
  sort order. Destinations can link to tour destinations. Shipped answers
  carry French, Spanish and Arabic; renamed or added ones go to Polylang's
  string table (group "Tripvance Trip Builder").
* "Create Trip Builder Page" makes Draft pages on the Tripvance full-width
  template: one per language enabled in Polylang, titled in that language
  where Tripvance has the words and linked as translations (setting
  `page_ids`), or one in the site language without Polylang. Pressed again
  it creates only the languages still missing, and never a second page: it
  re-finds the pages it made and keeps assigned pages as they are. An existing page can be moved to
  that template with one button; nothing is changed without it.
* Each step shows one number: the step navigation's list markers and theme
  counters are switched off, and questions carry no second number.
* Requests: private post type `wptm_tmr`, references TMR-1001 onwards,
  statuses New, In Review, Contacted, Proposal Sent, Confirmed, Closed,
  history, internal notes, privacy export and erasure. Reuses the booking
  capabilities.
* Create Draft Tour: title in the request's language with reference,
  days/nights and the destination terms saved with the request; the
  party size stays on the request. No itinerary, price or text
  is invented; the request is shown beside the draft. One per request.
* Emails: to the agency (booking address unless set) and, optionally, to
  the traveller in their language, through wp_mail() with filters
  (`wptm_tmr_admin_message`, `wptm_tmr_customer_message`,
  `wptm_tmr_pre_send`).
* Tour page call to action "Can't find exactly what you want?", off by
  default, on tours only, in every layout through `wptm_tour_sections`.
* New stored values: option `wptm_tmr_settings`, option
  `wptm_tmr_counter`, `_wptm_tmr_*` meta on requests (chosen destinations
  keep their option id, label and tour destination term id), `_wptm_tmr_request`
  on a Draft Tour, `_wptm_tmr_builder_page` on the page it creates.

New stored values, all optional and shared by both types:
wptm_hero_image_mobile, wptm_itinerary_route, wptm_sections,
wptm_presentation (activities), wptm_video_url (activities), and
_wptm_guide_language on a booking. New settings: experience_editor,
activity_presentation. New option: wptm_experience_version.

= 1.42.1 — Why the Activity page could look unstyled, and the phone =

Two faults with one symptom, and the Activity page on a small screen. No
tour page changes, no meta key moves, and every activity, price, gallery
and booking is where it was.

Why the styles were not there
-----------------------------

* The stylesheet was named behind two other handles. wptm-activity was
  enqueued with wptm-tokens and wptm-card as dependencies, which is what
  orders them correctly in the head — and WP_Dependencies::all_deps()
  returns false when a named dependency is not registered, so the style is
  not printed at all. No notice, no warning, nothing in the log: the page
  ships its whole markup and no rules. One handle deregistered by an
  optimisation plugin is enough. Both are now registered at the point of
  use, and a handle that still cannot be found is dropped from the list
  rather than allowed to take the page's own stylesheet with it.
* The page's appearance depended on a second file. Every panel, cell rule,
  hairline and tinted card on it is drawn with custom properties declared
  in tokens.css, and a declaration that reads a property with no value is
  invalid at computed-value time and dropped whole — `border: 1px solid
  var(--wptm-line)` becomes no border, not a default one. So losing that
  one 3 KB file, while activity.css loaded in full, left At a glance with
  no box and no rules between its cells, Highlights with no card behind
  them and the panels with square corners: the page reading as plain text
  and lists. activity.css now carries its own floor for the values its UI
  cannot draw without, declared through :where() so it has no specificity
  at all and tokens.css, the accent setting and the unified brand still
  win wherever they are present.
* Assets are versioned by the file that is served. asset_version() stamped
  the readable source while minified_src() handed the browser the .min
  built from it. Rebuild the .min without touching its source — a build
  step on deploy, a checkout of one file — and the bytes at that address
  change while the address does not, so every browser, CDN edge and page
  cache holding the old copy keeps it. The served file's own modification
  time is now part of the version.
* A .min that is empty or a tenth the size of its source is not served. An
  interrupted build leaves a file, and its modification time says it is
  the newest thing in the folder, so every existing test passed and the
  browser got an empty stylesheet with a 200.

The page on a phone
-------------------

* The hero starts below the theme's header. A fixed header is painted over
  the top of a band that reaches both edges of the screen, and at 360
  pixels the whole breadcrumb row and half the line above the title were
  underneath it. What is measured is the overlap, not a height: whatever
  is fixed or sticky at the top of the window, the admin bar included,
  against where the band actually begins. A header in the normal flow
  measures zero and nothing moves. No theme's height is written anywhere.
* One step tighter on the spacing scale. The page is spaced with three
  custom properties and every one of them was at the lower bound of a
  clamp written for a six-hundred pixel column, so twelve sections came to
  seven hundred pixels of air. The rhythm is the same rhythm, one step in.
* At a glance is compact cells rather than desktop cards, and a short last
  row takes the width instead of leaving an empty ruled half beside it.
* The gallery is two across. Twelve full-width frames at 390 pixels is
  three and a half thousand pixels of scrolling with what is included,
  where to meet and what it costs to cancel on the far side of it. A tap
  still opens the full-size picture in the viewer, which is larger than
  the one column ever was.
* The meeting point stacks, so an address is two lines rather than four of
  two words, and the photograph is large enough to recognise.
* The running order's clock column is the width of its badge at each size.
  The widening this page applies for the badge was unconditional and two
  classes deep, so it also overruled the itinerary component's own
  narrow-screen rules and took a fifth of the screen from the text.
* What to bring, Important information and the questions are headed like
  everything else on the page. They are the tour page's section templates
  and their title sets no colour, so they inherited the body grey and
  stood beside navy headings at a different size over a shorter rule.

The price bar
-------------

* Its height is derived from its own parts and the safe area, and the page
  reserves exactly that. The two were separate numbers and disagreed: at
  360 pixels the price wrapped, the bar became taller than the room left
  for it, and it covered the bottom of the last card.
* The price stays on one line and is never cut — the basis gives way on the
  narrowest screens, the amount never does — and a button label longer
  than the space for it ends in an ellipsis instead of past the card's
  edge. The label had also been given the calendar icon's line-height by a
  first-child selector written when every button carried the icon; on the
  one button that does not, that is the label.

= 1.42.0 — The Activity page, and a way out of the wrong post type =

The Activity page rebuilt around its reading column, a photograph viewer, and
a converter for a tour that should have been an activity. Nothing on a tour
page changes, no meta key moves, and every tour, activity, price, itinerary
and booking is where it was.

The page
--------

* The hero is full width. The band reaches both edges of the screen while
  its words stay in the same column as the reading below them, and it sits
  against the site header instead of under a band of white. The photograph
  keeps its own proportions rather than being stretched to the height of the
  text beside it, and the duration, difficulty, price and button are one
  card under the title rather than a row with a rule over it.
* The booking rail holds the booking card and nothing else. Meeting point,
  cancellation, important information and the questions were drawn in a
  three-hundred-and-seventy pixel column beside the form, where an accordion
  became a ribbon of text; they are sections of the reading column now, at
  its full width, each drawn once. Two dozen stylesheet rules that existed
  only to shrink them to fit that rail are deleted rather than left behind.
* The booking card follows the reader on a wide screen and stacks normally
  on a phone. It carried a max-height and an overflow of its own for a
  while, which put a second scrollbar inside a card of form fields; the tall
  case is answered by the offset instead, so a panel taller than the window
  travels until its last row is on screen and pins from there.
* One spacing scale. Six different margins between sections became three
  custom properties, and a section heading sits closer to what it introduces
  than to the section above it.
* At a glance carries every fact, including when the activity runs, which
  had a card of its own. The strip wraps into counted columns — one, two or
  three by its own width — instead of forcing every fact onto a single row,
  so the labels are read at their proper size rather than shrunk to fit.
* The description and the FAQ answers are justified, hyphenated and set at a
  reading measure, ragged on a narrow column, inheriting the theme's type.

The photographs
---------------

* "View photos" and every thumbnail open a viewer: the full-size picture, a
  counter, previous and next, Escape, arrow keys, a backdrop that closes it
  and a swipe on a touch screen. The page behind does not scroll while it is
  open and focus returns to the frame it was opened from.
* The viewer holds every photograph the activity has, not the twelve the
  grid draws, so the count on the banner and the pictures a reader can reach
  are the same number. The extra ones are links without an image: they cost
  a tag each and fetch nothing until somebody reaches them.
* The hero is the only image fetched eagerly. The gallery is lazy, every
  frame states its dimensions, and nothing below moves as the files arrive.

Tour to Activity
----------------

* An unpublished tour can be copied into a new draft activity, from the
  tours list or from the Publish box. Title, content, excerpt, featured
  image, gallery, destination and place terms, and every commercial and
  experience field the two post types already share — which is most of them,
  because an activity stores them under the tour's own names.
* The tour is never modified: it keeps its post type, its status and every
  meta row, and gains one key naming what it became. No media is copied — a
  gallery is attachment ids, and an id on a second post is the same file
  shown twice. Converting a second time opens the activity that exists.
* The itinerary is converted rather than copied. A tour keeps its stops as
  days and an activity keeps them as moments of one occasion, so a copied
  row arrived as an empty schedule with every word still in it. Stops are
  read through the itinerary layer's own reader — structured, legacy text,
  or the two merged — and written as schedule rows in order with their
  times, titles, descriptions, locations and photographs. A fact or a day
  note with no field of its own is carried into the description rather than
  dropped, and anything that cannot be mapped is named in a notice on the
  new activity.
* Activity type is left for a person to choose: activities have their own
  vocabulary, and guessing which one a "Desert tour" is would be inventing
  data. Booking settings come across with "Accept bookings" off, so nothing
  starts taking requests at a price nobody has checked.

Housekeeping
------------

* Nothing above this page is modified any more. The script used to rewrite a
  theme wrapper's overflow: hidden to clip so the sticky card would stick;
  it now reads that chain and, where a theme clips, writes no offset and
  lets the card fall back to ordinary positioning.
* A table name reaching SHOW INDEX is filtered to word characters, the field
  width is cast at the point of output, and the upgrade notices are inside
  the directory's length limit.

= 1.41.0 — The tour editor answers as you type =

Eight changes to the add and edit screen. Nothing was redesigned, no library
was added, no meta key moved, and the front of the site is untouched.

What was wrong
--------------

* The status card printed "wptm_booking" where the booking mode should be.
  It looked the stored value up in a map of four modes — form, whatsapp,
  external, none — and a tour stores wptm_booking, inquiry, whatsapp,
  external or disabled. Two of the five could never match, and the fallback
  was to print the raw value. It is now the same five the front end switches
  on, asked of the tour so an inherited mode and a mode with no data behind
  it both read correctly.
* Every line of that card was an answer from the last save, beside a
  readiness meter that had already learned to answer from the form.

What is new
-----------

* The status card is live. Destination, duration, price, booking and status
  are recomputed from the fields as they are typed, using the words PHP
  already had: the mode labels, the duration presets and the site's booking
  mode are handed to the script rather than restated in it. A line that ends
  up empty hides; a line a site added through the filter is left alone.
* Completion counts the essentials: Title, Destination, Duration, Short
  description, Price or on request, Itinerary. Destination and Duration are
  new rows. The hero image, the departure point, the inclusions and the
  gallery moved under a Recommended heading — still listed, still ticked,
  no longer holding the percentage down. Advice, never a gate, as before.
* The bar above the title says "Unsaved changes" from the first keystroke
  and returns to "Saved N ago" on submit. The state was already tracked for
  the warning on the way out; it is simply said out loud now.
* Field search covers the itinerary's own fields — the time, the distance,
  the transport, the overnight, the carried-over day info — and opens the
  card and the fold a match is behind.
* A closed day card carries its time, distance, transport and overnight
  rather than a count of facts, so a run of closed cards can be read without
  opening them. Kept current by the script as the fields change.
* Pricing, the Itinerary and the media box each open with one quiet line
  counting what is in them. Nothing is computed that the screen does not
  compute anyway, and a panel with nothing in it prints nothing.
* Eight help lines that only restated their label are gone. Every line
  carrying a format or a rule is kept.

The readiness rows are read three times a page and now build a Tour to
answer the duration, so the list is memoised per post for the request.

= 1.40.2 — A quieter itinerary =

One stylesheet, assets/css/itinerary.css. No markup, no PHP, no stored value,
no colour and no behaviour differs from 1.40.1.

* The hairline between days is gone. A rule above every day drew a stack of
  horizontal lines across a component whose whole structure is one vertical
  one, and the two were arguing: the thread says the days are a single run,
  the rules said they were a list of rows. The space between days went up to
  replace it, on all three breakpoints, along with the space between the
  parts of one day.
* The thread ends properly. It was cut at the last day's badge, on the
  reasoning that the badge is the last thing on it — which is wrong for any
  last day that has moments, and left their marks floating with nothing
  joining them to anything. Cutting it at the foot of the day instead only
  moves the problem: the line then runs past the final mark and stops dead in
  the white space under it. So it is not cut at all now. It runs the length
  of the last day and dissolves over the stretch below the last mark, a
  distance that follows the rail so it stays in proportion at every width.
* The prose is sized to be read. A day's description was 15px — caption size,
  for the one long read on the page. It is now clamped between 15.5px and
  17.5px, around 16px at tablet width and 17.2px on a wide screen, with the
  leading at 1.72 and paragraph spacing at 1.05em. A moment's words, which
  are also a day trip's stop descriptions, went from 14.5px to between 15px
  and 16.5px and stay a step below the day's own body, which is what holds
  the hierarchy together when a day has both.
* Justified, from 992px up. text-align: justify with text-justify: inter-word
  and hyphens: auto — the even right edge of a printed programme, with the
  slack spent on the spaces rather than stretched between the letters, and
  hyphenation to keep it from having to. Below that width the text is ragged,
  and so it is inside the container queries, because a wide window is not a
  wide measure: the layouts that put the itinerary in a column beside a
  sticky panel would otherwise justify at phone width. Both paragraphs also
  now state text-align: start rather than inheriting, so a theme that centres
  its body copy cannot centre a travel programme — and start rather than left,
  so an Arabic page still reads from the right.

= 1.40.1 — The migration actually runs =

One fix. Nothing about the editor, the templates, the storage, Activities,
bookings or pricing differs from 1.40.0 in any way.

The activation hook ran the 4.0 prefix rename and then wrote the schema
counter at its head. That is a claim about every migration up to the current
release, and one of them had not started: the itinerary move belongs to the
admin_init pass, which then saw a site already at the head and returned. The
tours kept their itineraries in the old text field and nothing ever came back
for them. It showed up on three paths — a plugin updated while it was
deactivated and then switched on, a reinstall over an old database, and a
fresh install.

* Activation records only what it performed: the rename, which is schema 2.
* The itinerary move is recorded only by the pass that runs it, and only once
  the last tour is through. A batch cut short resumes; a request that does
  not have the module loaded records nothing rather than assuming.
* The number only ever moves forward. Deactivating and reactivating a
  migrated site leaves it where it is instead of writing it back down and
  running the move again over days somebody has since edited.
* Sites already stranded by 1.40.0 are repaired. The head moves to 4, because
  a 3 written by that build may mean the move finished or may mean it never
  ran and there is no way to tell the two apart from the number alone. Every
  site goes through the move once more: one that genuinely finished has every
  tour flagged, finds nothing to do and pays a single query; one that was
  stranded finishes properly. The move itself is unchanged — idempotent, in
  batches, and it never writes over a card somebody has filled in.

= 1.40.0 — The itinerary lives on its cards =

Until this release a day of a tour lived in two places. Its title and its
prose were a numbered line in the Itinerary text field — "1- Marrakech to
Ait Ben Haddou", a syntax somebody had to remember — and everything else it
said was on the card beside that field. Adding a day wrote a line into the
text; naming the card rewrote the same line. One value, two homes, and the
whole arrangement resting on a text format.

The cards are the itinerary now. Press Add a day, write a title and a
description, and that is a day.

What changed on the editor
--------------------------

* Title and Description are fields of the card, saved with it like every
  other field. The description takes as many paragraphs as it takes; a blank
  line starts a new one, which is what a blank line always meant.
* Add a day, or Add a stop, creates the card and nothing else. No numbered
  line is written into any text field, by that button or by anything else.
* Days can be moved and removed on the screen. The order of the cards is the
  order of the itinerary, and the field names follow after every change, so
  what is on the screen is what is saved. Removing a card that has something
  written on it asks first.
* The Itinerary text field is no longer drawn. It is shown read-only under
  the cards, folded away, so an owner can still read and copy what it holds.
* Everything 1.39.0 added is still there and unchanged: the hour-led stop
  headers, More details, the moments with add, duplicate, move and remove,
  Copy the previous day into the empty fields, Open all and Close all, the
  remembered folds and the unsaved-work dot.

What happens to existing tours
------------------------------

Schema 3. The first time an administrator opens wp-admin, every tour is
walked once and its itinerary is moved into its cards.

* The old text is read through Parser::itinerary(), the same parser the tour
  page, the print sheet and the guest pass have always been drawn with. A day
  therefore means exactly the same thing after the move as before it.
* The title becomes the card's title. The paragraphs, or the rich content,
  become its description. The hour a numbered line stated — "1- 08:30 |
  Departure" — becomes the day's start time. A day info line such as
  "Accommodation: Riad | Meals: Breakfast" is carried over as it was.
* Nothing already entered is written over. A card that states a subtitle, a
  distance, a photograph or a moment keeps it; the text answers only where
  the card is silent.
* A tour written as rich text keeps its markup. Headings, links, emphasis and
  lists survive, cleaned with the allowance a post's own content gets, rather
  than being flattened into plain sentences.
* wptm_itinerary is never written and never deleted. It stays as the backup,
  and as the fallback the front of the site still reads for any tour the move
  has not reached.
* The move is idempotent. Each tour is stamped when it is done, so running it
  again is a no-op, and saving a tour on the new editor counts as moving it.
* A large catalogue finishes over several page loads, sixty tours at a time.
  Nothing disappears meanwhile: a tour still waiting is drawn from its old
  text exactly as it was. Tripvance → System Status says how many are left.

One reader
----------

The tour page, the Luxury Journey, the print sheet, the guest pass, the
Visual Builder's composer, the REST payload, Tour::itinerary(), the itinerary
mode and the editor's own cards all ask one function now: the structured
cards first, the old text only as the fallback. Two layouts of the same tour
cannot disagree about what its days say, and the shape that function returns
is the shape every one of those consumers already read, so none of them had
to change.

Day-by-day, Single day / Timeline and Automatic behave exactly as they did.
No template, class, stylesheet or piece of front-end markup was touched.

Activities
----------

Untouched, deliberately and carefully. An activity's running order shares
this meta row — it is day zero of it — and a row written by an earlier
version is now read at the version it was written at rather than refused by
the newer one, which is the one way a version bump could have made a schedule
disappear. The migration never queries activities at all.

= 1.38.0 — Stability & Performance =

No new features, on purpose. This release is about what happens underneath a
site that is already working: how much of the plugin a page has to load, how
the booking engine behaves when two people press the button at once, and what
somebody can find out when something is wrong without having to ask.

Every tour, activity, booking, price, group rate, itinerary, builder layout,
setting, translation, guest pass and queued message is exactly where it was.
No meta key, option, table or post type was renamed. Nothing needs migrating,
and nothing is reset.

Diagnosis
---------

Tripvance → System Status is a new screen and the only visible addition in
this release. It reads and never writes.

* Tripvance: the plugin version, the database schema version and the one this
  build expects, whether migrations finished, and whether permalinks and the
  rewrite rules are in a state the tour pages, the guest pass and the customer
  quote page can work with.
* WordPress: version, PHP version, memory limit, WP_DEBUG, SCRIPT_DEBUG,
  WP-Cron, whether a persistent object cache is in use, the database server.
* Database: every table the plugin owns, present or missing, and every index
  its queries depend on.
* Bookings: five checks over the reservation ledger, reported as counts and
  nothing else — seats held by a booking that was never written, ledger rows
  pointing at a booking that is gone, bookings whose ledger row never got its
  reference, ledger rows whose status disagrees with the booking's, and
  departures holding more seats than their capacity.
* Email queue: waiting, sending, retrying, given up on, stuck, sent,
  cancelled, and when the queue last ran.
* Scheduled tasks: when each of the four events this plugin owns next runs,
  whether any is overdue, and whether any is scheduled more than once — which
  is how one traveller ends up with two reminders and is the sort of thing
  that survives a migration unnoticed.
* Integrations: Polylang and its languages, Rank Math, ACF, WooCommerce.

Copy diagnostic report produces the same information as plain text. It carries
no customer name, e-mail address, telephone number, booking detail, API key or
secret, and it says so at the bottom, so somebody can paste it into a public
forum and see for themselves that it was safe to.

Loading
-------

A front-end request parses roughly ten thousand fewer lines of PHP than it did
in 1.37.0. Nothing was deleted to achieve it: the code was moved to where it
is asked for.

* includes/settings.php was 1,824 lines, of which about 1,500 were the screen
  that edits the settings. That screen is now includes/admin/settings-screen.php
  and loads in wp-admin only. Reading a setting, cleaning a submitted one and
  the spacing class on the body stayed where they were.
* includes/i18n.php was 4,543 lines, nearly all of it three tables. Each is now
  a data file loaded on first use, and the built-in French, Spanish and Arabic
  tables are not opened at all on a site serving English.
* includes/luxury/composer/schema.php was 3,763 lines of two arrays that are
  only read when somebody is editing or rendering a Luxury Journey. Same
  treatment, same values.
* includes/home/render.php kept the dozen functions other modules call and
  moved its thirty-six section renderers into render-sections.php, which
  Home\section() loads before it dispatches. A tour page, an archive or a
  Luxury journey no longer parses two thousand lines of homepage.

Booking and security
--------------------

* Rate limiting is atomic where it can be. On a site with a persistent object
  cache the counter is incremented in the cache rather than read, incremented
  in PHP and written back, so a burst of parallel submissions is counted once
  per submission instead of once for the burst. On a site without one — the
  default WordPress install — the transient path is unchanged, deliberately:
  wp_cache_incr() on the non-persistent cache forgets everything at the end of
  the request, which would switch metering off on exactly the sites that have
  no other protection.
* A new Advanced setting names where the client address is read from:
  REMOTE_ADDR (the default), Cloudflare's CF-Connecting-IP, or X-Forwarded-For.
  Only REMOTE_ADDR is trusted unless an administrator says otherwise, because a
  forwarded header is a value the sender chooses; a site that is not behind the
  proxy would be handing every script an unlimited allowance. Whichever is
  chosen, the value is validated as an IP address and falls back to the
  connection if it is not one, and the left-most entry of a forwarded list is
  the one used. The booking form, the date lookup, the price calculator, the
  guest pass actions and the quote page now all read the same answer.
* The schema installer is held to one attempt an hour when there is work to
  do. It was running unguarded on admin_init, which on a site whose database
  user may not CREATE TABLE meant dbDelta over seven tables on every single
  wp-admin request. The front-end path already had that guard; both now share
  it. table_exists() is also answered once per table per request instead of
  once per call.
* Every REST route, AJAX handler and admin-post action was reviewed for its
  nonce, its capability, its sanitising, its escaping and its use of prepared
  statements. No missing check was found. Two routes are documented where they
  were not: the Luxury content search states which screens it serves and why it
  asks for the lower of their two capabilities, and the import endpoint states
  that it stays on manage_options.

Assets and build
----------------

* booking.js, group-pricing.js and booking-panel.css were shipping without
  their minified copies, so the booking panel — the one page where size matters
  most — served the readable files. All three are built.
* bin/verify-build.mjs refuses a release with a missing or stale minified file,
  with a minified file that is not smaller than its source, with a version that
  disagrees between the plugin header, WPTM_VERSION, readme.txt and this file,
  or with any file that does not parse. bin/build-zip.sh will not package
  without it, and CI runs it on every push.
* bin/build-assets.mjs rebuilds every minified file from its source, and CI
  checks that a fresh build changes nothing.

Stylesheets
-----------

The design is unchanged. Every removal below was verified to leave the
computed value of every remaining declaration exactly as it was.

* The itinerary's old timeline component was replaced in 1.36.0 and its CSS was
  never removed: 74 rules and 79 selectors in tour.css that nothing could
  match. Gone, with the two grouped selectors that mentioned it trimmed rather
  than dropped, so the FAQ and gallery rules that shared them are untouched.
* 133 declarations across ten stylesheets were overridden by a later rule with
  the same selector in the same context and could never apply. Gone.
* The homepage and the car hire page guarded their full-width bands with a rule
  using :has(), which a browser without :has() support discards entirely —
  leaving a sideways scrollbar on the two pages most likely to be looked at on
  a phone. The guard is now also stated with the template's own body class,
  with a fallback for browsers that lack overflow: clip.
* One right-to-left fix in home.css: the gap between a guide's category and its
  date was a physical margin-left and sat on the wrong side in Arabic.

One defect found while reading
------------------------------

`get_terms_args` is a filter any plugin on a site may attach to. One that
forces `fields` to `ids` or `names` for its own reasons leaves integers or
strings where the homepage and the car hire page both read `$term->term_id`
and hand the object to a function typed `\WP_Term`. The result is a
TypeError on the front page: a white screen, caused by a plugin that has
never heard of this one. Four call sites now go through one guard,
`tripvance_term_objects()`, which keeps only real terms; the worst case is a
missing tile on a page somebody can still read. Nothing changes on a site
where no such filter exists.

Tests
-----

* A unit suite that needs no database, no WordPress and no Composer: run it
  with `php tests/run.php`. 127 cases over the price calculator and its bands,
  the itinerary shape rules, the booking status map and which statuses hold a
  seat, the rate limiter on both storage layers, the client-address rule, the
  settings sanitiser, the mail retry policy and what it is allowed to store,
  and the diagnostic report's redaction. Each file runs in its own process,
  because this plugin memoises on purpose and a shared process makes a suite
  depend on load order.
* An integration suite for what only a database can answer: the last seat
  contested by two requests, the conditional insert holding when the advisory
  lock is taken away, a transaction rolled back after a throw, the lock
  released however the function leaves, an idempotent retry resolving to the
  first booking, the mail queue's claim, retry, backoff and attempt limit, the
  homepage builder recovering from malformed input, and an upgrade from an
  earlier schema with every tour, reservation and setting intact.
* Neither suite ships. tests/, bin/, .github/, composer.json and the PHPUnit
  configuration are all excluded from the release ZIP by .distignore, and CI
  fails if any of them reaches it.

Release candidate review
------------------------

A second pass over the finished release, fixing only what was wrong.

* uninstall.php said one thing and did another. Its comments claimed the
  settings row "has already gone above" when the code deliberately keeps it,
  named two Luxury flags where three are deleted, and promised to name term
  meta rows that it correctly does not touch. The retention policy is
  unchanged — no table is dropped and nothing the owner typed is deleted —
  but the file now says so accurately, names every option it keeps, and
  states the rule for anybody adding a line to it. A test reads the file and
  checks each promise against the code, so the two cannot drift again.
* System Status told a site with no tables that its indexes were "all
  present". There were none to look at; it now says so, and says how many it
  did check when there were. A memory limit of -1 reads as "no limit" rather
  than as a number, and a site whose address cannot be parsed prints
  "unknown" rather than an empty line.
* The customer directory read the requested sort column twice, once to test
  it against the allowlist and once to use it, with a small difference
  between the two expressions. Both were correct; writing it once removes the
  way that stops being true.
* The build check now covers blocks/ as well as assets/. `minified_src()`
  serves anything under the plugin's own URL, so a stale editor.min.js there
  would have been served to the block editor exactly as a stale stylesheet
  would be to a visitor — and it was not being checked.
* blocks/tours/editor.js is empty, and now says why: the Tours block is
  registered from PHP with every other section block, and block.json beside
  it is the readable record rather than the source of the registration. Kept
  rather than deleted, so nobody has to work it out a third time.

Nothing else was changed. The itinerary, the booking engine, the database
schema, every meta key and every option name are exactly as they were.

Verification
------------

* 202 unit cases, 2,078 assertions, all passing, with no PHP diagnostic of
  any kind emitted by any of them.
* The whole plugin — core, admin and importer, 306 files — loads on PHP 8.4
  under E_ALL with zero warnings, notices or deprecations. No parameter in
  the plugin is implicitly nullable, which is 8.4's own deprecation.
* The itinerary is rendered and read back for every shape that matters:
  multi-day, single-day, a one-day activity, an activity told only in hours,
  an overnight, an untimed running order, and each of Automatic, Days and
  Timeline. Automatic never draws day numbers for a single day or for hours.
* The diagnostic report was generated against a database rigged to answer
  every query with a customer's name, address and telephone number. None of
  it reached the report, because the report is built from counts and
  literals; the same check now ships as a test.
* Every one of the ninety-odd request handlers was re-read for its nonce, its
  capability, its sanitising and its SQL. Nothing was missing. The seven
  public endpoints are now listed by name in a test, each with the reason it
  takes no capability, so an eighth is a decision rather than an omission.
* Every fixed width wider than a 320px phone was traced to the scroller that
  contains it. The mobile booking bar is printed hidden and measured by the
  script, so with JavaScript off there is nothing to cover the page.

Continuous integration
----------------------

PHP 7.4, 8.0, 8.1, 8.2, 8.3 and 8.4: every file parses on each, and the unit
suite passes on each. JavaScript is parsed, the build is verified, a fresh
asset build is checked to change nothing, the ZIP is built and inspected, and
the integration suite runs against MySQL on three PHP and WordPress pairings.
Coding standards run as a report rather than a gate.

= 1.37.0 — Evidence, not a guess; and the cards come off =

Two things 1.36.0 left half done.

The first: its automatic rule still fell back to days. A tour that stated no
duration and whose stops carried no times was read as a run of days, which is
exactly the tour a day trip usually is — somebody fills in the title, the
price and the stops, and never touches the Days field. The rule is now one
sentence, and the asymmetry in it is deliberate: a multi-day trip drawn as a
timeline is a plainer page that still reads correctly, while a day trip drawn
as days tells the visitor they are buying a week and somewhere to sleep. Only
the second is the page stating something untrue, so "days" is never the
fallback — it is claimed on evidence, and on nothing else.

* Days requires one of: more than one day, at least one night, a preset naming more than one day, or an overnight or stay written on a day of the itinerary. Everything else is a timeline, and the Itinerary format setting still overrules all of it.
* The heuristics that read the shape of the itinerary text are gone, and the file is shorter for it. Guessing from how many stops there are, or how many wore a clock, was the part that produced the wrong answer.
* The Visual Builder's `day()` renderer asked nobody. It printed "Day %s" for every row it drew, so a single-day journey composed there numbered its stops as days no matter what the rest of the plugin had decided. It now asks `Itinerary\mode()` like everything else, and a number typed into the row by hand is still honoured.

The second: the itinerary was still a stack of cards, and the new single-day
timeline was not, so one component looked like two.

* A day is no longer paper, a border, a radius and a gap. It is a stretch of the page with a hairline above it, its number on the thread, and its words beside it — one unbroken line from the first day to the last, which is what the timeline already was and what a printed programme looks like.
* The `--steps` variant and its stylesheet are gone. No code path printed that class any more; both shapes are now days or a timeline.

= 1.36.0 — A day trip is not seven days =

The itinerary had one shape: a run of days. That is right for a journey of
a week and wrong for everything that happens between breakfast and dinner,
and a full-day excursion written 08:30, 10:00, 11:30 was therefore
announced to the reader as DAY 01 to DAY 06.

* Two shapes now. Multi-day is unchanged — the numbered badge, the day title, the day's figures. Single-day is a timeline: the hour leads the line, the title follows it, a hairline threads the stops together, and nothing on it is numbered.
* One place decides which. `Tripvance\Itinerary\mode()` answers it for the section template, the Luxury Journey page, the printed sheet, the guest pass and the activity schedule. The Luxury page used to hardcode days, so the same tour could read two ways on two layouts of one site.
* A new optional meta, `wptm_itinerary_mode`, holds a deliberate choice: Automatic, Multi-day trip, Single day / Timeline. Automatic is the default and what every existing tour has; it reads the duration first, and when the tour states none, the shape of the itinerary itself.
* Nothing is migrated, converted or deleted. No stored value changes on update, and a tour saved from the new panel posts exactly the fields the old one did — the two modes were checked field for field.
* The itinerary parser reads "1- 08:30 | Departure from Marrakech" as a numbered stop with a time. It never had: the guard that keeps "2 - 3 hours of walking" and "15 - 20 minutes on foot" inside their sentences also refused this, the form the field's own help text has documented for years. The guard is untouched — the new rule fires only on a real clock followed by a separator, and the FAQ's rules are not involved.
* Progressive disclosure in the panel. A stop leads with its time, a day with its one-line note; the road, the night, the photograph and the named moments sit behind More details. Every field still renders and still posts, so nothing can be lost by not opening a fold.
* Moments can be moved, duplicated and removed with real buttons, and their order is renumbered as they move, so the screen and the page agree.
* A day or a stop can carry one photograph of its own, in place of the gallery picture at that position.

= 1.35.2 — The last warning, and a steadier mark =
* The last Plugin Check warning cleared: the panel flags are read and sanitised in one statement, with the nonce the caller already verified named alongside it.
* An area clicked in the section navigation keeps its mark while the page travels to it. The mark is normally moved by what is on screen, and a smooth scroll crosses two or three boxes on the way.

= 1.35.1 — Plugin Check and palette clean-up =
* Two findings from Plugin Check cleared: the panel flags are sanitised where they are read, and the duplicate action states why it reads the tour id before checking its nonce.
* The last of the WordPress blue inside the Tripvance parts of the tour editor is gone: the section navigation, the readiness list, the switches, the pickers and the gallery controls all use the plugin's own green.
* This file rewritten as a plain list of releases rather than development notes.

= 1.35.0 — Stability and Tour Editor refinement =
* Stability release before publication: no new features, and no change to how anything is stored.
* A saved value a dropdown no longer offers — a layout from a theme that has gone, a value written by an import — is kept instead of being erased on the next save.
* A numbered FAQ question whose text begins with a number is read as a question again. Itineraries are unchanged: a figure inside a line still never starts a day.
* Numbers that count something can no longer be saved below zero.
* Labels on the rich text fields point at the field they name.
* Larger tap targets in the editor: remove a row, remove a photograph, switch a table to text, and every row of the readiness list.
* The section navigation, the readiness list and the editor's own controls use the Tripvance palette throughout.
* Duplicated styling from the previous four releases removed; every rule on the tour screen scoped to that screen.

= 1.34.0 — Tour Editor redesign =
* The tour screen reads as an editor rather than a stack of boxes: warm background, white cards, clear spacing, and a header per card with an icon, a title and a line of explanation.
* Overview becomes two cards — Tour identity (destination, type, hero image, duration, short description) and Trip essentials (group size, difficulty, languages, departure and end point). Content becomes Experience.
* The itinerary and its day cards are one section: the text, then a card per day with route, distances, transport, overnight, meals and named experiences, each opening and closing on its own.
* An Add a day button writes the next numbered day into the itinerary text.
* Pricing opens with what the tour costs, above the fields that decide it.
* Tour media moves into the main column, with the main image named and the gallery given room.
* Layouts are chosen from miniatures instead of a dropdown.

= 1.33.0 — Section navigation moves to the sidebar =
* The section navigation is a card in the sidebar, so nothing on the screen can cover a field while scrolling. Each area shows a tick when its essentials are filled in and a mark when one is missing.
* The sidebar follows the page when it fits on screen and scrolls with the page when it does not; below 851px it becomes ordinary navigation above the fields.
* A bar above the title carries the tour's status, how complete it is, when it was last saved, and Preview, Duplicate, Bookings and Update.
* Every box on the tour screen has a card header; Tour status and Tour readiness become one card.

= 1.32.0 — One editor, pricing tables, duplication =
* Tripvance's own editor is the tour screen on every site; ACF, where installed, is a data layer over the same meta keys. One filter restores the previous arrangement.
* Group rates, optional extras and seasons are tables: a row per band, a column per part, add and remove buttons. The stored lines do not change, and Edit as text is one click away.
* Destination and tour type are fields in the overview panel; the raw custom fields table is removed from the screen.
* A sidebar card shows the tour's picture, destination, duration, price, booking mode, status and completeness; the readiness list names what is missing and each row jumps to its field.
* Tours can be duplicated from the Tours list: content, fields, itinerary and terms are copied into a new draft.

= 1.31.0 — Safer saves in the tour editor =
* A save the server cuts short can no longer undo a tour: each panel ends with a flag, and a panel whose flag did not arrive is left exactly as it was. You are told which areas were dropped, and what to ask your host for.
* Leaving the editor with unsaved changes now warns you.
* A field finder at the top of the editor filters the screen to the fields that match, opening whatever fold or box they are in.
* Ctrl+S, or Cmd+S, saves the tour.

= 1.30.1 — Plugin Check fixes =
* Translator comments added to the party-size strings, and one admin attribute escaped at the point it is printed. No behaviour changed.

= 1.30.0 — Activity page visual pass =
* The activity page is laid out around its photographs: the title, the facts and the picture in one band, with a View photos button on the picture.
* At a glance is one ruled strip; every section carries a line under its heading.
* Highlights and good-to-know notes are cards; what the price covers is two tinted panels.
* The meeting point, cancellation policy, important information and questions stand beside the booking panel on a wide screen.
* A meeting point can carry a photograph.

= 1.29.0 — Activities become full products =
* An activity page now answers what a tour page answers: starting time, group size, languages, pick-up, meeting point, what to bring, cancellation and the questions — each shown only when filled in.
* A new Activity schedule box writes the running order hour by hour, drawn as the same timeline the tour itinerary uses.
* An activity with fixed start times offers them on its booking form, and the chosen time is stored with the request.
* The packing list, the good-to-know note and the FAQ accordion are the tour page's own blocks, borrowed rather than rewritten.

= 1.28.0 — Moroccan Editorial Carousel =
* Three tours across by default, with cards that are never squeezed below a readable width and titles that are never cut.
* The band can be laid on a picture of your own, with the colours you chose becoming a wash over it.

= 1.27.0 — Manual bookings and request sources =
* Bookings can be entered by hand: same reference, same ledger, same board, same voucher, for the requests that arrive by telephone or WhatsApp.
* Every booking records where it came from — a search engine, a social network, a campaign, another website, or direct. No cookie, no third party, and it can be switched off.
* The tour editor gained a navigation card, Collapse all, and the itinerary panel beside its day facts.

= 1.26.0 — Premium itinerary layout =
* The day-by-day section is redrawn: a day badge, a step indicator, an information bar and a timeline of the day's moments, each with its own icon.
* Photographs beside the steps, badges on the moments, and an accordion on a phone.
* Everything remains readable to search engines, and nothing about the itinerary text or its storage changed.

= 1.25.0 — Tour editor navigation, editable footer column =
* The tour edit screen reads as one editor: a navigation card, a readiness card, and one stylesheet across every box. Same metaboxes, same save request, same meta keys.
* The footer's Journeys column is written by the owner rather than generated.

= Earlier releases =

1.24.1 — The Journey Composer admin box dressed to match the rest of the screen.
1.24.0 — A wider Included highlight, tighter experiences, and an admin box that follows the editor.
1.23.1 — The day facts strip drops its frame and joins the day's composition.
1.23.0 — The itinerary and its details become one design, and the tour screen reads in order.
1.22.0 — Structured day facts and experiences, and paragraphs that stay paragraphs.
1.21.0 — Stability release: five audit passes, the three-area editor, and a plugin that deletes nothing you wrote.
1.20.0 — Builder saves that cannot be cut short, a lossless Gutenberg round-trip, and search instead of long catalogues.
1.19.1 — Plugin Check clean: translator comments on every placeholder string.
1.19.0 — A shorter Luxury footer, a mosaic that closes its last row, and a settings screen you can search.
1.18.0 — The footer on every page and edge to edge, and a settings screen with Save always in reach.
1.17.0 — The Luxury footer becomes the foot of an agency's page.
1.16.2 — Fixes a fatal error on the homepage.
1.16.1 — Hardened admin and enquiry input boundaries.
1.16.0 — A cinematic opening that is a film first, and a homepage curated rather than filled.
1.15.0 — Custom HTML anywhere, a framed Our purpose, and a hero that meets the header without moving.
1.13.0 — Tripvance Visual System 2.0: shared admin primitives, operational dashboard, saved-price preview.
1.12.1 — Custom Experiences, landing translations, and Featured Tours refinements.
1.12.0 — A fourth featured-tours layout, offered in two places and drawn by one renderer.
1.11.0 — Three tones for the Luxury Editorial experience, and every colour measured against what is behind it.
1.10.0 — The Luxury Editorial experience gets a homepage of its own, with a film in the hero.
1.9.1 — Stabilising 1.9.0: eight corrections to the Luxury framework, no new features.
1.9.0 — The Luxury Editorial experience: a second presentation of the same catalogue.
1.8.13 — The bookings dashboard rebuilt around the question it is opened to answer.
1.8.12 — Booking response reliability and private-document protection.
1.8.11 — A third Why choose us layout, and square featured photographs shown whole.
1.8.9 — Card grids keep their own height; the sidebar information cards respond.
1.8.8 — Good to know in the column, Need help redrawn, Book via WhatsApp.
1.8.7 — Tour cards redrawn, with a strip of gallery photographs.
1.8.6 — Homepage dialogs centred; coding-standards pass.
1.8.5 — Booking management and Guest Pass preparation.
1.8.0 — Visual Builder 2.0: edit the homepage and every starter page with a live preview.
1.7.7 — Full-width pages, and search-engine tools in the page builder.
1.7.6 — A page builder for every page made from a starter.
1.7.5 — The homepage and Site pages screens rebuilt on one shared framework.
1.7.4 — Car hire settings, rebuilt for an owner with no technical background.
1.7.3 — The navigation language switcher sits on the same baseline as the menu.
1.7.2 — The car hire builder gains a setup checklist and quick navigation.
1.7.0 — Activities become products: an activity can carry its own price and take booking requests.
1.6.1 — Hardening release for pricing, scheduled departures and the operations workflow.
1.6.0 — The operations release: quote, acceptance, payment and voucher inside WordPress.
1.5.0 — The Digital Guest Travel Pass.
1.4.7 — Stability and performance release. No visual change and no change to stored data.
1.4.6 — Travel Guides.
1.4.5 — Homepage and language switcher refinements.
1.4.4 — Language switcher refinements.
1.4.3 — Homepage builder refinements.
1.4.2 — Homepage refinements.
1.4.1 — SEO hardening: Schema defaults, filter URL handling, archive duplication protection.
1.4.0 — Brand identity: colours, typography and presets.
1.3.0 — Homepage refinements.
1.2.0 — Homepage refinements.
1.1.2 — Language switcher refinements.
1.1.1 — Language switcher, visual and mobile refinement.
1.1.0 — The optional language switcher.
1.0.0 — First public release.


== Appendix: release notes as previously published in readme.txt ==

Until 2.7.0 readme.txt carried its own, differently worded copy of the
release history and every upgrade notice since 1.35.2. They are kept here
verbatim so nothing that was published is lost; readme.txt now carries only
the current releases.

=== Changelog (readme.txt, 2.5.1 and earlier) ===

= 2.5.1 =
* Hotfix: Improved stability and responsive layout of the Homepage FAQ section.
* Fixed inconsistent FAQ card heights and layout shifting when displaying answers.
* Added a safe manual upgrade option for Legacy Tours to use the New Tour Editor while preserving existing data and legacy metadata.

= 2.5.0 =
Tripvance 2.5 focuses on administrator workflow, editor stability, UI consistency, draft/publish actions, validation feedback and overall admin polish. It is a refinement release: tours, activities, transfers, bookings, pricing, group rates, quotes and the Guest Pass work as before, product pages keep their design with a finishing pass on the gallery, spacing and review form. One conservative migration runs on upgrade: a migrated itinerary still stored as a single untouched collapsed day is rebuilt from its original text; edited cards are never replaced and the original text is kept.

**Editor layout**

* The tour and activity editors are laid out by the server and by CSS from the first paint: the section navigation is printed in the left column, the section on screen is chosen before the page is drawn, and the boxes of the other sections are marked off as WordPress prints them. The page no longer opens as WordPress's two-column screen and then rearranges itself. Without JavaScript every section is shown and the navigation links jump to each.
* The editor reopens on the section you were working in after a save.
* The 1.x "Tour sections" side card is no longer registered where the section navigation replaces it, so the same controls are not drawn twice.
* Conditional fields, picker chips and the field finder are decided on the server, so nothing moves when the script arrives. The setup and welcome panels are no longer shown above the editor.

**Save, publish and status**

* One action bar that follows the post's status: Save draft and Publish for a new or draft post, Update for a published or scheduled one, Save as pending and Submit for review where WordPress's permissions call for them. Preview, Duplicate, View bookings, Move to draft and Move to trash sit in a More menu and appear only where they apply.
* The buttons press WordPress's own Save Draft, Publish and Update, so nonces, capability checks, autosave and revisions are WordPress's; they also work without JavaScript. Enter in a field still saves as it always did.
* Status is shown in words — New tour, Draft, Pending review, Scheduled (with its date), Published, Private — never "auto-draft".
* The save state reads Not saved yet, Unsaved changes, Saving… or Saved just now / N minutes ago, and only says "saved" after the server has confirmed it. The leave-page warning no longer fires after a save or when nothing changed, and a preview no longer clears it.
* Ctrl/Cmd+S saves a draft as a draft instead of publishing it.
* Duplicate is available for activities as well as tours, always creates a draft, and no longer copies the original's Polylang translation group or its import and restore history. Bookings were never copied and still are not.

**Completion and validation**

* One completion definition: the percentage counts the essentials only; recommended items are listed without holding it down, and optional sections such as FAQ never count against it. Activities now distinguish essential from recommended rows too.
* "N items need attention" opens a short list of actions — Choose a destination, Set a price, Add a hero image — each of which opens the right section and puts the cursor in the field.
* Sections show one set of marks: complete, needs attention, incomplete, must be fixed, or nothing for an optional section.
* Saving a draft is never blocked. Publishing and updating stop only for what would break the page — a missing title, a malformed number or address — with the reasons listed under the bar and marked on the section, never in a browser alert. A first publish without a title is also kept as a draft on the server.

**Consistency and accessibility**

* One icon family (Dashicons) with one size and alignment policy, one button shape with primary, secondary, menu and destructive roles, one chip style, and a shared spacing scale. The admin uses one accent colour across the editor, bookings, operations and transfers.
* Same-named terms in the editor's term lists — one per language, or duplicates left by an import — are shown with their language, parent or slug. No term is merged or deleted.
* Keyboard support for the More menu, the attention list, the section navigation and the field finder; visible focus everywhere; states are never told by colour alone; logical CSS properties for right-to-left screens; no horizontal scrolling at narrow widths.

**Product pages**

* Tour galleries: every photograph is in the thumbnail strip, which scrolls sideways with the next picture showing at its edge and previous/next arrows once there is more to see; the selected thumbnail stays clear and in view. Small sets (two to six photographs) fill the row instead of leaving a gap, with no arrows when everything fits. Activity galleries mark the last frame "+N" when the viewer holds more photographs than the grid shows.
* Product pages: no empty "related tours" block (and its reserved blank space) when there is nothing to suggest; the classic layouts get a content width and gutter on block themes; the "prices checked" date wraps inside a narrow booking card.
* Reviews: the logged-in line reads as one sentence, the rating select is sized to its options, the submit button uses the plugin's primary style, and the summary and form line up with the review list in every layout (full width on tablets and phones).
* Itinerary and FAQ: text pasted from Word, Google Docs, AI tools or other plugins no longer collapses into one day or one question. Numbered days or questions run together in one paragraph ("2- Day two" or "2-Day two") are separated when the numbering is unambiguous, a day recovered that way as "Title / Description" keeps a short title, and an ordered list keeps its sequence; ranges, times, dates and prices never open a day, and bullet lists stay inside their answer. Content that already read correctly is read exactly as before. Migrated tours whose single collapsed day was never edited are rebuilt from their original text once, on upgrade; edited cards are never replaced and the original text is kept.

**Structured data and SEO**

* Tour and Activity pages: Tripvance adds at most one Product, describing that tour or activity only — name, URL, image and short description where the page has them, the bookable price as one Offer at the public "From" price (the fixed price, or the lowest valid Group Rate), and the aggregate of approved on-site reviews where the page shows it. Prices come from the same pricing code as the page and the booking engine; tours on request, incomplete rate tables and seasonal discounts get no price. No availability, SKU, brand, shipping or return data is invented.
* With Rank Math active the data goes into Rank Math's graph: a Product Rank Math already builds for the page is enriched in place, otherwise one Product is added to that graph. Nothing else in the graph is changed. Without Rank Math one JSON-LD Product is printed.
* Removed: the homepage graph (TravelAgency, WebSite, WebPage, FAQPage, ItemList) and its setting, and the ratings and individual reviews previously added to Rank Math Service, TouristTrip or Trip entities. Legacy settings are preserved. The old rating_schema value is read only to preserve an existing rating opt-out until the new product_rating preference is saved.
* The "Stars in search results" setting is now "Search result product data", on by default, with a separate "Include the average rating from approved reviews" option. A site that had switched the old rating option off keeps ratings off; the old value is kept. It makes pages eligible for rich results; search engines decide what is shown.
* Rank Math sitemaps: the empty-terms switch is no longer forced on for every taxonomy on the site; only empty Tripvance terms are left out.
* The page builder no longer writes or clears Rank Math's SEO title, description and focus keyword; with Rank Math active they are shown read-only and edited in Rank Math.

= 2.1.0 =
Transfers with reusable Vehicle Types, Fleet links in Operations, transfer-aware booking documents, and an optional Privacy & Cookies module. Tours, activities, group rates and their pricing are unchanged, and nothing is migrated: existing bookings and settings keep working.

**Transfers**

* New Transfers post type (`wptm_transfer`, /transfers/{slug}/): route, pickup and drop-off types, estimated duration, distance, operating hours, minimum booking notice, pickup and meeting instructions, inclusions, important information, cancellation and gallery. An optional transfer archive (Settings), off by default. No schema is printed; the SEO plugin owns the markup.
* Vehicle Types (`wptm_vehicle_profile`): reusable customer-facing vehicle options with category, class, passenger and luggage capacity, features and a picture. Private data, not public pages.
* Transfer pricing: each transfer sets its own price per Vehicle Type. A round trip is two journeys at the same price; there are no distance, surcharge or discount rules. One pricing service feeds the page, the server-side check and the price stored with the booking.
* Transfer booking form: Vehicle Type cards, one way or round trip, pickup date and time, passengers, luggage, optional flight details and addresses. Passenger capacity is checked in the browser and on the server; minimum notice is enforced on the server.
* A transfer request is an ordinary booking record (same statuses, history, reservation ledger, messages and confirmation), marked `_wptm_booking_type = transfer`, with its facts under `_wptm_trf_*` and its price snapshot stored once.
* The price snapshot stored with a transfer booking is worded in the booking's language (one way, round trip); amounts are unchanged and earlier bookings are not rewritten.
* The WhatsApp follow-up after a transfer request carries the transfer's facts — reference, transfer, route, pickup and drop-off, pickup date and time, passengers, Vehicle Type and return — to the site's WhatsApp number. Tours and activities keep their message.
* Homepage: a new Transfers section, off until ticked in Page layout. Latest transfers or transfers chosen and ordered by hand, a heading and a line, and cards with the route, Vehicle Types, capacity and the "from" price per vehicle from the transfer pricing service, linking to each transfer page. Existing homepages render unchanged.

**Operations and Booking Admin**

* Operations → Vehicles is now labelled Fleet, and Transfers → Vehicle Types holds the customer-facing options. Post types and meta keys are unchanged.
* A fleet vehicle can be linked to a Vehicle Type, whose capacity and class it then reads (an existing seat figure still wins). Optional brand, model and operational status (Available, Assigned, Unavailable, Maintenance).
* Assigning a fleet vehicle to a transfer: a warning when it does not match the requested Vehicle Type or is Unavailable or in Maintenance; refused when it cannot carry the passengers. The requested Vehicle Type is never overwritten.
* Transfer bookings show a Transportation block (route, requested Vehicle Type, passengers, luggage, assigned fleet vehicle, driver), a booking type filter in the list, and Trip details with pickup date, pickup time and one Passengers field kept equal to the reservation ledger.
* Quotes, the Guest Pass, the voucher, check-in, the operations board and the personal-data export read transfer bookings in transfer terms, and quotes start from the price stored with the booking rather than tour rates. The Guest Pass and quotes no longer read a transfer as a tour.

**Privacy & Cookies**

* Optional consent banner, off by default and loading nothing while off: Tripvance → Settings → Privacy & Cookies. Three layouts, four categories (Necessary, Analytics, Marketing, Preferences), editable wording, RTL and keyboard support.
* Scripts placed in the Analytics, Marketing and Preferences boxes, or tagged `type="text/plain" data-wptm-consent="…"`, run only after their category is accepted. Consent is kept in one first-party cookie, `wptm_consent`.
* Campaign attribution (`wptm_attr`) now honours the Analytics choice made in Privacy & Cookies. When a consent manager runs the WordPress Consent API in opt-in mode, its statistics answer is final and a refusal there is never overridden by Privacy & Cookies; the `wptm_attribution_consent` filter still has the last word. Without consent it stays off.
* The module helps collect and honour consent; it does not by itself make a site legally compliant.

**Display**

* Homepage: the hero no longer jumps under the theme header, spacing in rails and lists is restored, and search fields have a 44px touch target.
* Tour and activity pages: the price, its basis and the group-rate note read as one phrase; phone bar and quick-fact layout fixes. Display only.

= 2.0.1 =
Visual refinement of the tour page and the booking screens. No change to the pricing engine, the booking workflow, the stored data or the meta keys.

* Tour header: title, place, rating and the starting price read as one opening; the quick facts sit under the gallery as one strip.
* Gallery: the picture takes the reading column on a desktop, the thumbnails are an even strip, and a "View all photos" button on the picture opens the viewer, which now shows where you are in the set ("3 / 8") and follows the reading direction.
* Booking panel: reads in the order of the decision — From price, date and travellers, the rate for that party, extras, the estimated total, then the button — with the site's own reassurances and reply time under the button. Every figure still comes from the server.
* Fix: optional extras in the two-step form were drawn as large empty boxes with upper-case labels; they are real checkboxes with their price.
* Fix: a form reopened at step two after an error did not fill its summary.
* Phone bar: more compact, safe-area aware, and hidden while a dialog or the photo viewer is open.
* Sections: one spacing and type scale for Overview, Itinerary (multi-day thread and timed day timeline), Included / Not included, Important information, questions, reviews and related tours.
* Reviews: a summary card with the average as stars, and each review as a card with the name, date and stars.
* Fix: a single related tour grew to the width of the page; the rate table on SalmaDesign split its header on phones; sections below the booking panel carried a double gutter on phones; the page ran edge to edge on block themes.
* Bookings list: a Total column (accepted quote, else the price worked out with the request), status views with counts, a "Move to" menu for a safe status change from the list (same transitions, history and hooks as the booking screen), and a readable layout on phones.
* Booking screen: the request is set out as Booking, Trip, Customer, Pricing (line by line from the stored price snapshot) and Notes, each reachable from the header.
* Fix: group-rate input uses one decimal parser for dot/comma decimals and grouped thousands; displayed tiers retain cents.
* Admin: non-blocking warnings for group-rate gaps, overlaps, reversed ranges and invalid prices; saved values and engine selection rules are unchanged.
* Reviews: full, true half and empty SVG stars use the existing icon set, with RTL and accessible rating text.

= 2.0.0 =
Tours and Activities become one Experience Builder, while staying two post types with their own URLs and designs. No meta key moves and nothing is migrated.

* Experience Core: one internal API for the fields tours and activities share (media, pricing, itinerary, inclusions, booking, FAQ, important information, sections, presentation), mapped onto each type's existing meta keys.
* Experience Editor: Basics, Media, Pricing, Itinerary, Inclusions, Booking, FAQ, Additional information and Publish, with a left navigation, one section at a time and marks on incomplete sections. Same form, nonces and save as before. Posts written with blocks keep the block editor; a setting turns the editor off.
* Itinerary: multi-day, single-day timeline, or a new simple route (Marrakech → Atlas Mountains → Ourika). Day cards are kept when switching.
* Pricing: booking forms show how the total is reached ("300 MAD × 4 travelers = 1,200 MAD", or group price and total), from the server's quote. The editor previews it.
* Booking requests: children, hotel / pick-up, preferred language and notes on every form; Pending is now labelled New. Statuses, the ledger and Guest Passes are unchanged.
* Section manager: show, hide and reorder a page's sections, per tour or activity.
* Media: optional mobile hero image (served with picture, no cropping), Select / Replace / Remove pickers, video on activities.
* Layouts: Adventure joins Classic and Editorial for tours; activities gain the same three, with a site default. Theme fonts are always inherited.
* Homepage: Travel Agency, Local Guide, Luxury Operator and Adventure Company presets. Presets never change content.
* Admin: a new minimal Tripvance menu icon (an SVG "T" that follows the admin colour scheme).
* Onboarding: the welcome offers Create your first Tour, Create an Activity, Configure Booking and Build Homepage, and is not shown to sites that already have tours.
* Tailor-Made Requests: a multi-step Trip Builder (shortcode, block or assigned page) with configurable steps and answers, private TMR-numbered requests with six statuses, agency and traveller emails, Create Draft Tour, and an optional tour-page call to action (off by default). No existing meta key, address or setting changes, and no page is created automatically.

= 1.42.1 =
Why Activity pages could render unstyled, and the Activity page on a phone. Nothing on a tour page changes, no meta key moves, and every activity, price, gallery and booking is where it was.

* The Activity stylesheet reaches the page. It was enqueued with two other handles named as dependencies, and WordPress prints nothing at all — with no notice and nothing in the log — when a named dependency is not registered. One handle removed by another plugin and the page shipped its full markup with no stylesheet. Both handles are now registered at the point of use and a handle that still cannot be found is dropped rather than allowed to take the page's stylesheet down with it.
* The page no longer depends on a second file for its appearance. Every panel, card, cell rule and tinted block on it is drawn with custom properties declared in tokens.css, and a custom property with no value invalidates the whole declaration that reads it — so losing that one file left At a glance with no box, Highlights with no card and the panels with square corners, which is the page looking unstyled. activity.css now carries its own floor for those values, at zero specificity, so tokens.css, the site accent and the unified brand all still win when they are there.
* Assets are versioned by the file actually served. The version string stamped the readable source while the browser was given the .min built from it, so rebuilding the .min without touching its source changed the bytes at an address that had not changed and every cache kept the old copy. A .min that is empty or a tenth the size of its source — an interrupted build — is no longer served at all.
* The Activity hero starts below the theme's header. A fixed header is painted over the top of the banner, and on a phone the breadcrumbs and part of the line above the title were underneath it. The overlap is measured from whatever is actually fixed or sticky at the top of the window, including the admin bar, so no theme's height is written into the stylesheet and a theme whose header scrolls away is unchanged.
* The Activity page on a phone. One step tighter on the spacing scale the whole page is set with, reading type at phone proportions, At a glance as compact cells with no empty one at the end of a short row, the gallery two across instead of twelve full-width frames, the meeting point stacked so an address is not four lines of two words, and the running order's clock column at the width of its badge rather than a tour's.
* What to bring, Important information and the questions are headed like the rest of the page. They are drawn by the tour page's section templates, whose title sets no colour, so they inherited the body grey and stood beside navy headings at a different size over a shorter accent rule.
* The mobile price bar cannot clip or cover. Its height is derived from its own parts and the safe area, and the page reserves exactly that much; the price never wraps or is cut, and a long button label ends in an ellipsis instead of past the card's edge. The button's label also stopped being given the icon's line-height — an old first-child selector that matched the label on the one button with no icon.

= 1.42.0 =
The Activity page and a way out of a tour that should have been an activity. Nothing on a tour page changes, no meta key moves, and every tour, activity, price, itinerary and booking is where it was.

* The Activity hero is full width. The band reaches both edges of the screen while its words stay in the same column as the reading below them, and it sits against the site header instead of under a band of white. The photograph keeps its own proportions rather than being stretched to the height of the text beside it.
* The booking rail holds the booking card and nothing else. Meeting point, cancellation, important information and the questions were being drawn in a three-hundred-and-seventy pixel column beside the form; they are sections of the reading column now, at its full width, each drawn once. The card follows the reader on a wide screen and stacks normally on a phone.
* One spacing scale for the page. Six different margins between sections became three custom properties, and a section heading now sits closer to what it introduces than to the section above it.
* At a glance carries every fact, including when the activity runs, which had a card of its own. The strip wraps into counted columns instead of forcing every fact onto one row, so labels are read at their proper size rather than shrunk to fit.
* The gallery opens. "View photos" and every thumbnail open a viewer with the full-size picture, a counter, previous and next, Escape, arrow keys and a backdrop that closes it; the page behind does not scroll and focus returns where it was. Without JavaScript each frame is still a link to its own picture.
* Tour to Activity conversion. An unpublished tour can be copied into a new draft activity from the tours list or the Publish box: title, content, excerpt, featured image, gallery, destination and place terms, and every commercial and experience field that both post types share. The tour is never modified and no media is duplicated.
* The itinerary is converted rather than copied. A tour keeps its stops as days and an activity keeps them as moments of one occasion, so a copied row arrived as an empty schedule. Stops are now read through the itinerary layer's own reader — structured, legacy text, or both — and written as schedule rows in order, with their times, titles, descriptions, locations and photographs. Anything with no field of its own is carried into the description rather than dropped, and whatever cannot be mapped is named in a notice on the new activity.
* Description and answer paragraphs are justified and set at a reading measure, ragged on a narrow column, inheriting the theme's own type.

= Earlier releases =

In brief below. The full notes for every release, including these, are in
changelog.txt, which ships with the plugin.

* **1.41.0** — The tour editor answers as you type: a live status card that says the booking mode in words, completion counted on the six things a tour needs, and field search that reaches the itinerary.
* **1.40.2** — A styling pass over the itinerary: the thread runs to the last moment of the last day, and the descriptions are set at a reading size.
* **1.40.1** — A migration fix. Activation recorded the itinerary migration as finished before it had started; stranded sites are picked up automatically.
* **1.40.0** — The itinerary is written on cards. Existing tours move across automatically, and the old wptm_itinerary field is kept as the backup.
* **1.39.0** — An editor release: days and stops appear as editable cards straight away, and Activities use the same editor as Tours.
* **1.38.0** — A stability release: less loaded on a front-end request, a correct rate limiter under load, a System Status screen and a test suite.
* **1.37.0** — A tour is drawn day by day only when it says it lasts more than one day, so day trips are no longer announced as DAY 01, DAY 02.
* **1.36.0** — Day trips stop being announced as a run of days: a one-day itinerary reads as a timeline of times and stops on every layout.
* **1.35.2** — A stability release. Tours written by any earlier version open, save and render exactly as they did.

=== Upgrade notices (readme.txt, 2.5.0 and earlier) ===

= 2.5.0 =
Admin editor polish (stable layout, status-aware Save/Publish bar, clearer validation) and a finishing pass on tour and activity pages. Pasted itineraries and FAQs no longer collapse; a one-time repair rebuilds untouched collapsed itineraries. No pricing or booking changes.

= 2.1.0 =
Adds Transfers with reusable Vehicle Types, Fleet links in Operations, and an optional Privacy & Cookies module. Tours, activities, bookings and their pricing keep working; nothing is migrated.

= 2.0.1 =
A clearer tour page and booking panel, a fix for the optional extras in the two-step form, and a tidier bookings list with totals, status views and a quick status change. No data, pricing or booking changes.

= 2.0.0 =
Experience Editor, route itineraries, live price breakdowns, a section manager, Adventure layout and a Tailor-Made Trip Builder with private requests. Nothing is migrated: tours, activities, URLs, bookings and Guest Passes keep working.

= 1.42.1 =
Take this first if Activity pages look unstyled: the stylesheet could be silently not printed, and its panels lost their appearance when a second file was missing. Both fixed. The hero no longer starts under a fixed header, and the page is retuned for phones. No Tour or data changes.

= 1.42.0 =
The Activity page, rebuilt around its reading column: a full-width hero, one glance strip, and the meeting point, cancellation and questions moved out of the booking rail. The gallery opens in a viewer. Tours can be converted into draft Activities without retyping. No Tour output changes.

= 1.41.0 =
A tour editor pass. The status card printed the raw booking mode and answered from the last save; both are fixed, and it now follows the form as you type. Completion counts the six things a tour genuinely needs. No stored value, meta key or front-end output changes.

= 1.40.2 =
A styling pass over the itinerary. The thread now runs to the last moment of the last day instead of stopping short, and descriptions are set at a reading size: justified on a wide screen, ragged on a phone. No markup, colour, stored value or behaviour changes.

= 1.40.1 =
A migration fix, and the reason to take this first if you are on 1.40.0. Activation recorded the itinerary migration as finished before it had started, so some sites kept their itineraries in the old text field. Those sites are picked up and finished automatically.

= 1.40.0 =
The itinerary is written on cards: add a day, type a title and a description. Existing tours move across automatically on the first admin page load, and the old wptm_itinerary field is kept untouched as the backup. Nothing on the front of the site changes.

= 1.39.0 =
An editor release. A day or a stop is added as an editable card straight away, the completeness meter answers as you type, and Activities now use the same editor as Tours. Nothing on the front of the site changes and no meta key moves.

= 1.38.0 =
A stability release. No feature or stored value changes. Underneath: less loaded on a front-end request, a rate limiter that counts correctly under concurrent submissions, a new System Status screen, and a test suite behind the booking engine.

= 1.37.0 =
Finishes what 1.36.0 started. A tour is drawn day by day only when it says it lasts more than one day, so day trips are no longer announced as DAY 01, DAY 02. The itinerary is now one thread down the page. No stored value changes.

= 1.36.0 =
Day trips stop being announced as a run of days. A one-day itinerary reads as a timeline of times and stops on every layout, including Luxury. Nothing is converted or deleted, and an Itinerary format setting covers the tours the rule gets wrong.

= 1.35.2 =
A stability release. Nothing about how tours are stored has changed, and tours written by any earlier version open, save and render exactly as they did. Recommended before updating anything else.


Release notes as they appeared in readme.txt (2.8.8 and earlier)
==================================================================

Moved here in 2.8.9 so readme.txt stays within the 5,000 characters the plugin directory reads; nothing was removed.

= 2.8.8 =
Booking workspace on the edit-booking screen. No table, option, meta key, price, status rule, message or document behaviour is changed.

* Summary from saved data: customer contacts, product, date and time, travellers, the price named after its source (accepted quote, revision, price at request), payment figures only when the ledger exists, and the latest quote's state.
* Next action: one step from simple rules — review a request, follow up a quote, review an accepted quote before confirming, set the date, add the meeting time or place, assign a driver or vehicle for a transfer, mark a past trip completed — plus short notes (crew clash, balance due, email not delivered). It only opens the panel.
* Five sections (Customer & request, Price & quote, Trip preparation, Messages/documents/Guest Pass, History & notes) plus All panels; invalid fields, action results and links open the right section. Without JavaScript all panels stay on the page.
* Unsaved-changes warning, action buttons marked and locked after the first click, and one timeline of status, quote, document, payment, crew, email and WhatsApp events.

= 2.8.7 =
Settings screen reorganised, and stability fixes found by a full pass over the admin screens, the front end in every tour layout, both experiences and right-to-left. No setting, option, table, price or booking behaviour is changed.

* Settings screen reorganised: one section navigation instead of two — a column beside the settings on a wide screen, grouped under Core, Booking, Content & pages, Public & localization, Setup & integration and Advanced, with "Find a setting" at its top; on a tablet or phone the same list opens from the section button in the toolbar. The sections follow that order on the page, and the content uses the width the navigation used to leave empty. One Save button, in a slim toolbar that stays in reach without covering the section headings it scrolls to. Nothing about the settings themselves changed: the same fields, names, values, defaults and save.
* Fix: Settings printed a PHP warning ("Undefined array key 0") in Section pages when a hierarchical section had terms without tours. The example link now uses the first term returned, whatever its key.
* Fix: System status was laid out as one row, because its wrapper shared the class of the status badge; the diagnostic report was squeezed beside the cards and the page scrolled sideways. The report now sits below the cards at full width.
* Phones: the Site pages list and the Section pages table in Settings scroll inside their own region instead of widening the screen, and the Site pages actions wrap.
* Code: the WPML hooks used for translation are marked as WPML's own for Plugin Check, a file-scope loop variable carries the plugin prefix, and the rate-table cache key is built with wp_json_encode() instead of serialize(). The booking screen's stylesheet and script now ship with minified copies like every other asset.
* readme: the 2.8.3 upgrade notice is shortened to fit the 300-character limit.

= 2.8.6 =
Visual polish of the booking screens, the Guest Pass and the PDF documents. No change to how bookings, prices, seats, notifications, documents or the Guest Pass work; no data, option or table is changed.

* Add booking: the agreed total and its currency sit together in one highlighted panel, the amount larger and the currency a narrow code; status and "Send the usual emails" are each a tile, the ticked one marked; more space between rows and their descriptions; the button fills the width on a phone. The error list is indented on the correct side in right-to-left admin.
* Booking screen on a phone: the page no longer scrolls sideways. The Guest Pass map-pin pair and the Trip content image and itinerary fieldsets now shrink to the screen.
* Money fields: "Total to offer" and "Deposit asked for" (Quote) and "Amount" (Payments) are wide enough to show the full figure, with the currency beside them; the balance in Payments is larger; the invoice card shows "Not issued yet" / "Issued as …" as a small label.
* Right to left: no letter-spacing on Arabic small-capital headings in the booking screens, or on the Guest Pass reference label, status pill and payment terms.
* Trip programme PDF: the "Itinerary" heading is kept on the same page as the first day instead of being left alone at the foot of a page.
* "On this page" bar (Bookings dashboard, Operations, Settings): it now sticks directly under the WordPress toolbar at the toolbar's real height (32px, 46px on narrow screens) instead of a fixed 46px that left a strip of content showing above it, and it stays one row that scrolls sideways instead of wrapping into a two- or three-row bar (up to 99px) over the content. Headings it links to land below it. On phones it is not sticky.
* Edit booking, shorter: for anyone who has not arranged the screen themselves, the panels follow the work — request, trip, reception, crew, price, payments, quote, documents, trip content, messages, notes — and the less used ones (Guest Pass details, crew, quote, trip content, messages, WhatsApp, referral, voucher, history) start closed. A closed panel shows a one-line summary of what it holds in its heading, marked when something was typed in it before closing. Fields in a closed panel are still saved; the jump links open the panel they point to. Opening or closing any panel keeps that person's own choice, as WordPress always does. The header adds the received amount and the balance beside the agreed total; on a phone its document buttons sit two to a row.
* Booking screen: a screen-reader label inside the documents table no longer widens the page on a phone; the internal notes panel is tinted and edged so it reads apart from what the guest sees.

= 2.8.5 =
Booking documents: a price set for one booking (revisions with a reason, never over the original), the booking's own trip content, and real PDF quotes, invoices and trip programmes — numbered at issue, frozen, shareable on the Guest Pass. Manual bookings for tours, activities, transfers and custom trips. Schema 6 adds two tables; nothing existing is migrated or repriced.

* Price & services (booking screen): a line editor — service, quantity, unit price, line total — with a discount (amount or percentage), an optional tax line and the currency. Totals are worked out on the server in integer minor units; the browser's running total is a preview. A price of zero and a price on request are kept apart. Each save is a revision stored with the booking (`_wptm_price_revision`, one meta row each, append-only) carrying who, when, the reason (required), the previous figure, its source and the difference; the snapshot taken when the request arrived is never touched, and the product, its rates and its group prices are never written. Payments already recorded fix the currency: a revision in another currency is refused. A revision records no payment, moves no status and sends nothing.
* One money authority: `Payments\expected()` now reads the latest agreement — a revision, or a quote the customer accepted after it — before the snapshot, so the list, the header, the payments panel, the Guest Pass, quotes and invoices print the same figure. A total below what was paid shows "paid in excess" instead of hiding it; nothing is refunded automatically. A quote drafted after a revision starts from the revised lines (`wptm_quote_pricing_context`, priority 5).
* Imported prices never double: an engine line is read as quantity × unit only where that reproduces its own total. A group price ("Private tour, 4 travellers, 660") imports as 1 × 660. The basis (per person, per group, per vehicle) is shown.
* Exact parsing: `Payments\to_minor()` converts the decimal on its digits (`Payments\decimal_to_minor()`), not through a float. What it accepts is unchanged.
* Trip content for this booking: an explicit "Make an editable copy" stores title, description, chosen images, itinerary (keeping days, timeline or route), included / not included, meeting point, time and instructions on the booking (`_wptm_trip_copy`). Opening a booking never copies anything; editing the copy never writes to the product; editing the product never reaches the copy. The Guest Pass prints the copy's title and itinerary when there is one.
* Documents: quotes (from Quotes versions), invoices and trip programmes, previewed as DRAFT without a number and issued with one. New table `wptm_documents` (schema 6): the issued content is frozen as JSON — company, customer, lines, totals and the words in the booking's language — with its SHA-256; an altered row is never served. Numbers are allocated at issue only, from the rows (MAX + 1 per type and series), under a UNIQUE (type, series, number, revision) key that refuses a duplicate under concurrent requests and retries. A correction is a revision with the same number; the previous one is kept, marked superseded. Issuing confirms nothing and records no payment. The PDF is rendered once before a number is taken: a layout that cannot be produced issues nothing and uses no number.
* PDF: real PDF files from the bundled TCPDF 6.11.4 (LGPL-3.0-or-later, `vendor/tcpdf`, trimmed to the engine and DejaVu Sans; loaded only when a PDF is produced; no Composer, no external service). Arabic is shaped and laid out right to left; references, amounts, telephone numbers and Latin-only lines keep their direction. Images come only from the media library, checked to be inside the uploads folder; WebP and other formats are converted to a temporary JPEG through WordPress's image editor. Page numbers, a running header, the company footer, and headings kept with what follows them. Every text is escaped before layout.
* Access: admin downloads need the booking capability and a nonce. A customer can download a document only when it is issued, current, intact and shared, with their Guest Pass key (revoked or regenerated keys stop working at once) or, for a quote, that quote's own key. The Guest Pass lists shared documents; the quote page offers its shared PDF. No file is written to the uploads folder and no address works without a key.
* Company details (Settings): trading and legal name, logo, address, contacts, free identifier rows (ICE, RC, IF, VAT…), footer and short terms per language, numbering (prefixes, year, digits, starting numbers), defaults for payment instructions and programme prices, and an optional tax line (off by default; rate in basis points). Empty fields fall back to the values the site already has (Guest Pass name and logo, support telephone and e-mail, WhatsApp, site address). Payment details stay on the Payments screen and are printed only when chosen.
* Add booking: a tour or activity, a transfer (vehicle and trip type, priced per vehicle) or a custom trip with no product (holds no seats). The product's price for the party is stored as the booking's snapshot, as a request from the site stores it; an optional agreed total is recorded as a revision. Source "Other" needs a description. Values are kept when a field is refused.
* Fix: a booking entered by hand never held its seats. Its ledger key was 43 characters for a 40-character column, so `$wpdb` refused the row; it is now 39. Capacity is respected again for manual bookings with a date.
* Fix: the Guest Pass showed only the old typed payment fields to guests: the ledger summary was registered on wp-admin requests only. It now loads with the pass, and "paid" is what is held (received less refunded).
* Bookings list: Trip, Source and "Total · paid · due" columns (Guests and Submitted are under Screen Options), filters by source and travel-date range, a custom trip's own title, and an empty state that says what to do.
* Booking screen: the header carries the Quote, Invoice and Programme PDF buttons (the current issued file, or the Documents panel), the jump links follow the order a booking is handled, and the default panel order matches it until the user rearranges. The payments form is one readable column.
* Front-end request form: a summary line at the top when a field below was refused.
* Debug notice: the Luxury section registry is attached on `init` instead of at load, so WordPress 6.7+ no longer reports a translation read too early.
* Erasure keeps issued invoices (accounting records) and removes quote and programme documents of the erased booking.
* Issued PDFs are kept. After the number is allocated the document is rendered with it and the file stored in `wptm_document_files` (database only, base64 with a SHA-256 checked on every read; nothing in uploads or the media library). The row stays "pending" — not current, not shared, not shown to the customer — until the file is read back intact; only then is it issued and the revision it replaces marked superseded. Every download, admin or customer (same capability, nonce and key checks), is that file, so later changes to images, logo, company details or the product do not reach it. A render or storage failure leaves the numbered row "Not issued — PDF not stored" with "Finish issuing", which reuses the row and the number; issuing again meanwhile is refused. Deleting a booking or erasing it removes the files with the documents. A document issued by an earlier 2.8.5 build has no stored file: it is drawn again from its frozen text with the pictures as they are now (said so in the list) and its row is not changed.
* Fix (Add booking): an agreed total that could not be saved no longer lets the booking be confirmed or announced. The booking is kept, stays pending, queues nothing, and quotes and invoices are refused until the price is saved; the booking screen shows the amount entered and "Save the agreed price and finish". The retry runs on the same booking under a lock and reads the table first, so a revision whose write reported a failure but held is not written twice. 0 is a price. A booking finished later by recovery follows the same rule. The confirmation is checked in the table, not taken from `set_status()`'s answer: if it did not hold, nothing is queued, what is left stays on the booking and the retry resumes there; a booking already confirmed is not transitioned again, and one a person moved elsewhere (cancelled, declined…) keeps that decision. In recovery, a booking that is saved but not finished is not marked done: its entry is kept and the next run resumes after the effects, without binning it or releasing its seats.
* Fix (Add booking): "Notify" now covers the status change too. Unchecked, confirming queues nothing (`Mail\held_back()`: one booking, this request only, released in `finally`; every other status listener still runs). Checked, the messages are queued once, after the booking and its price are saved. Later changes made on the booking behave as before.
* Fix (Add booking): with seat limits enforced, any failure to hold the seats refuses the booking, not only "full", as the website form does. Nothing is written and what was typed is kept. Without enforcement, transfers and custom trips are unchanged.
* Fix (recovery): `settle()` compares the saved values with those submitted; a missing row is incomplete as before, a different value is unknown (kept for the administrator notice, not binned). A record that stopped mid-save with nothing to compare against is unknown instead of complete. Seats are checked on the ledger row. The settling lock (`wptm_booking_settling_{id}`) lapses after five minutes and only its owner releases it; the 2.8.3 claim that could stall a booking for ever is ignored. Progress after the save is recorded (`_wptm_settle_stage`), so a stop after "complete" still queues the messages; a stop inside the `wptm_booking_created` listeners does not run them again (logged). Waiting bookings are one option row each (`wptm_booking_unverified_{id}`), so concurrent writers lose none; the 2.8.3 list is moved on first read.
* Fix (prices): `Pricing\snapshot_state()` no longer accepts an existing snapshot on presence. It must be one readable row with its total and currency agreeing with the expected snapshot; otherwise the new answer `invalid` (unknown, never complete or binned). Missing or wrong report copies (`_wptm_price_total_minor`, `_wptm_price_currency`) are written again from the stored snapshot, never from current rates; the snapshot is never replaced.

= 2.8.3 =
Stabilisation: bookings, prices and notifications are confirmed as stored before success is announced; Guest Pass map priority. No schema, URL, token, QR, permission or pricing-formula change; nothing is migrated or repriced.

* Bookings: `Booking\create()` reads the required values back from the table (uncached) and compares them with what was written — reference, customer name, e-mail and phone, product, date and start time, departure, party, status, the price snapshot when one was worked out, and the reservation link — instead of trusting `wp_insert_post()` or `update_post_meta()`'s return. Three outcomes are kept apart. Complete: the booking carries `_wptm_save_state = complete` (itself verified), the ledger is joined and `wptm_booking_created` fires. Confirmed incomplete: the post is moved to the bin and the move is confirmed in the table before `create()` returns `wptm_booking_incomplete`; only then do tour, activity, transfer and manual callers release the seats; if the move cannot be confirmed the outcome is unknown. Unknown (the verification read failed): `create()` returns `wptm_booking_unverified`; nothing is binned, deleted or released, the ledger row and the reservation link stay with the request so a resend waits for this booking instead of creating another, the visitor sees the translated "still being recorded" message with their answers kept (the manual form says to check the Bookings list first), and the booking is recorded in `wptm_booking_unverified`. The queue runner (loop back, cron, hourly sweep, admin fallback) settles it with `Booking\recover_unverified()`: complete → the save state, ledger join, history, `wptm_booking_created` and the notifications, once (an atomic claim row guards them); confirmed incomplete → binned, the move confirmed, then the seats released; still unreadable → kept, and after ten minutes administrators see a count with a "Check now" button. A request that stopped mid-save (state still "pending" after ten minutes) is settled the same way. Age alone never decides an outcome. Luxury's transaction is unchanged: the same checks run inside it, and any failure rolls it back.
* Repeated requests: a repeated copy of a request (`await_booking()`, `resolve_duplicate()`, and the fingerprint shortcut for tours, activities and transfers) is answered with a booking only once `Booking\completion_state()` says it is complete. A ledger row naming a booking, or a booking naming its reservation, is no longer enough; a failed read is never complete. Until then the copy waits up to the existing limit and gets the "still being recorded" message. Bookings made before 2.8.3 carry no save state and count as complete when they have a reference and a status. Reconcile no longer joins a booking whose save is still pending. Uninstall removes the two new operational options.
* Prices: `Pricing\store_snapshot()` keeps its signature and returns true only when the snapshot (and, for a priced booking, its total and currency) read back intact; `Pricing\snapshot_state()` gives the full answer (stored / exists / failed / unknown). An existing snapshot is never replaced, checked in the table. Tour, activity and transfer requests pass the server-side snapshot to `create()`, which stores it before the booking is announced. A zero total is stored as a price; a price on request stores the snapshot without a total; no quote stores nothing, as before. The calculation is unchanged and uses the same inputs, just before the post rather than after it; listeners of `wptm_booking_created` (the payment state) now see the snapshot. Recovery stores the snapshot worked out at request time if it is missing; nothing is repriced.
* Notifications: `Mail\queue_state()` tells queued / exists / skipped / failed apart (`queue()` still returns the row id). Customer mail switched off, a missing address or a refused duplicate is not a failure. A required request or confirmation message that could not be added to the outbox flags the booking with the existing `_wptm_mail_queued` key; the queue runner adds what is still owed — dedupe keys make repeated or concurrent runs write one row — and removes the flag only when nothing owed is missing. What is owed follows the booking's current status (`Mail\still_owed()`): "request received" only while the request is open (pending, contacted), the confirmation only while confirmed, the agency's new-request message not once cancelled, declined or completed. The same test runs again when a row is composed for sending; a message that no longer fits is cancelled as "No longer applicable", not failed. Retries are spaced by a one-minute gate; a retry that still fails shows administrators a count and "Try again now" (capability and nonce checked). No personal data in notices or the log. A failed notification never cancels or recreates a booking; "sending"/"uncertain" rows are untouched and never resent automatically.
* Guest Pass map: the booking's own map link, then the booking's own coordinates (a complete, in-range pair; 0 is valid), then the tour's meeting map link, then a search for the place and address. Before, the tour's link outranked a pin set for the booking.
* Luxury: the private-enquiry storage check is shown under Settings → "How journeys are sold" when the luxury experience is on, so a non-InnoDB or unreadable-engine database is visible before the first enquiry is refused. Nothing is converted and nothing is saved unsafely.
* Changelog: 2.8.2 said sites with non-InnoDB tables "keep the previous behaviour and log a warning"; in fact the enquiry is refused, the reason logged and an administrator notice shown. Corrected below.

= 2.8.2 =
Luxury enquiry concurrency fix and Guest Pass visual polish. No schema, URL, token, QR or permission change.

* Luxury enquiries: "sent" is answered only when the enquiry is stored. The booking carries its submission key (`_wptm_enquiry_once`, written with the record), so a retry after a crash between saving and finishing resolves to it instead of filing a duplicate. A copy that arrives while the first is still writing waits up to 6 s (`wptm_luxury_once_wait`) and then gets a translated, recoverable message with its answers kept. A failed save releases the claim at once. Claims are taken with INSERT IGNORE, expired claims (120 s) are taken over with a compare-and-swap UPDATE, and releases delete only the releasing request's own token, so a stale worker can neither overwrite nor release a newer claim. The hourly sweep deletes only the exact expired values it read.
* Luxury enquiries, storage: the booking, every meta row (the submission key included), the enquiry fields, the outbox rows and the end of the claim are one InnoDB transaction (`store_enquiry()`). It opens by locking the claim row (SELECT … FOR UPDATE) and checking the owner token, so a worker whose lease was taken over writes nothing and queues no mail, and no takeover can happen while the owner is inside it. A request that dies before COMMIT leaves nothing (the server rolls back on disconnect); a silent reconnect mid-transaction is detected by connection id and its fragments removed; a failed COMMIT is resolved by reading. Notifications are queued inside the transaction and only started after COMMIT (`queue_mail()`, `notify()` unchanged for callers); `wptm_luxury_enquiry_created` fires after COMMIT. Lock waits are bounded to 3 s for this connection. Sites whose posts, postmeta, options or outbox tables are not InnoDB (or whose engines cannot be read) store no enquiry: the visitor gets the translated retry message, the reason is logged and an administrator notice explains it (see Safeguards).
* Luxury enquiries, safeguards: no enquiry is stored without a transaction. If posts, postmeta, options or the outbox table is not InnoDB ('unsupported') or the engines cannot be read ('check_failed'), the visitor gets the translated retry message with their answers kept, only this request's claim is released, the reason is logged and an administrator notice explains it (clearing itself once the check passes). Before COMMIT every required value (customer, tour, party, notes, status, enquiry fields, reference, request key) is read back and compared with the validated submission, and the owed outbox rows are checked against the booking; `queue_mail()` now reports queued / exists / skipped / failed, so switched-off customer mail or a missing address never blocks an enquiry while a failed insert rolls it back. A failed COMMIT, or one answered on a replacement connection, is never taken at its word: the transaction is ended with ROLLBACK and the durable state is read afresh (`committed_state()`): complete is success, absent is a retryable failure, anything else is "confirmation pending" with nothing recreated or deleted. The lock-wait setting is restored at shutdown.
* Guest Pass: header reads agency, pass title with reference and status, trip, then the facts; the facts grid has no empty cells; a reset rule that outranked every spacing rule now carries no weight (`:where()`), restoring the intended spacing; long names and addresses wrap inside their cards; pickup details and guest actions are grouped under hairlines; plain buttons get the primary style instead of the browser's; the QR sits on plain white with quiet space; the phone bar colours WhatsApp by destination; print shows black text (the header's white text vanished on paper), hides controls and WhatsApp links, keeps the QR block and key groups whole.

= 2.8.1 =
Stabilisation pass before 3.0. No schema change, no new dependency, no change to pricing formulas, URLs, hooks or stored data.

* Bookings: the idempotency key is now the form's request identifier plus the normalised request (who, and every fact that distinguishes the booking) and the nonce. Two visitors served the same cached page no longer share a booking or see each other's confirmation; a retried or concurrent copy of one submission still resolves to one booking. A copy that arrives while the first is still being written waits for it instead of creating a second booking on the same reservation. The request identifier is also regenerated per page view in the browser.
* Bookings: duplicate detection includes start time, departure, extras, pick-up and, for transfers, vehicle, trip type, pick-up time, drop-off and return. Phone numbers are no longer cut to their last nine digits.
* Bookings: an expired form returns to the tour, activity, transfer or journey with a translated message and the answers put back (server-side, never in the address, page uncacheable). Nothing is booked from it; the nonce check is unchanged.
* Luxury enquiries: the same cache-safe dedupe, claimed atomically; the answer page is kept out of page caches.
* Outbox: a send interrupted mid-way is marked "Delivery unconfirmed" after 15 minutes and never retried automatically. The booking's Messages panel offers "Mark as delivered" or "Send again anyway"; both are capability- and nonce-checked and version-guarded, so a double click acts once. Composition errors no longer leave a row stuck; the first retry waits the intended two minutes.
* Pricing: an extra with a non-Latin name (e.g. Arabic) could be ticked but was silently dropped from the price and the booking; it is now kept.
* Editor: saving an untouched tour no longer changes it. A pre-1.6 per-group tour kept "Per group", a tour with departures kept "Scheduled", a tour without a currency kept the site currency, and a price written "45,5" or "1 250,50" is no longer emptied by the browser.
* Layouts: "Route at a glance" uses the design's heading face; its end label no longer widens the page on phones; Atlas's ruled route row spans the section. The closing booking form (Qamar) is held to its intended width. The phone booking bar no longer covers the end of the page.

= 2.8.0 =
A curated set of tour layouts. Seven layouts with distinct purposes replace the long list of overlapping ones; the older choices stay supported. Nothing is migrated and no tour needs re-saving.

* Layout families: Salma (booking focused), Sara (marketplace), Atlas (multi-day), Sahara (photo-first), Medina (short experiences), Qamar (premium storytelling), Classic (simple).
* Variants: Atlas styles Clean and Zellige; Sahara galleries Cinematic, Immersive and Mosaic. A variant changes presentation only.
* Legacy layouts (Immersive, Editorial, Mosaic, Adventure, ZelligeDesign) keep their templates and stylesheets and render as before; they are no longer offered for new choices.
* Layout picker in Settings, the tour editor and the setup wizard: each layout with its purpose and when to use it.
* Structural changes to Sara, Atlas, Sahara, Medina and Qamar (see changelog.txt). Prices, Group Rates and booking are the shared components, unchanged.
* Final polish: Sara's booking column no longer hides the help card's WhatsApp button while scrolling; a proper WhatsApp mark on every WhatsApp button; a cleaner Atlas route summary; Medina places its questions and reviews without empty half pages; a finishing pass on Qamar and on the "Need help?" card.

= 2.7.0 =
Tour display stabilization and consistency. Every layout keeps its look; the layouts now share the same components and data. Nothing is migrated and no tour needs re-saving.

* One tour card data layer and renderer for standard, archive and related cards; prices always from the tour's own pricing (Group Rates included).
* Trust section on SalmaDesign and QamarDesign; section navigation follows each layout's real order.
* One booking confirmation per page; no booking dialog or empty link when booking is off; no duplicate ids when the form is drawn twice.
* The two-step booking form is a shared component: its styles moved to booking-steps.css, and salma.css (same handle) holds SalmaDesign's layout only.
* Highlights: one shared item style on every layout — fixed 34px icon aligned to the first line, two columns where the section is wide enough, one on phones, lighter type.
* Empty states: no empty bands, cards, asides or headings when optional content (facts, highlights, related tours, booking, trust, meeting point, help) is missing.
* Phone booking bar no longer collapses with long translated labels; itinerary titles no longer repeat the day number; bind-once scripts; documented z-index scale.

= 2.5.1 =
* Homepage FAQ section: equal card heights and no layout shifting when answers open.
* Safe manual upgrade option for legacy tours to use the new tour editor, keeping existing data and legacy metadata.

= 2.5.0 =
Admin editor workflow and polish: stable layout, status-aware Save/Publish bar, clearer validation, and a finishing pass on tour and activity pages. Pasted itineraries and FAQs no longer collapse.

The full release history is in changelog.txt, which ships with the plugin.
