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