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.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.
