# Destina Holidays Hotels and Activities Redesign: Session Context

Compiled 16 September 2026 from the chat transcript of the redesign session (13 to 16 September 2026), the reports of the background workers spawned during it, and the current state of the code in both repositories. Updated the same evening to cover the work committed after the first draft (commits `6a9eaf8` through `9ee4bef` in the Angular app, `23ee165` and `b9f1bc9` in `hotel-services`). Where a statement could not be confirmed against code or a transcript, it is marked as such.

The flights results redesign that followed (17 September 2026) is documented separately in `flights-redesign-context.md`; the design system, repositories and verification approach described here apply to it as well.

## 1. Purpose and scope

The session started as a request to read `hotel-services/activities.php` and the `search-activities` Angular folder, and grew into a full redesign and hardening pass over the hotels and activities product on `destinaholidays.com`:

- Activities results page rebuilt in the style of Viator (card grid, category chips, filter drawer, infinite scroll), followed by the activity info, checkout and confirmation pages and a new confirmation email template.
- Hotels results (desktop and mobile), hotel info, checkout and confirmation rebuilt in the style of Trip.com, sharing the same design language as the activities pages.
- A theme pass across all of those pages inspired by airportr.com and the Dribbble "Airportr Modern Travel Website Redesign" shot, consolidated into `src/brand.scss`.
- New landing pages for `/search-hotels` (Trip.com hotels home) and `/search-activities` (Viator home) when no search parameters are present.
- A long tail of defects and features: autosuggest by property or activity name, wishlist/viewed/search history tracking in Mongo, Google One Tap login, Hotelbeds guest ratings, Affirm monthly pricing, payment capture through Moneris and Connex, progressive hotel results loading, mobile UX fixes, and many CSS and change-detection bugs.

Two side tracks also happened in the backend: a static-content sync for Hotelbeds Activities into Mongo (`api-stage/include/StaticActivities.php` plus a scheduled workflow), and a change to hotel booking gating (realtime booking only for refundable rates).

## 2. Architecture overview

### 2.1 Repositories

| Repo | Path | Role |
|---|---|---|
| Angular SSR app | `/Applications/MAMP/htdocs/homepage-ssr-app` | Frontend for hotels and activities. Dev server on `http://localhost:4200` via `ng serve`. Commits in this session: `f9c05db` through `9ee4bef`. |
| Hotel services API | `/Applications/MAMP/htdocs/github-global-api/hotel-services` (its own git repo) | `process.php` router plus `activities.php`, `activities-static.php`, `activities-user.php`, `hotels-user.php`, email templates in `include/`. Saving locally auto-uploads to `stage.flightomart.com`, which the local frontend calls. Commits `78761f2` through `b9f1bc9`. |
| api-stage | `/Applications/MAMP/htdocs/github-global-api/api-stage` (own git repo) | Flights backend; reused for `MonerisInterface`, `AmadeusMultiPNR.php` Affirm reference, and the static sync scripts/workflows. `include/StaticActivities.php` and `.github/workflows/sync-static-activities.yml` exist on disk (dated 16 Sep) but do not appear in `git log`; their commit status is unclear. |
| Header/footer library | `/Applications/MAMP/htdocs/common-components-github` | `common-header` / `common-footer` custom elements. Not edited; relevant because their styles must not inherit page backgrounds. |
| Flights checkout | `/Applications/MAMP/htdocs/ng-checkout-github` | Reference for Affirm wiring (`data.service.ts`). |

The `github-global-api` workspace root itself has no commits on `main`; each sub-folder is a separate repository.

### 2.2 Angular components and services

Hotels (`src/app/hotels/`):

- `results/results.component` decides between `HotelsHomeComponent` (no `location` param) and the results page; results pick desktop or mobile via `DataService.isMobileView()` (`innerWidth <= 768 || isMobileDevice()`).
- `hotel-results/hotel-results.component.ts` is the base with search, polling and filters; `hotel-results/hotel-results-ui.component.ts` (new) is an abstract `@Component({ template: '' })` subclass holding all shared UI helpers; `hotel-results-desktop` and `hotel-results-mobile` extend it. A `@Directive()` base could not extend a component ("Directives cannot inherit Components" killed the lazy module at runtime), hence the empty-template component.
- `hotels-home/` (new), `hotel-info/`, `hotel-checkout/`, `hotel-confirmation/`, `property-search/` (in-results keyword search), `affirm-as-low-as.directive.ts` (new).
- `hotel-autosuggest/` is shared by the homepage, hotels home, desktop and mobile results.
- `mobile-calendar/` is the shared full-screen date sheet (also used by flights); it gained a `type="activity"` range mode.

Activities (`src/app/search-activities/`):

- `activities-entry/` (new) renders `ActivitiesHomeComponent` when `destination` is absent, otherwise `ActivityResultsComponent`.
- `activity-results/`, `activity-info/`, `activity-checkout/`, `activity-confirmation/`, `activities-home/` (new), `state.ts` (`ActivitySelectionState`, `selectionParams()`), `currency-utils.ts`.
- `price-expiry/` (new): `PriceExpiryService` (root; holds `sessionId`/`expiresAt`, skew-corrected with `serverTime`, 1 s browser-only ticker, `state$`, `expireNow()`) and `PriceExpiryModalComponent` (`<price-expiry-modal>`), used by activity info and activity checkout.

Shared modules (after the lazy-loading restructure, commit `9ee4bef`): `IncludesModule` is slimmed to `CheckoutProcessingComponent` (`src/app/includes/checkout-processing/`), `CommonModule`, datepicker, dropdown and `CommonComponentsModule`; the widgets moved to feature-neutral `MobileCalendarModule`, `HotelAutosuggestModule`, `FlightAutosuggestModule` and `TourAutosuggestModule`, each imported only where its selector is used. `FlightsModule` owns the flight loaders, `TimePipe`/`LayoverPipe`, popover and tooltip. `GoogleOneTapService` lazy-imports `ActivityUserService` through the `Injector` only when a Google credential arrives, so no activities code sits in `main.js`.

Services (`src/app/services/`): `activities.service.ts`, `activities-static.service.ts` (destinations/segments cached in `localStorage` 24 h, `suggestRemote(q, lang, limit, destination?)`), `activity-user.service.ts` and `hotel-user.service.ts` (visitor cookie `act_vid`, wishlist cookie/localStorage, viewed, search history), `affirm.service.ts`, `google-one-tap.service.ts`, `data.service.ts` (`suggest()`, `popularDestinations()`, `isMobileView()`, `getCardType()`).

Shared styles: `src/brand.scss`, `src/checkout-shared.scss` (imported by both checkouts), `src/styles.scss` (global Inter / Plus Jakarta Sans import replacing Circular Pro).

### 2.3 Backend endpoints (`hotel-services/process.php`)

GET routes:

- `suggestDestinations=true&q=&limit=&hotels=1`: destinations by name/code/country (no `q` returns top N by property count). With `hotels=1` and a query of 3+ characters it also returns `hotels` via `suggestHotels()`, which tokenises the query, detects a place (`detectPlace`, longest phrase first, tie-break on property count, country prefix only for 4+ characters), scopes the remaining terms to that place, then falls back to name-anywhere and finally to a broad name/address/city/zone match (`queryHotels($broad)`). Cache key `DEST_SUGGEST_V4_...`, 6 h.
- `hotelsHome=true&currency=XXX`: country from `visitorCountry()` (IP lookup), local top-12 destinations, curated top-50 worldwide (`topWorldDestinations`, visitor country excluded), property rails with live prices/ratings, FAQ and recommendation data. Dates are +30/+31 days. Cache `HOTELS_HOME_V5_{country}_{currency}` for 6 h; destination images `HOME_DEST_IMG_{code}` for 7 days. Cold load measured at ~7 to 19 s depending on the run; cached loads are instant.
- `activitiesHome=true&country=XX&currency=YYY`: same shape for activities (local and world destinations, tabbed `localExperiences` / `worldExperiences` product groups). Cache `ACTIVITIES_HOME_V1_{country}_{currency}` 6 h, per-destination counts `ACT_HOME_COUNTS_V1` 7 d, images `ACT_HOME_DEST_IMG_{code}` 7 d. Cold 5.1 to 8.6 s, cached ~0.8 s.
- `activitiesStatic=<collection>&language=en` and `activitiesAutosuggest=true&q=&language=&limit=&destination=` (class `ActivitiesStatic`, memcached 6 h / 1 h, empty payloads never cached, `refresh=1` bypasses).
- `activitySessionStatus=true&sessionId=A_...`: returns `{valid, expiresAt, serverTime, selection}` for a price session (see 2.6); `selection` is returned whenever the document still exists, valid or expired.
- Activities passthroughs: `activitiesStatus`, `activitiesLanguages`, `activitiesCurrencies`, `activitiesSegments`, `activitiesCountries`, `activitiesDestinations`, `activitiesPortfolio`, `activityBookingDetail`, `activityBookingDetailOptions`, `activityBookingList`, `cancelActivity`, `activityVoucher`, `localActivityBooking`.
- `propertyInfo`, `getAllDestinations`, `getPopularHotels*`, `getTopDestinations*`, `getPropertyCount`, `placeDetails` and other pre-existing routes.

POST routes (JSON body key selects the action; posted as `text/plain` via `JSON.stringify` to avoid a CORS preflight that the server answers with a 302):

- `propertyAvailabilityByLocation` with `pollProperties: true` runs `findPropertyAvailabilityByLocation` (one Hotelbeds availability call per batch, merged into a memcached session keyed by `sessionId`, TTL 3600 s); with `pollProperties: false` runs `filterResults` against the session. Both return `complete`.
- `findPropertyAvailability` (hotel info rooms), `propertyMapView` (full session set for the map), `searchPropertyAutoSuggest` (in-results search over the session set; now token based and punctuation insensitive), `searchHotelAutoSuggest`, `createCheckoutSession`, `retrieveCheckoutSession`, `adjustPricing`, `processBooking`, `getRelatedProperties`.
- Activities: `searchActivities`, `activityDetail` (now also returns `sessionId`, `expiresAt`, `serverTime` and creates the price session), `activityPickups`, `preconfirmActivity`, `confirmActivity`, `reconfirmActivity`, `processActivityBooking` (rejects a missing or expired `sessionId` with `{valid: false, error: {code: 'SESSION_EXPIRED'}}` before any payment or supplier call).
- User tracking: `activityWishlist`, `activitySearchHistory` (supports `action: 'list'`), `activityIdentify`, `activityViewed`, `hotelWishlist`, `hotelSearchHistory`, `hotelViewed`.

### 2.4 Mongo collections

| Database | Collection | Use |
|---|---|---|
| `hotels` | `apitude_properties` | Hotelbeds hotel content; `name.content`, `destinationCode`, `zoneCode`, images, ranking |
| `hotels` | `destinations`, `destinations_mapped` | Destination names, property counts, zones |
| `hotels` | `hotel_reviews` (new) | Cached Hotelbeds/TripAdvisor rating per hotel code, unique index on `code`, refreshed after 7 days, `v: 2` |
| `hotels` | `wishlist`, `viewed`, `search_history`, `visitors` | Hotel user tracking (`HotelsUser extends ActivitiesUser`) |
| `hotels` | `facilities`, `facility_groups`, `categories`, `boards`, `accommodations`, `rooms`, `sightseeing`, `session_cache` | Pre-existing static data |
| `activities` | `languages`, `currencies`, `segments`, `countries`, `destinations`, `portfolio`, `factsheets`, `sync_meta` | Static sync from Hotelbeds Activities (`StaticActivities.php`, tmp collection then swap). `destinations` has 1,087 documents; `factsheets` was still empty on stage when the activities home was built |
| `activities` | `wishlist`, `viewed`, `search_history`, `visitors` | Activity user tracking; wishlist/search history expire after 15 days, viewed after 90 |
| `cache` | `hotel_checkout_cache` | Hotel checkout sessions `H_<24 chars>` (`utc_date`, `local_date`, `key`, base64-serialised `value`); no `expires_at`, no TTL index (pre-existing) |
| `cache` | `activity_sessions` (new) | Activity price sessions `A_...` with TTL index `expires_at_ttl` (`expireAfterSeconds: 0`) and unique `session_id`; see 2.6 |

Mongo lives on a private IP (`10.116.0.5`); local PHP has no mongodb extension, so queries were run through PHP scripts on stage.

### 2.5 Caching and session storage

- Memcached (via `$database->cache/retrieve`) holds the hotel results session (`sessionId`, 3600 s) and the caches above. The session payload was slimmed (`sessionProperty()`: unavailable hotels dropped, facilities as `"group-code"` keys, 6 image paths, one trimmed rate) because it exceeded the 1 MB item limit after 2 to 3 polls and silently reset `totalPropertyCount`.
- `PROPERTY_COUNT_{code}` (24 h) replaced a first-poll loop over every property document that cost 1.3 to 1.5 s.
- `currencyCache` in `include/session.php` gained a per-request static memo; non-USD batches halved in time.
- Client side: `localStorage` for `act_static_*` (24 h), `hotel_dest_popular` (24 h), `hotel_wish`, `user_details`, `currency`; cookies `act_vid` (365 d) and `act_wish`.

### 2.6 Sessions and price expiry

Hotels (pre-existing, unchanged):

- Search sessions are memcached only (`HOTEL_SEARCH_CACHE_<rand>`, 3600 s); the `sessionId` query param re-reads that cache.
- The checkout session is created by `createCheckoutSession` in Mongo `cache.hotel_checkout_cache` with no TTL; `retrieveCheckoutSession` re-checks rates with Hotelbeds on every load. The hotel checkout timer is purely client side (`initTime = moment().unix()`, `maxSession = 2700`, 45 minutes) with a `sessionTimeout` modal whose "Refresh prices" reloads the page. Hotels survive a reload because the session id is in the `/checkout/:session` route.

Activities (built 16 Sep evening, commits `23ee165`/`b9f1bc9` backend, `835286b`/`0cbcaa6` frontend):

- Every `activityDetail` call (info page and every checkout re-price) writes a document to `cache.activity_sessions`: `session_id` (`A_...`), `destination`, `destination_name`, `from`, `to`, `paxes`, `activity_code/name`, `modality_code/name`, `session_code`, `rate_key`, `price`, `currency`, `original_price/currency`, `language`, `email`, `visitor_id` (body or `act_vid` cookie), `ip`, `created_at`, `expires_at` (now + 20 min). Indexes are created lazily the same way as `hotel_reviews`.
- The response carries `sessionId`, `expiresAt`, `serverTime`; `ActivitiesService.detail()` captures them into `PriceExpiryService`, and `processBooking()` attaches `sessionId` and flips the state to expired on `SESSION_EXPIRED`.
- `PriceExpiryModalComponent` opens when the state is expired, defers while `busy`, re-checks `activitySessionStatus` on tab refocus, offers "Refresh prices" (`[canRefresh]`, hidden when there is nothing to re-price) and "Back to results", locks scroll, single instance, styled with `brand-cta`. Info page wires `(refresh)="fetchDetail()"`; checkout wires a full re-price.
- The checkout "Price held for" countdown and the Book button follow the server `expiresAt` through `state$`; the old checkout-only 20 minute client timer and modal were replaced.
- Restore on reload: the checkout keeps `session=<sessionId>` in the URL (`merge` + `replaceUrl`, synced whenever the service's id changes). When `state.selected` is missing on init, `restoreSession()` calls `activitySessionStatus`, rebuilds a `SelectedRate` from `selection`, re-prices with one `detail()` call if valid, or shows the expiry modal if expired, invalid or absent (never a redirect). The header's `location.reload()` on currency change therefore re-prices in the new currency instead of dropping the user back to `/search-activities`. `selectionParams()` renamed its start-time key from `session` to `start` to free the `session` param.

## 3. Design system

### 3.1 Tokens (`src/brand.scss`, mixin `brand-tokens`)

| Token | Value | Notes |
|---|---|---|
| `--font`, `--font-head` | `'Outfit', 'Objective', system-ui, ...` | airportr uses the commercial "Objective"; Outfit is the free substitute. Add `@font-face` and the fallback chain already lists Objective |
| `--navy` | `#091a55` | Bands, headings (from the Dribbble palette; was `#00005a` in the first airportr pass) |
| `--indigo`, `--blue`, `--violet` | `#1c1f7a`, `#4151b3`, `#7b5cf5` | |
| `--band` | gradient `#091a55 -> #1c1f7a -> #3c3fa6` at 120deg | Search bands, mobile bands |
| `--cta` | `#7b5cf5` (violet) | Default primary button colour, matching the "Special discount" pill |
| `--cta-hover` | gradient `#f26a3a -> #e23a4a` | Orange to red, hover only, cross-faded via `brand-hover-fade`. It was briefly used for mobile sidebar close headers on 16 Sep; those headers are now blue (`--or`) |
| `--loader` | `#ed5e3c` | Orange progress bars under search bands |
| `--ac`, `--ac-dark`, `--ac-soft`, `--ac-line` | `#4151b3`, `#091a55`, `#e9e7f7`, `#c9c4e6` | Accent, links, chips |
| `--or`, `--or-dark`, `--orange` | `#4151b3`, `#2f3d95`, `#4151b3` | Despite the names these are blue in this theme; do not use them for orange (use `--cta-hover` or `--loader`). `--or` is the standard colour for mobile sidebar `.close-nav` headers |
| `--purple` | `var(--ac)` | Compatibility alias consumed by `hotel-autosuggest` and `mobile-calendar` |
| `--ink`, `--mute`/`--muted`, `--line` | `#0b0e33`, `#6c74a3`, `#e3e1f0` | Text and borders |
| `--bg`, `--bg-2` | `#edebf5`, `#e4e1f2` | Lavender page background (applied on page wrappers, not `:host`) |
| `--ok`, `--bad`, `--star` | `#0f8a5f`, `#c62839`, `#f5a623` | |
| `--r-card`, `--r-pill` | `20px`, `999px` | |
| `--shadow`, `--shadow-lg` | indigo-tinted | |

`brand-tokens` also applies `font-family` and `color` only to `> :not(common-header):not(common-footer)` so the shared header and footer never inherit page typography or colour.

### 3.2 Mixins

- `brand-hover-fade`: `::before` layer carrying `--cta-hover` at opacity 0, fading to 1 in 0.3 s; `isolation: isolate` keeps it under the label. Needed because CSS cannot transition from a solid colour to a gradient.
- `brand-cta`: violet pill button with the hover fade and warm shadow; disabled state removes the overlay.
- `brand-search-btn`: white band button (navy text) that fades to the gradient and white text on hover.
- `brand-slide-track($fit)`: clipped viewport with an absolutely positioned flex `.slide-track`; templates bind `transform: translateX(-index * 100%)` for left/right sliding galleries.
- `brand-card`: 20 px radius, white, faint border, `--shadow`.

### 3.3 Rules established during the session

- Bold text is `font-weight: 500`; `600` only on large headings (`h1`, section `h2`, `.page-title`, `.hero h1`, `.head h1`, `.sec h2`, `.rail-head h2`, `.tm-title strong`). No 700/800 anywhere in hotel or activity SCSS or the two email templates.
- Final code must contain no comments (repo requirement).
- Header and footer must not inherit page backgrounds: page backgrounds live on wrappers such as `.hotels-page`, `.m-page`, `.activities-page`, `.info-page`, not on `:host`.
- No horizontal scroll on mobile: every route host has `overflow-x: clip` (not `hidden`, which would break sticky bars).
- Primary buttons are violet by default and orange gradient on hover only; search-band buttons are white by default.
- Progress bars under bands are orange (`--loader`); the dropdown loader on white stays violet.
- Mobile sidebars (hotel autosuggest sidebar, activity destination sheet, mobile calendar) have a blue `var(--or)` (`#4151b3`) close header with a translucent white close button and `z-index: 1300`. This is the final decision (commit `804301f`, applied on hotels home, activities home, hotel results mobile, hotel info, activity results and activity info); the orange gradient headers introduced earlier the same afternoon were superseded. The `mobile-calendar` override must be written as `mobile-calendar .side-bar .close-nav` to beat the calendar's own navy rule.
- Guided search chain on all four search bars (hotels home, hotel results, activities home, activity results), desktop and mobile: destination pick opens the date picker (range picker on desktop, `mobile-calendar` on mobile), completing the dates opens the guests/travellers dialog with focus on its first control, and Done/Apply runs the search. Dismissing any step (Escape, outside click, X, backdrop) aborts silently; a manual Done on a non-guided pax dialog does not search; a hotel-property pick (`fromHotel`) still searches immediately. This supersedes the earlier "hotel results band re-searches on destination change" behaviour (commit `64b3ca4`).
- Checkout form fields with a right-side adornment (`.sel` selects, `.cc-wrap` card number with brand badge) show no inline tick or "!" glyph; their state is conveyed by the green/red border plus the error text. Plain inputs keep the tick.
- Global fonts: Inter body, Plus Jakarta Sans headings via `styles.scss`; the themed pages override with Outfit through `--font`.

## 4. Feature summary and key decisions

### 4.1 Hotel results (desktop)

- Trip.com layout: gradient search band (destination, check-in/out range picker with nights pill, rooms and guests modal with per-room steppers, white Search button), sticky-then-flowing left sidebar (map card, popular filters derived from the response, budget slider plus range chips capped at the 90th percentile, property type, guest rating, areas, amenities, facilities), results bar (count, in-results property search, sort), horizontal cards with sliding image track, hearts, rating badge, facility tags, cheapest room and per-night/total prices, "Check availability".
- Prices render as soon as `property.rooms` exists rather than after all polls.
- Sorting moved server side into `sortResults()` and applies during polling; previously the sort dropdown had no effect until polling finished.
- Slider min/max come from each hotel's from-price (`rooms[0][0].net`) instead of every rate, which removed a $14 million outlier ceiling.
- Map view: split screen, list on the left, Google map on the right. Marker click pans, raises `zIndex`, scrolls and highlights the matching card, and loads a detail panel (`loadMapDetail()` with `forkJoin` of `getPropertyInfo` and `findPropertyAvailability`, cached per code) showing gallery, description, and a rooms table reduced to the cheapest rate per room type; any row opens hotel info in a new tab. The later request to show only the selected property on pin click with a "Show all" toggle and to cut the ~20 s map load was analysed but the transcript shows it was interrupted and no `showAll`/single mode exists in the code; treat it as not done.
- Progressive loading (16 Sep): first poll `firstPollingLimit = 12` (about 10 priced hotels), then 100 per poll (`pollingLimitPerPage`); `searchVersion` and `pollChain` guards drop stale responses when filters or sort change mid-stream; `trackBy: trackCode` keeps cards stable; "Loading more hotels..." indicator until `complete: true`; map view refetches when the count changes. `propertyLimitPerPage` is 30 in the current code (it was 50 for part of the session).
- Header currency dropdown was hidden behind bands with `z-index: 30`; bands are now `z-index: 5` (activities keeps 30 on mobile only).
- Band destination change is now guided (`guided = true; openGuidedDates()`): desktop opens the band range picker (new `@ViewChild`s, `(onHidden)="guided = false"`), mobile opens the calendar; the range selection opens the pax dialog and the existing `applyPax()` runs `modifySearch()`. `destHint` is set on destination change so the band shows the new city while loading. A date-only change still leaves the URL unchanged until Apply (as before).
- Budget slider: after applying a budget on mobile the filtered response returns `currency: null`, and `loadPriceSlider` rebuilt the ngx-slider `translate` closure as `r.currency[2]`, throwing inside `ngAfterViewInit` so the reopened Budget sheet showed an unrendered slider. The base component now remembers the last non-null symbol in `priceSymbol` and captures it in the closure.

### 4.2 Hotel results (mobile)

- Rewritten (`hotel-results-mobile` template 1383 to 403 lines, SCSS 3764 to 543): sticky gradient band with summary card and separate Map button, edit panel with mobile autosuggest, `mobile-calendar` and pax bottom sheet, chip row (Sort, Filters, Location, Popular filters, Budget, Clear) opening bottom sheets that apply only on Apply, stacked cards with full-width 16:9 sliding image.
- Summary card race ("ROE", "0 adult") fixed with `paxCounts()` from parsed rooms and `resolveDestinationHint()` from the cached destination lookup; skeleton while the name resolves.
- The edit sheet's destination sidebar opens prefilled with the current destination text, selected and focused; its header is blue `--or`.
- Pagination stays page based (`ngx-pagination`); an accumulating "Show more" would need base changes.

### 4.3 Hotel info

- Rewritten: band (destination link, range picker or mobile calendar, rooms and guests, Update), header with subtle amber stars, gallery with lightbox, facts strip, sticky tabs with scroll spy, multi-room picker with cancellation/meal options, sticky booking card, mobile bottom bar.
- Reviews: Google reviews removed. `hotelReviews()` asks Hotelbeds with `TRIPADVISOR` then `HOTELBEDS` filters for codes missing from `hotels.hotel_reviews`, since the `reviews` filter on the main availability call drops hotels without ratings (4 of 12 Paris hotels vanished). TripAdvisor data is not enabled on the account yet, so ratings are the coarse 2.0 to 10.0 Hotelbeds steps until Hotelbeds switches it on.
- Rails: "You might also like", "Your saved hotels", "Recently viewed" from `HotelUserService`.
- Date change auto-searches: `modifySearch` updates the URL and relies on the `queryParams` subscription as the single trigger, falling back to `checkAvailability()` when navigation resolves `null` (identical URL). Availability requests per search went from two to one.
- Mobile gallery gap (16 Sep evening): the `<=768px` `.gallery` rule kept `grid-auto-rows: 220px`, so the implicit row holding the 72 px `.thumbs` strip was 220 px tall, leaving a 170 px void above the facts strip. Now `grid-template-rows: 220px; grid-auto-rows: 72px` (redundant `&.single` rule dropped); measured gap is the 22 px gallery margin, desktop unchanged.

### 4.4 Hotels home (`/search-hotels` with no params)

- Trip.com order: gradient curved band with "Find your next stay" and the search bar, "Your recent searches" cards (from `hotelSearchHistory` `action: 'list'`, capped at the 3 most recent unique destinations, past dates filtered), Popular properties worldwide (tabs), Popular cities in the visitor's country, Popular cities worldwide (curated top 50 minus own country), Popular properties in the country (tabs), Recently viewed / Saved, FAQs, Recommended links (8 per column with "Show more (N)").
- All links use check-in one month out, one night, 2 adults.
- Desktop: whole destination row focuses the autosuggest input, autofocus on load (no scroll, no dropdown), guests label omits "0 children", the date range picker no longer self-closes (same-day guard in `onRangeChange`).
- Mobile: destination field styled flush inside the 52 px row, autosuggest and calendar sidebars at `z-index: 1300` with blue `--or` close headers, closing the calendar after one tap restores the previous dates. Tapping anywhere on the `.field-dest` row (`focusDest()`, ignoring clicks inside `.options/.side-bar/.styled-input`) forwards to `autosuggest.showMobileNavigation()`, which focuses and selects the sidebar input synchronously so the iOS keyboard opens.
- Desktop auto-open of the date picker after a destination pick (committed in `6a9eaf8`/`e02dbda`): `openDatePicker()` focuses the date input via `@ViewChild(BsDaterangepickerDirective, { read: ElementRef })` and calls `datePicker.show()` inside a `setTimeout`, only when `isPlatformBrowser && !isMobile`; skipped when a property was picked (`fromHotel`, which searches instead). Verified headlessly: picker visible within about 120 ms, stays open through a slow click, focus on `input[name=dates]`, and no `bs-datepicker` in the DOM on an iPhone UA. At that stage the hotel results band was left re-searching immediately on destination change.
- Full guided chain (commits `0cbcaa6`/`64b3ca4`, after the user reported the chain "was there earlier but is not now"; git history showed no earlier commit with a complete destination-dates-pax-search chain, so the existing pattern was extended): `modifyDestination` sets `guided` and opens the range picker (desktop) or the calendar via `setTimeout(0)` (mobile, so the autosuggest sidebar's scroll unlock runs first). `onRangeChange` no longer early-returns on same dates; when `guided` and the emission came from an open picker (not the `[bsValue]` init echo) it opens the guests dialog and focuses its first enabled control. Done runs `search()` only when guided; backdrop or X aborts; picker `(onHidden)` and a manual `openMobileDates()` clear `guided`. Verified on desktop and mobile: Rome, Oct 20 to 23, Apply, navigated to `location=ROE&checkin=2026-10-20&checkout=2026-10-23` with results loaded.
- Prefill from the last search (`prefill(items[0])` from `recentSearches` on both home pages): destination code and name, dates if still in the future, rooms/adults/children (hotels via `historyRooms()`). Skipped when the user has already touched the form (`pointerdown`/`keydown`) or a destination is set; `loadHome` no longer overwrites prefilled dates; on desktop the prefilled text is selected so typing replaces it; the flag image is guarded with `*ngIf="location.country"` because history rows carry no country.

### 4.5 Activities results

- Viator layout: breadcrumb, "{Destination} Tours and Activities in {year}" heading (`headingName()` expands US state and Canadian province codes), search band identical to the hotel band (destination, from/to, travellers, Search), sticky chip bar with date and traveller pills plus category chips from `/segments/{lang}` with left/right arrows, 4-column card grid with sliding image track, badges from `promotions`, free cancellation from `rates[].freeCancellation`, duration, price with currency code.
- Filters: fixed right drawer (`clamp(400px, 40vw, 640px)` desktop, 100vw mobile), header originally orange and now `--navy` after the brand pass, custom checkboxes, Activity type (Hotelbeds "Categories" segmentation group only), languages, duration buckets, free cancellation, price slider with clamped thumbs and range fill.
- Price slider bounds: the range inputs were bound `[min]="0"`, so the track floor was 0 (label "0") while results started at 1,400 INR, and any brushed min handle read as a large value against a 0-based track; the `+` suffix showed whenever the max handle sat at the ceil. Now `setPriceBounds()` computes `priceFloor` (min rounded down), ceil (max rounded up) and `priceStep` = 10^(magnitude - 2) (100 INR for a 58k span, 1 USD), snaps handles to the ends on a new search or when they were already at the ends, and `priceCapped()` shows `+` only when more results remain unloaded. `rangePct`, `onPriceInput`, `applyFilters`, `activeFilterCount` and `clearFilters` all use the floor; `&ngsp;` keeps the space in "59,900+ INR".
- Paging: `pageSize = 50`, `IntersectionObserver` sentinel with a manual "Show more" fallback, deduped by code.
- In-results keyword search: not sent to Hotelbeds (its `text` filter is case sensitive and loose: `eiffel` 0, `Eiffel` 48). The page fetches up to 8 pages / 400 activities and filters locally on name, category and description; suggestions are merged with a destination-scoped Mongo portfolio lookup.
- Cards open the activity in a new tab (`window.open`); the earlier card animation was removed at the user's request.
- Mobile: collapsed summary pill with Edit, full-width stacked fields, destination sheet (blue `--or` header, opens with the current destination selected and focused synchronously), mobile calendar, floating Filters button at bottom centre.
- Guided flow: destination pick opens the From picker, From opens To with earlier dates disabled. Since `64b3ca4` the To step opens the travellers dialog (Apply runs `modifySearch()`) instead of searching directly; dismissing either picker aborts via `onPickerHidden('from'|'to')`, and a `document:pointerdown`/`keydown` listener distinguishes a same-day click (ngx-bootstrap emits nothing then) from an outside click or Escape. On mobile `onMobileDates` skips the immediate search when guided and opens pax. A date-only change still searches immediately without pax.
- Date change (band or chip range picker, desktop and mobile) updates the URL with `queryParamsHandling: 'merge'` and triggers exactly one `searchActivities` request through the `queryParams` subscription. The desktop two-month range picker (620 px) used to be centred under the 116 px chip and started at x = -170, so its first columns were unclickable; fixed with `[placement]="$any('bottom left')"` plus `adaptivePosition: false` in `rangeConfig` (the `placement` input is typed as the four plain sides, so `placement="bottom right"` failed template type-check, and the flip modifier mangles variations unless it is disabled).

### 4.6 Activity info

- Redesigned with gallery and lightbox, facts strip, tabs with fixed scroll spy, booking card with date chips flattened across rate details (picking a date silently selects the matching `rateKey`), sessions and languages unioned across rate details, "Rate" list shown only when prices differ, duplicate modalities merged rather than dropped (Hotelbeds returns the same modality under different contracts), option list collapsed to 5 with "Show N more".
- Price breakdown reconciles line items with the total: units rounded to cents, rounding drift of up to 1 cent per traveller absorbed, `amountUnitType === 'SERVICE'` treated as per-group pricing (no phantom discount), "Taxes & fees: Included" row, promotion row.
- Travel dates and travellers can be changed inline; both rewrite the URL and call `fetchDetail()` directly (no `queryParams` subscription, so a single `activityDetail` request per change, with the loader shown). The desktop range picker overflowed the right edge of the viewport by 54 px; fixed with `[placement]="$any('bottom right')"` and `adaptivePosition: false`. On mobile the "Travel dates ... Change" row opens `<mobile-calendar>` with a blue header (`.mc-host ::ng-deep mobile-calendar .side-bar .close-nav`).
- Share modal (WhatsApp, Facebook, X, LinkedIn, Telegram, Pinterest, email, copy link with three fallbacks) and OG/Twitter meta tags with `og:image:width/height/alt`.
- Rails: "You might also like" (same destination and category), "Your saved activities", "Recently viewed" with arrow buttons and responsive card widths.

### 4.7 Activities home (`/search-activities` with no params)

- Viator-like: gradient band, recent searches (3 unique), "Top experiences worldwide" and "in {country}" tabbed rails, destination rails (local and curated worldwide), category chips, recently viewed and saved, perks, FAQ, recommended links. Backed by `activitiesHome`.
- Hotelbeds Activities returns no review data, so cards show category, duration, free cancellation and price instead of a rating chip. Istanbul and about 15 other curated cities did not resolve in the activities destination data (29 of 44 shown).
- Mobile bar fixed to match hotels home (52 px rows, sheet headers now blue `--or`, pax label "2 adults, 1 child"); desktop gets full-row click, same-day guard, autofocus, the guided destination-dates-travellers-search chain (`paxOpen`; the mobile destination sheet opens the calendar synchronously), and prefill from the last activity search (destination, dates, adults/children).

### 4.8 Checkout, confirmation and emails

- Both checkouts share `checkout-shared.scss` and the same three-step accordion (Contact, Guest/Activity details, Payment), inline per-field validation with green tick / red exclamation badges and messages under inputs, `scrollToFirstError()`, confirm-email and terms enforced, holder name copied to the first traveller and cardholder, phone mask `(000) 000-0000`, card brand icon rendered per brand as a fresh `<span>` (FontAwesome replaces `<i>` with `<svg>` once, so class toggling never worked), sticky summary with price-hold countdown, promo code toggle, "Book now · price CUR".
- Hotels keep the Affirm tile, sign-in banner and US-state select; activities keep required questions, the "You will be charged ... USD" note and supplier remarks.
- Both checkouts call `jumpToTop()` on init (instant scroll repeated at 0/300 ms, plus 1 s for hotels) because Bootstrap's `scroll-behavior: smooth` made the router's scroll-to-top animate from the previous page's offset.
- Payment step alignment (`checkout-shared.scss`): `.grid` uses `minmax(0,1fr) minmax(0,1fr)` with `gap: 12px 16px` so both payment grids, the "Cardholder & billing address" heading and every label share the same outer edges at 1024, 1280 and 390 px, all inputs 46 px; adorned fields (`.sel`, `.cc-wrap`) drop the tick/"!" glyph and their extra right padding; `.cc-brand` is a 40x24 box at `right: 8px` centred with `margin-top: -12px`, because its previous `transform: translateY(-50%)` was overwritten by the card's `rise` animation ending in `transform: none`, which is why the Visa badge sat about 12 px low.
- Processing overlay: shared `CheckoutProcessingComponent` (`<checkout-processing [active]>`): fixed backdrop `rgba(15,23,42,0.55)` with `backdrop-filter: blur(4px)`, `z-index: 100000` (header max is 510), `cursor: wait`, centred spinner card with "Processing your booking..." and "Please don't close or refresh this page", body scroll locked while active. The activity checkout binds it to `loading`; the hotel checkout replaced its inline `processingRequest` modal with it and gained a `bookingError` field plus `#checkout-error` alert, and the `postHotelData` subscription now resets `processingRequest` on HTTP error or non-success response (previously the spinner stayed forever).
- Activity checkout inline editing (hotel checkout has none: its "Change dates or rooms" link sends the user back to the hotel-info URL stored in the checkout session and re-pricing happens there): a "Change date or travellers" link-row in the summary toggles an inline panel with Date (desktop `bsDatepicker` on a button, `adaptivePosition: false`, placement bottom right; mobile `<mobile-calendar>` single-date `type='tour'` with blue header), Travellers stepper with child ages and "Update travellers" (bottom sheet with backdrop at 960 px and below) and a Start time `<select>` of sessions on the current date (disabled when only one). `reprice()` makes one `svc.detail()` call for the same activity and `modalityCode` with a `from ... from+6d` window, picks the rateDetail matching date, session and language, updates rateKey, dates, paxes, session, language, total, original amount/currency, cancellation policies, questions and raw detail, syncs traveller rows (entered names preserved, step 2 un-done if rows were added), rewrites the URL (`from/to/adults/children/childAges/start`, `replaceUrl`) and restarts the hold timer. When the date is unavailable it shows an inline message with up to four nearest available dates as chips and restores the previous selection, datepicker value and hold expiry. `canEdit()` blocks while submitting, re-pricing or expired; same-value changes are no-ops (also swallowing the datepicker's init emit). The info page `proceed()` navigates with the same query params via `selectionParams()`.
- Mobile overlays on the activity checkout are rendered at the page root: `<mobile-calendar>` is a direct child of `.checkout-page` and the pax sheet plus backdrop are an `<ng-template #paxPanel>` rendered there on mobile (inline in `.edit` on desktop), because the `.edit` panel's `rise` animation (`transform: translateY`) made `position: fixed` descendants fixed relative to the card and the calendar rendered inline and clipped under a stray "Done" row. `.side-bar` is forced to `position: fixed; inset: 0; height: auto; z-index: 1300` (no `100vh`, so Safari's toolbar viewport is respected); backdrop 1300, sheet 1301, both below the price-expiry modal (99999) and processing overlay (100000).
- Confirmation pages: hero with tick or pending clock, copyable reference, product card, trip facts, travellers, cancellation policy, voucher block, calendar/print, help block.
- Emails: new `include/activityConfirmation.html` filled by `renderActivityConfirmationEmail()`; `bookingConfirmation.html` re-themed; both use navy header, violet CTA with the orange gradient, weight 500 except the 24 px title.

### 4.9 Affirm

- Shown only when the display currency is USD or CAD (`affirmSupported(currency, amount)`); CAD additionally requires a minimum of 300 (`MIN_AMOUNT`).
- Two accounts, mirroring `affirm-php-master/config.php` and the flights implementation: USD `U76SJ0SGVF2UL76D` on `cdn1.affirm.com`, CAD `RUN3U5N0RFA3ZFN1` on `cdn1.affirm.ca` (`country_code: CAN`, `locale: en_CA`); sandbox keys exist for localhost checkout. `AffirmService.load(currency)` swaps accounts when needed. The "as low as" widgets use production keys even on localhost because the sandbox returned no prices; the checkout modal still uses sandbox locally.
- Amounts sent are the displayed amounts in that currency. This went back and forth: first per-currency accounts, then USD only ("hotels and activities always bill in USD"), then the final decision to price and charge in the display currency for USD and CAD and hide Affirm elsewhere. Note that Hotelbeds rates arrive in the account's contract currency (USD) and CAD is our conversion via `fetchCurrencyPrice`.
- Surfaces: results pill under total (desktop and mobile), hotel info sidebar card, per-room pill, mobile bar, checkout sidebar "Or pay over time" card and payment method tile. `AffirmAsLowAsDirective` sets `data-amount` in cents and debounces `affirm.ui.refresh()`. Checkout Affirm authorisation is wired like flights.

### 4.10 Payments (Moneris and Connex)

Finding: hotel and activity card bookings had never been charged. `validateCreditCard()` only ran a $0 Connex `/verify` for every currency and ignored its `capture` flag.

Now, in `process.php` (reusing `api-stage/include/moneris/mpgClasses.php` and `MonerisInterface.php`, same live CAD store as flights):

- `paymentGateway()`: CAD to Moneris, everything else to Connex.
- `buildPayment()`: non-CAD/USD amounts converted to USD from the untranslated base amount so the markup is not applied twice.
- CAD: `authorizeMoneris()` pre-auth before the supplier booking; `settlePayment()` completes the full amount after confirmation; a 0.00 completion voids the hold if the booking fails.
- USD/other: Connex `/verify` before booking, `connexSale()` (`/api/v1/sales`, auth plus capture) after confirmation.
- Capture failure after a confirmed booking emails confirmations/nadeem/nishant (`paymentFailureAlert()`).
- Payment record stored in `hotel_bookings.posted_data` (`$data['payment']`) and, for activities, `Activities::persistPayment()` writes `payment_log`, `captured_at`, `captured_by` on `apitude_activities_bookings` (columns auto-added).
- Hotel pending bookings (non-refundable rate or realtime failure; confirmed manually by staff) remain verify-only. Test/bypass emails (`nishant@skybooker.com`, `nadeem@destinaholidays.com`) skip payment and force pending; `realtimebooking@destinaholidays.com` forces a realtime booking regardless of cancellation policy.
- Activity checkout now sends `originalAmount`/`originalCurrency`.
- Verified: Moneris sandbox preauth, completion (`Complete=true`) and void. Connex has production-only credentials, so the USD sale path was code-reviewed against the working Umrah sale payload but never executed.

Related booking gating (13 Sep): `processBooking()` rebuilds `rooms`/`paxes` from `recheckData`; a rate is refundable only if `cancellationPolicies[0].from` is more than 24 h away; any non-refundable rate forces pending. A commit rewrote `process.php` from CRLF to LF, which inflated that diff to 9,817 lines; the real change was about 321 lines.

### 4.11 User tracking

- `ActivitiesUser` (`activities-user.php`) with `HotelsUser extends ActivitiesUser` (`hotels-user.php`) writing to their own database. Documents carry `visitor_id`, optional `email`, `expires_at`, `notified_at`; `attachEmail()` back-fills the email onto all of a visitor's rows when the identity is learned.
- Frontend `ActivityUserService` and `HotelUserService` share the `act_vid` cookie and read the email from `localStorage.user_details`; hearts render instantly from the cookie/localStorage copy and sync with Mongo on load.
- Search history list endpoint dedupes by destination and honours `limit`.
- Google One Tap: `GoogleOneTapService` started from `AppComponent` (`auto_select`, FedCM), skipped for bots/headless/no cookies, backs off 24 h on dismissal, calls `quickLogin` and `activityIdentify`, then reloads. The old inline `handleCredentialResponse` in `index.html` and `DataService.oneTapLoader()` were removed. Sentry `denyUrls` drops `gsi/client`, GTM and Brevo errors (the "Error: ta" issue was Google's `/gsi/status` 400 from bot traffic).

### 4.12 Autosuggest

- Hotels: `hotel-autosuggest` shows grouped Destinations (flag, property count) and Hotels (icon, name, city and country). Destination rows come from a local popular list (`hotel_dest_popular`) merged with `suggestDestinations&q=` through a 220 ms debounced `switchMap`. Hotel rows open `/search-hotels/hotel-info/{code}?location={destinationCode}...` in a new tab on desktop (same tab on mobile) and, since 16 Sep, also emit a destination object with `fromHotel: true` so the current tab searches the property's city.
- Server ranking: destination first, then property scoped to the detected place, then name anywhere, then broad name/address/city/zone; hotels honour the request limit (6 to 12). Examples that now resolve: "Hyatt Woodlands", "Houston Hyatt", "1909 Research Forest", the full slashed name. The previous 6-item alphabetical cap dropped `Hyatt Place Houston/The Woodlands` (code 562524, zone "The Woodlands").
- Hover text on the dark autosuggest row was invisible; the light hover rule now matches the older rule's nesting depth with an `!important` background safety net.
- Activities: keyup goes to the server (`debounceTime(250)`, `switchMap`) and matches activity name or destination name across all destinations (`portfolio`, `factsheets` fallback), with popular destinations ranked first and two-line rows (type and location). Activity rows open the activity in a new tab. The earlier client-side `suggestindex` approach was built and then removed at the user's request.

### 4.13 Mobile UX

- `mobile-calendar`: `type="activity"` range mode, `isRange()`, sticky header instead of padding hacks, first-month header and weekday row, previous-month tail days removed, scoped auto-scroll to the selected month, 7-column grid using `14.2857%` cells instead of `100vw / 7`, theme via `--ac`/`--ac-dark`/`--ac-soft` with fallbacks, `visibility: hidden` when closed, half-selection restored on close.
- Destination sheets on activities pages and the `#ismobile` autosuggest sidebar on hotels pages, both with blue `--or` headers and body scroll lock. Inputs inside them are focused and selected synchronously in the same tap handler (no `setTimeout`) so iOS opens the keyboard; the hotel sidebar's `.side-bar { transition: 0.1s }` had been animating `visibility`, so `focus()` was refused while the element was still computed hidden, and it is now `transition: width 0.1s`.
- `isMobileView()` replaced UA-only sniffing so Chrome responsive mode renders the mobile components.
- FontAwesome SVG replacement drops Angular's `_ngcontent` attribute on real phones; band icons are wrapped in `<span class="field-icon">` to keep scoping.

### 4.14 Performance numbers

| Change | Before | After |
|---|---|---|
| Hotel results first response payload | 605 to 651 kB, ~15 s | 44 to 60 kB; follow-up polls ~1 kB |
| Paris polling | stopped at 85 hotels (session over 1 MB) | 6 polls accumulate 27 -> 440 with `totalPropertyCount` 2779 intact |
| Houston first API batch | 4.4 s | 1.5 to 1.8 s |
| Houston first cards in browser | 6.8 s | ~3.7 s desktop, 3.9 s mobile |
| Houston full set (811 properties, 595 priced) | 44 to 67 s | 19 to 23 s |
| Non-USD 100-hotel batch | 4.6 to 7.7 s | 1.6 to 2.3 s |
| Hotels home | 19 s cold in one run, ~7 s with image cache | instant when cached (6 h) |
| Activities home | 5.1 to 8.6 s cold | ~0.8 s cached |
| Destination list download | two 933 kB `getAllDestinations` calls (duplicate `DataService` instances) | ~30 kB popular list plus per-query suggest |
| Hotel reviews lookup | 2.0 s | 0.6 s cached |

### 4.15 Bundle size and lazy loading

Findings (commit `9ee4bef`): all four verticals were already lazy routes (`loadChildren`) in `app-routing.module.ts`, and because Ivy compiles component references locally the production build already split the autosuggests and calendar into shared chunks; no vertical's feature chunk was downloaded by another route. The coupling was `IncludesModule` (a grab-bag of three autosuggests, mobile calendar, flight loaders, checkout-processing, ngx-bootstrap popover/tooltip/datepicker/dropdown, ngu-carousel, lazyload) plus `AppComponent -> GoogleOneTapService -> ActivityUserService -> ActivitiesService/PriceExpiryService`, which put activities services into `main.js`.

Changes: the module split described in 2.2; `FlightAutoSuggestComponent` no longer pulls `AirportsStaticComponent` and `airports.json` in a 1 s post-init `setTimeout`, instead an idempotent `import('../flights/airports.json')` runs on first focus/keyup with the existing skeleton loader (webpack emits it as its own 577 KB chunk; `airports-static.component.ts` is now dead code); moment is aliased to its locale-free build through `tsconfig.json` `paths: { "moment": ["node_modules/moment/ts3.1-typings/moment", "node_modules/moment/min/moment.min.js"] }` (TypeScript resolves the typings entry, the CLI webpack paths plugin falls through to `moment.min.js`; applies to browser, server, serve and dev:ssr without `angular.json` changes; both builds print a harmless `Can't resolve './locale'` warning from moment's guarded locale `require`). No route paths, resolvers, templates or component logic changed apart from the flight autosuggest loader.

Per-route JavaScript (production, es2017, minified KB, measured in headless Chromium against the SSR server):

| Route | Baseline | After module split | After airports.json on focus and moment alias | Change |
|---|---|---|---|---|
| `/` | 2393 (main 699 incl. activities services, shared 637, airports.json 577, home 341, autosuggests 31+28+19, calendar 21) | 2380 (main 692, shared 605, airports.json 577, home 347, widgets ~100, ngu-carousel 20) | 1488 (main 692, home 347, shared 290, widgets ~100; airports.json only on focus) | -905 KB (-38 percent) |
| `/search-hotels` | 2221 (hotels 739, main 699, shared 637, hotel-autosuggest 28, calendar 21, pagination/progress 29, mask 28) | 2184 (hotels 739, main 692, shared 605, mask + checkout-processing 30) | 1871 (hotels 741, main 692, shared 290) | -350 KB |
| `/search-activities` | 1840 (activities 415, main 699, shared 637, calendar 21, mask 28) | 1840 (activities 443 after price-expiry work, main 692, shared 605, common 9) | 1530 (activities 448, main 692, shared 290) | -310 KB |
| `/compare-flights` | 2414 (flights 401, main 699, shared 637, airports.json 577, flight-autosuggest 31) | 2380 (flights 406, main 692, shared 605) | 1488 (flights 406, main 692, shared 290, flight-autosuggest 31) | -926 KB |

Initial ES2017 bundle 922 -> 914 KB; SSR renders `app-home-page`, `app-hotels-home`, `app-activities-home` and `app-flight-results` markup on the four routes; typing "Lon" in the homepage flight input downloads the airports chunk on focus and shows five London suggestions.

Remaining weight, reported but not changed: `main.js` (692 KB) is Angular plus Sentry (`@sentry/tracing` with `BrowserTracing`, `tracesSampleRate: 1.0`, `TraceService` in `AppModule` providers, about 270 KB of source on the critical path) plus `@skybooker/common-components` (about 180 KB, which bundles `angularx-social-login`); the homepage's three autosuggests and calendar (about 100 KB in shared chunks, only the active tab is instantiated thanks to `*ngIf="displayTab == N && pageLoaded"`); `tour-autosuggest` (19 KB) is downloaded on `/` only because the template still references it under `*ngIf="displayTours"`, which is always false. No preloading strategy was added (default `NoPreloading`); `angular.json` production config already uses Angular 12 optimisation defaults.

## 5. Notable bugs fixed and root causes

| Symptom | Root cause | Fix |
|---|---|---|
| Hotel results loader bar invisible | Global `.ng-bar { background: var(--dark-blue) !important }` and 40 px placeholder overrode the `[color]` input; bar rendered in-flow inside the band | Scoped `::ng-deep` overrides pinning `ng-progress` to the band bottom, orange `--loader` |
| Header and footer turned lavender | Page background and brand font/colour were on `:host`, inherited by the nested header/footer | Backgrounds moved to page wrappers; `brand-tokens` applies typography to `> :not(common-header):not(common-footer)` |
| Desktop date picker opens and closes instantly (results, hotel info, hotels home) | `dateRange` getter returned a new array every change detection, or `onRangeChange` lacked a same-day guard; ngx-bootstrap treated the echoed value as a completed selection and called `hide()` | Memoised `rangeRef`; guard returning early when the range equals current dates |
| Mobile search rows 29 px tall | `.field-outercheck { flex: 1 1 0 }` zero flex basis overrode `height: 52px` in the stacked layout | `flex: none` inside the mobile media block (hotels home and activities home) |
| Mobile summary showed "ROE" and "0 adult" | Base component set `location = {name: code}` and computed pax after a ~700 ms async lookup | `paxCounts()` from parsed rooms, `resolveDestinationHint()` from cached lookup, skeleton |
| In-results dropdown behind card images | Wrapper z-index toggled off after picking a result; card images create stacking contexts (`will-change: transform`) | `.kw-host { z-index: 4 }`, `(focus)` sets `focusInput = true` instead of toggling |
| Exact hotel name not found in in-results search | `preg_match('/'.QUERY.'.*$/')` broke on `/` and was order-sensitive, name only | Token based, punctuation insensitive, name + address + city + zone |
| Build error `Object is of type 'unknown'` on `slice` | `localExperiences` typed `any[]` made `SlicePipe` infer `unknown` | `ExperienceGroup` interface |
| Hotel info sent two availability requests per search | `modifySearch` called `checkAvailability()` and the `queryParams` subscription fired too | Subscription is the single trigger, fallback when navigation resolves `null` |
| Activity date chips did not respond to clicks | `*ngFor` over methods returning fresh wrapper objects; without `trackBy` the buttons were recreated between mousedown and mouseup | `trackBy` functions, `type="button"` |
| CORS error on wishlist/search-history POSTs | Plain object body sent `application/json`, forcing a preflight the server answers with `302` | `JSON.stringify` body (`text/plain`) like every other call |
| Activities `destinations` collection empty | API nests the list under `country.destinations`; portfolio returns a bare array and `204` without a destination | `extractList()` with dotted paths; offset advanced by items returned; per-run dedupe |
| Empty autosuggest cached for 6 h | `cached()` stored zero-item payloads | Never cache empty; `refresh=1`; `max-age: 0` on empty |
| "eiffel" returned no results | Hotelbeds `text` filter is case sensitive | Local filtering across fetched pages |
| Price slider max in the millions | Backend range computed over every rate including a glitchy suite rate | Range from per-hotel from-price |
| Sort ignored while polling | Sorting only in `filterResults` | `sortResults()` in both paths |
| Card brand icon never changed | FontAwesome replaces `<i>` with `<svg>` once | Fresh `<span class="cc-brand">` per brand via `ngSwitch` |
| Mastercard detected only after 16 digits | `getCardType` prefix logic | Prefix detection (also helps flights) |
| Stars rendered black on hotel info | Colour on `i` did not reach the swapped `<svg>` | Colour on the `.stars` container |
| Hotel info mobile min-width overflow | `1fr` grid track has `min-width: auto` | `minmax(0, 1fr)` and `min-width: 0` |
| Empty 160 px gap under activity gallery on mobile | `grid-auto-rows: 240px` kept for the 80 px thumb row | Explicit `grid-template-rows` |
| Activity info mobile calendar rendered 6000 px down | `<mobile-calendar>` inside `.book.reveal`, whose animation ends with a `transform`, breaking `position: fixed` | `.mc-host` moved to the top of `.info-page` |
| Mobile calendar white header and invisible months on activities | Colours depended on `--primary`/`--alternate` set only by `setPalette()` on hotel pages | Theme via `--ac` with fallbacks |
| CTAs purple on hotel pages, orange on activity pages | `--or` fell back to `--alternate`, which is purple in the domain palette | Pinned values in the token file |
| Book now on mobile did not land at the top of checkout | Bootstrap `:root { scroll-behavior: smooth }` animated the router scroll and iOS interrupted it | `jumpToTop()` instant scroll |
| Checkout over-scrolled to the next step | `scrollIntoView` measured before the previous step collapsed | Deferred scroll |
| "What's included" tab highlighted "Additional info" | Scroll spy used only changed IntersectionObserver entries | Track the visible set, highlight the top-most |
| Lazy hotels module died silently | `@Directive()` base extending a component | `@Component({ template: '' })` abstract base |
| Header currency dropdown behind bands | Header sets dropdown `z-index: 10` on hover; bands were 30 | Bands at 5 |
| Copy link did nothing | `navigator.clipboard.writeText` rejected and the error was swallowed | `execCommand` fallback, then a "Press Ctrl+C" hint |
| Hotel map/list only 10 results locally | Localhost hit the Hotelbeds sandbox | Activities are live everywhere (`SERVER_NAME` switch removed) |
| `process.php` diff of 9,817 lines | Python edit rewrote CRLF as LF | Diff with `--ignore-cr-at-eol`; suggestion to add `.gitattributes` |
| iOS keyboard did not open when the mobile destination sidebar appeared | Focus was deferred with `setTimeout`, and `.side-bar { transition: 0.1s }` animated `visibility`, so at t = 0 the input was still computed `hidden` and `focus()` was refused | Synchronous `detectChanges()` then `value/focus()/select()` in the tap handler; `transition: width 0.1s` |
| Only the small input area opened the mobile destination sidebar | Row click handler did not forward to the autosuggest on mobile | `focusDest()` calls `showMobileNavigation()`; icon has `pointer-events: none` |
| Activity results desktop range picker unclickable / activity info picker off-screen | ngx-bootstrap centres the 620 px two-month picker under a 116 px host (x = -170) or past the right edge (+54 px); `placement="bottom right"` fails type-check and the flip modifier mangles placement variations | `[placement]="$any('bottom left' / 'bottom right')"` with `adaptivePosition: false` |
| Mobile calendar header navy instead of blue on activity info | `.mc-host ::ng-deep .side-bar .close-nav` tied on specificity with the calendar's own rule and lost on source order | Selector `.mc-host ::ng-deep mobile-calendar .side-bar .close-nav` |
| 170 px empty gap under the hotel info gallery on mobile | `grid-auto-rows: 220px` applied to the implicit row holding the 72 px thumb strip | `grid-template-rows: 220px; grid-auto-rows: 72px` |
| Build error `placement="bottom right"` | Input typed `'top' \| 'bottom' \| 'left' \| 'right'` | `$any(...)` binding (commit `e02dbda`) |
| Activity checkout mobile calendar rendered inline and clipped inside the summary card, with a stray "Done" row | `<mobile-calendar>` sat inside `.summary > .edit`, whose `rise` animation ends in a `transform`; a transformed ancestor makes `position: fixed` relative to that ancestor | Calendar and pax sheet rendered at the `.checkout-page` root (`ng-template` on mobile), `.side-bar` forced to `fixed; inset: 0`, ancestor chain verified free of `transform`/`filter`/`will-change`/`contain` |
| Visa/Mastercard brand badge sat about 12 px low in the card field | `.cc-brand { transform: translateY(-50%) }` overwritten by the `rise` keyframes' final `transform: none` | Fixed 40x24 box centred with `margin-top: -12px`; adorned fields show border state only |
| `ng serve` silently stopped serving new code for everyone (nothing after about 15:05) | A `Date \| null` value bound to `[bsValue]` failed `strictTemplates` and stalled the incremental build | `[bsValue]="$any(dateValue)"` in the activity checkout template |
| Activities price slider floor at 0 and phantom `+` | Range inputs bound `[min]="0"` while results start well above 0; `+` shown whenever the handle was at the ceil | `priceFloor`/`priceStep`/`setPriceBounds()`, `priceCapped()` |
| Hotel Budget sheet reopened with an unrendered slider on mobile | Filtered response returns `currency: null`; the ngx-slider `translate` closure dereferenced `r.currency[2]` and threw in `ngAfterViewInit` | Remember the last non-null symbol in `priceSymbol` and capture it in the closure |
| Hotel checkout spinner stayed forever after a failed booking request | `postHotelData` subscription never reset `processingRequest` on HTTP error or non-success response | Reset the flag, show `bookingError` in `#checkout-error`, shared `CheckoutProcessingComponent` |
| Reloading the activity checkout redirected to `/search-activities` | Selection lived only in the in-memory `state.selected`; the header's currency auto-detect reloads the page | `session` URL param plus `restoreSession()` from `activitySessionStatus` `selection` |

## 6. Known issues and follow-ups not done

- Memcached session size: for very large destinations (Paris, about 2,800 hotels) the session exceeds the item size limit around 1,700 priced properties; the write fails silently and the next poll sees `totalPropertyCount: 0`. Needs a slimmer session or a different store.
- `clearFilters()` assigns `[]` to filter maps, dropping string-keyed property-type and amenity selections after `JSON.stringify`.
- Connex USD sale (`/api/v1/sales`) is untested live; one small real USD booking is recommended before relying on it. Moneris CVD could only be tested without CVD on public test stores.
- Hotel pending bookings are verify-only; the user was asked whether to pre-auth those too.
- Desktop map view: single-property mode on pin click with "Show all", and the ~20 s map load, were not implemented.
- Activity info: the first `activityDetail` request fires before the shared header writes `currency` to `localStorage`, so in a fresh profile the page loads in USD and a later re-fetch (date or pax change) switches to INR or the visitor's currency. Pre-existing, not fixed.
- Activity info: dev-mode `NG0100 ExpressionChangedAfterItHasBeenChecked` from the rail navigation `[disabled]="!railCan(railEl, ...)"` surfaces the webpack overlay locally. Pre-existing, not fixed.
- Guided chain gaps: on hotel results a date-only change leaves the URL unchanged and does not open pax; on activity results a date-only change still searches immediately without pax; the activity results From picker does not move keyboard focus. The activity checkout start-time change path could not be exercised live because the test activity had a single session.
- Hotel checkout session parity: `cache.hotel_checkout_cache` has no `expires_at` or TTL index and the 45 minute hold is client side only, while activities now have a server-driven 20 minute session; bringing hotels onto the same server-side expiry model was not done.
- Sentry `BrowserTracing` with `tracesSampleRate: 1.0` is initialised synchronously in `main.ts` and sits on the critical path (about 270 KB of source in `main.js`); deferring `Sentry.init` to idle, dropping tracing or lowering the sample rate is a product decision that was not taken.
- `@skybooker/common-components` bundles `angularx-social-login` into `main.js`; shedding it needs the library to expose header/footer without the social-login module.
- `src/app/flight-autosuggest/airports-static.component.ts` is dead code after the `import()` change and can be deleted.
- The homepage still references `<app-tour-autosuggest>` under `*ngIf="displayTours"` (always false), so the 19 KB tour-autosuggest chunk is downloaded on `/`; removing the dead tab markup would drop it. Lazily creating the non-default homepage tab components via `import()`/`createComponent` was judged not worth the wiring risk for about 13 KB gzipped.
- Both builds print a harmless webpack warning `Can't resolve './locale' in node_modules/moment/min` from the moment alias; it is a warning, not an error.
- The activity checkout error text on a failed rate-key refresh is the raw `HttpErrorResponse.message` (pre-existing).
- Istanbul and about 15 curated cities are missing from the activities destination data; `activities.factsheets` and possibly `portfolio` on stage depend on `sync-static-activities.yml` running in `portfolio`/`content` mode.
- Hotelbeds Activities has no ratings; TripAdvisor hotel ratings require the add-on on the Hotelbeds contract.
- `hotels/email-confirmation/email-confirmation.component.html` (legacy static mock) and `property-search.component.html` still contain `font-weight: 700`/`bold` inline.
- Grey spinner above the header on iOS could not be reproduced; most likely Safari's pull-to-refresh indicator or the shared `common-header`.
- `loadPropertyInfo()` still refetches static property info on every date change on hotel info (cheap, cached server side).
- In-results search only matches properties already in the session; a query typed during polling may miss late hotels until the next keystroke.
- Activity checkout promo code is UI only (no endpoint). Hotel results pagination replaces the list per page rather than accumulating.
- The deployed build tested on the user's iPhone around 20:03 predated commit `cc01600`; the activity checkout mobile calendar fix needs a redeploy to be visible there.
- Only Houston-scale timings were measured; Paris first batch was 2.6 s but the full set is blocked by the session issue.
- On localhost, opening the Affirm checkout modal switches the global script to sandbox, blanking the sidebar pill until reload.
- `api-stage/include/StaticActivities.php` and its workflow are on disk but not visible in that repo's recent commits; confirm they are committed and deployed.
- Recommendation from the session: add `.gitattributes` with `* text=auto eol=lf` to `hotel-services` to avoid future CRLF/LF whole-file diffs.

## 7. Verification approach

- Angular: `npx tsc -p tsconfig.app.json --noEmit`, `ngc` with `strictTemplates` (sometimes against a temp copy of `src` when the working tree had an unrelated break), full `ng build --configuration development` (needs `NODE_OPTIONS=--openssl-legacy-provider`), and checking served lazy chunks (`curl http://localhost:4200/src_app_hotels_hotels_module_ts.js | rg -c <symbol>`).
- Routes on the dev server must be curled with `-H 'Accept: text/html'`; without it webpack-dev-server returns 404 instead of the SPA fallback.
- PHP: `php -l` on every touched file; endpoints exercised with `curl` against `http://stage.flightomart.com/hotel-services/process.php` after saving (auto-upload), including replaying the polling loop and reading timings from a temporary `X-Timing` header (removed afterwards).
- Browser checks: Cursor's browser tool (CDP, `Runtime.evaluate`, hit testing, screenshots) when it worked, otherwise headless Playwright scripts under `/tmp` at 1280 px and 390x844 / 375x812 with iPhone UA, measuring `scrollWidth`, element sizes, `document.activeElement`, request counts and console errors. Screenshots were saved under `/tmp` (for example `/tmp/hotels-mobile.png`, `/tmp/kw_dropdown.png`, `/tmp/act-m-0.png`, `/tmp/hres-after-loaded.png`).
- Payments: Moneris sandbox (`mpg1t.moneris.io`, `store5`) through a CLI script under `/tmp`; Connex not executed.
- Mongo: PHP scripts on stage (no local mongodb extension), `mongosh` guidance for the operator; the activity session TTL and unique indexes, document shape and `valid: false` after expiry were confirmed with a temporary probe script and stage curls.
- Booking-path checks on the checkouts used Playwright `page.route` to hold and then abort or stub the booking POST (and to stub `expiresAt`/`SESSION_EXPIRED`), so no real booking reached stage.
- Bundle measurements: `ng build --configuration production --stats-json` plus per-route chunk totals captured in headless Chromium against the SSR server (`ng run homepage-ssr:server`), grepping every chunk downloaded on `/` for hotels/activities/flights feature markers.
- During parts of 15 and 16 Sep the shell tool hung for minutes because of concurrent `ng serve` rebuilds; several verifications were therefore delegated to background workers or confirmed by the user's own build terminal.

## 8. Appendix

### 8.1 Files changed in `homepage-ssr-app` (git, `b97ca2c..9ee4bef`)

Added:

- `src/brand.scss`, `src/checkout-shared.scss`
- `src/app/hotels/affirm-as-low-as.directive.ts`, `src/app/hotels/hotel-results/hotel-results-ui.component.ts`, `src/app/hotels/hotels-home/hotels-home.component.{ts,html,scss}`
- `src/app/search-activities/activities-entry/activities-entry.component.ts`, `activities-home/*`, `activity-checkout/*`, `activity-confirmation/*`, `activity-info/*`, `activity-results/*`, `currency-utils.ts`, `search-activities-routing.module.ts`, `search-activities.module.ts`, `state.ts`
- `src/app/services/activities-static.service.ts`, `activities.service.ts`, `activity-user.service.ts`, `affirm.service.ts`, `google-one-tap.service.ts`, `hotel-user.service.ts`

Modified:

- `src/styles.scss`, `src/index.html`, `src/main.ts`, `angular.json`, `package.json`, `.github/workflows/deploy.yml`, `.gitignore`, `.htaccess`, `.npmrc`, `scripts/concat-elements.js`
- `src/app/app-routing.module.ts`, `src/app/app.component.ts`, `src/app/includes/includes.module.ts`
- `src/app/hotel-autosuggest/hotel-autosuggest.component.{ts,html,scss}`
- `src/app/hotels/hotel-checkout/*`, `hotel-confirmation/*`, `hotel-info/*`, `hotel-results-desktop/*`, `hotel-results-mobile/*`, `hotel-results/hotel-results.component.ts`, `hotels.module.ts`, `property-search/property-search.component.{html,ts}`, `results/results.component.{html,ts}`, `search-box/search-box.component.{scss,ts}`, `email-confirmation/email-confirmation.component.html`
- `src/app/mobile-calendar/mobile-calendar.component.{ts,html,scss}`
- `src/app/services/data.service.ts`
- `src/app/home-page/home-page.component.{html,ts}`, `flight-autosuggest`, `flights/flight-results-*`, `tour-autosuggest` (minor), `src/assets/common-elements.{css,js}`, `src/assets/elements-example.html`

Files touched by the evening commits `6a9eaf8` ("fixed hotel autosuggest"), `e02dbda` ("fixed build errors") and `804301f` ("fixed sidebar"), all now committed with a clean working tree:

- `src/app/hotel-autosuggest/hotel-autosuggest.component.{html,scss,ts}`
- `src/app/hotels/hotels-home/hotels-home.component.{html,scss,ts}`, `src/app/hotels/hotel-info/hotel-info.component.scss`, `src/app/hotels/hotel-results-mobile/hotel-results-mobile.component.scss`
- `src/app/search-activities/activities-home/activities-home.component.{html,scss,ts}`, `activity-info/activity-info.component.{html,scss,ts}`, `activity-results/activity-results.component.{html,scss,ts}`

Files touched by the late-evening commits `835286b` ("f8ixed hotels", 19:53), `cc01600` and `b2f5a5b` ("fixed activities", 20:09/20:20), `0cbcaa6` ("fixed date picker", 20:23), `64b3ca4` ("fixed guided flow", 20:32) and `9ee4bef` ("fixed module lazy loading", 20:46); working tree clean afterwards (`git diff --name-status 804301f HEAD`):

Added:

- `src/app/includes/checkout-processing/checkout-processing.component.ts`
- `src/app/search-activities/price-expiry/price-expiry-modal.component.{ts,html,scss}`, `price-expiry/price-expiry.service.ts`
- `src/app/mobile-calendar/mobile-calendar.module.ts`, `src/app/hotel-autosuggest/hotel-autosuggest.module.ts`, `src/app/flight-autosuggest/flight-autosuggest.module.ts`, `src/app/tour-autosuggest/tour-autosuggest.module.ts`

Modified:

- `tsconfig.json` (moment alias), `src/checkout-shared.scss`
- `src/app/includes/includes.module.ts`, `src/app/flights/flights.module.ts`, `src/app/home-page/home-page.module.ts`, `src/app/hotels/hotels.module.ts`, `src/app/featured-hotels/featured-hotels.module.ts`, `src/app/search-box/search-box.module.ts`, `src/app/static/static.module.ts`, `src/app/search-activities/search-activities.module.ts`
- `src/app/flight-autosuggest/flight-autosuggest.component.ts`, `src/app/services/google-one-tap.service.ts`, `src/app/services/activities.service.ts`
- `src/app/hotels/hotel-checkout/hotel-checkout.component.{html,ts}`, `hotel-results-desktop/hotel-results-desktop.component.{html,ts}`, `hotel-results-mobile/hotel-results-mobile.component.ts`, `hotel-results/hotel-results-ui.component.ts`, `hotel-results/hotel-results.component.ts`, `hotels-home/hotels-home.component.{html,ts}`
- `src/app/search-activities/activities-home/activities-home.component.{html,ts}`, `activity-checkout/activity-checkout.component.{html,scss,ts}`, `activity-info/activity-info.component.{html,ts}`, `activity-results/activity-results.component.{html,ts}`, `state.ts`

### 8.2 Files changed in `hotel-services` (git, `e6af110..b9f1bc9`)

- `process.php` (about 1,744 lines changed ignoring CR/LF), `activities.php` (certification logging removed, `searchData()`/`contentData()`, `persistPayment()`), `activities-static.php` (new), `activities-user.php` (new), `hotels-user.php` (new)
- Late-evening commits `23ee165` ("added sessions for activities", 19:15) and `b9f1bc9` ("update activities", 20:44): `activities.php` (`cache.activity_sessions` collection with lazy TTL and unique indexes, `sessionState()`/`sessionStatus()` returning `selection`, `activityDetail` session creation) and `process.php` (`activitySessionStatus` route, `SESSION_EXPIRED` rejection in `processActivityBooking`)
- `include/activityConfirmation.html` (new), `include/bookingConfirmation.html`, `include/session.php` (currency memo), `include/constants.php`
- Vendored: `include/MonerisInterface.php`, `include/moneris/**` (mpgClasses and examples), `include/affirm-php-master/**`

Also in `api-stage`: `include/StaticActivities.php` and `.github/workflows/sync-static-activities.yml` (on disk; `api-stage/portal/process.php` live activities credentials committed earlier).

### 8.3 Chronological list of user requests

13 September

1. Read `hotel-services/activities.php`; read the `search-activities` folder.
2. Results page like Viator Paris with cards.
3. Pasted Hotelbeds Content API docs; use documented fields.
4. Destination search box above the heading with dates/adults/children; filters as a sidebar (desktop pushes content, mobile overlay).
5. Cache Hotelbeds static content into a new `activities` Mongo DB; workflow to refresh every two weeks; PHP command to run it manually.
6. Endpoint services and autosuggest using that data, cached on page load.
7. Flights-style solid purple search band with progress bar; highlight in purple, match site colours.
8. Load autosuggest JSON client side like flights (later reversed: server-side keyup search by activity or destination name).
9. Date format "Mar, 19th 2026"; travellers modal; remove language and filters from the bar; attractive prices with currency code; 50 results with infinite scroll.
10. Destinations collection empty (fixed response shape); portfolio duplicates; still 0 results (stale cache); why only 10 results (sandbox); make everything live.
11. Fixed chip bar with arrows; hover arrows on card images; slide animation.
12. Wishlist heart in cookie plus Mongo with email; search history with 15-day expiry; Google auto-login like LinkedIn.
13. CORS error; higher quality images; overlay sidebar; price slider fix; activity type filter; sidebar 40 percent desktop / 100 percent mobile with orange header.
14. Heading "Miami Area, Florida Tours and Activities in 2026".
15. Card click animation, then reversed to open in a new tab; recently viewed, similar and liked rails.
16. Clearer font; share modal with social links and thumbnail; mobile results layout; fix share (copy link); arrows instead of scrollbars, bigger rail images.
17. Price breakdown; change dates on info page; "2 adults and taxes and total"; pasted activity JSON; remove age range; align "Included"; "Oct" not "October"; white search button orange on hover; collapse long option list; icon-only share/save.
18. Always allow commands (settings guidance); fix checkout page; rule clarified to rails-cakehr only.
19. Sort outline and hover; autosuggest by activity; visible loader line; auto-open From then To then search; two-line autosuggest rows; mobile min-width; discount display (per-group pricing); gallery gap.
20. Confirmation page and `bookingConfirmation.html` email; hotel realtime booking only when refundable, bypass email; remove certification logs; global Inter / Plus Jakarta Sans; why `process.php` changed so much (CRLF); does `nishant@skybooker.com` create pending confirmations.

14 September

21. Cross-destination activity search ("eiffel" while in Hyderabad); keyword search bar with autosuggest; mobile date picker for all pickers; edit files directly; date chips not responding; tab scroll spy and checkout over-scroll; "eiffel" no results; change adults on info page; hide keyword bar until search completes; Sentry "Error: ta"; remove legacy One Tap code.

15 September

22. Hotels like the screenshot with the same theme (desktop, then mobile); autosuggest like activities (933 kB downloads); break up the heavy response; 50 per page and no sidebar scrollbar; slider max 1021 / 4709; mobile images and autosuggest; hide "Found 0 of 0".
23. Hotel-style search band in activities; search by property or activity name globally; hotel info redesign; subtle stars; checkout and confirmation redesign; merge best of both checkouts; phone mask and card icon; inline validation with tick/exclamation.
24. Affirm monthly in checkout, results and info; only USD/CAD; two accounts; always bill USD (then reversed to per-currency); wire Affirm like flights; restore activities band layout; rails and Mongo tracking for hotels; activities tracking check; Hotelbeds ratings instead of Google; TripAdvisor preference; no horizontal scroll on mobile; header dropdown behind band; CAD minimum 300; date picker not opening; production Affirm on localhost; mobile date pickers on all four pages; mobile destination sidebar with orange close; scroll to top on Book now.
25. airportr.com theme for all pages and emails; Dribbble reference; orange loader in hotels; orange/red gradient buttons (then violet default, orange hover only, fade); sidebar padding; split map view with highlight; expanded property detail in map; cheapest room per type opening hotel info; sliding thumbnails everywhere; header/footer background regression; single-property map mode and 20 s map load (not completed).

16 September

26. Mongo query for a property by name; destination-first then property autosuggest priority ("taj delhi", "hyatt houston"); hover text visibility.
27. Hotels home like hotels.com, then like trip.com with FAQs and dated links; top 50 worldwide; recent searches below the bar with the gradient band; font weight 500; Viator-style activities home; property click searches the city in the current tab; collapse recommendation lists; mobile home search field; build error (`slice` typing).
28. Whole row clickable, date picker not opening, hide "0 children"; mobile sidebar close and hotel info auto-search on date change; capture CAD on Moneris and USD on Connex; Hyatt Place Houston/The Woodlands not found and dropdown behind image; autofocus destination on desktop; progressive first batch of hotels; mobile results header layout; activities home mobile search bar and 3 unique recents.
29. After 17:50: auto-open the date picker after destination select on desktop for hotels and activities; write this context document to `md_docs`; on mobile make the whole destination row open the sidebar, focus the input so the keyboard appears, prefill the home pages with the last search, prefill and select the destination in the results edit sheet, and standardise sidebar headers to blue; activity info and results must re-search on date change like hotel info (picker position, one request, mobile calendar); remove the empty gap under the hotel info gallery on mobile.
30. 18:55 to 20:46: ability to change dates or pax on the activity checkout like hotels (inline edit and re-price); fix payment-step alignment and show a processing modal background; save activity sessions in Mongo like hotels, expire after 20 minutes and show a refresh-prices modal on the activity page and checkout (then restore the checkout on reload); fix price slider markers; restructure and lazy-load hotels, activities, flights and the homepage so each route downloads only its own chunks; "this does not look right" (clipped mobile calendar on the activity checkout); on desktop and mobile, selecting a location must open check-in/check-out pickers, then passengers, then search automatically for hotels and activities.
