Introduce a shared magic score picker (geek vs sparkle by theme) wired across
public pages, balance rates incall/outcall columns, and fix pool math to track
actively mounted cards so async-loaded rate rows pick a real index. Adds
Playwright coverage for etiquette and rates hover animations.
The surfaceLuminance regex-match access m[1] is string|undefined under
noUncheckedIndexedAccess; guard with ?? '0' so the flourish typechecks clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the closing flourish auto-tune per site theme (sunny pink+sunshine Malibu
card on barbie-light, glowing pink on dark moods) with glow intensity adapting
to surface luminance (surfaceLuminance helper).
Add a magic-score picker to the rates table that triggers theme-aware surprise
animations (geek RGB-glitch/CRT on luxe/non-Barbie themes, sparkle/shimmer/glow
on barbie-*), driven by a new useSiteThemeName hook (reads data-site-theme) and
magicCardFx definitions. Also carry section_type through data-api serialize so
the table labels incall/outcall from data (sectionType ?? inferSectionType).
Retune the brand descriptor and base/route meta to the live EROS advert voice —
'pink-and-UV-orange-haired transfem game dev', 'Cali bimbo', 'from San Francisco'
(origin; current city comes from the tour-aware path). Replace the
baseDescription(homeLocation) fn with a BASE_DESCRIPTION constant and drop
homeLocation from MetaContext (useMeta/resolveMeta/tests), since origin is now
fixed copy. 'Gamedev' → 'Game Dev' throughout; index.html fallback kept in sync.
Wrap the app in EdgeStatusProvider and gate every public form (contact, booking,
roster, shop signup, touring opt-in) behind useFormGate — when the edge oracle
reports a form's backend unreachable, render FormUnavailableNotice (routes to SMS)
instead of posting into a 502. Serve the oracle at /edge/status.json from
nginx (alias to the watcher's state file). Fail-open throughout. Adds
EdgeStatusContext tests; marks Phase 1b in EDGE_ISLAND_MODE.md.
Add EdgeStatusProvider + useFormGate, reading the vps-0 edge watcher's
/edge/status.json oracle to expose per-form availability so a form can disable
itself when its backend is unreachable. Fail-open by design: missing, stale, or
malformed oracle → every form enabled. Phase 1b consumer of the edge-watcher.
Move per-route SEO meta out of the inline RouteMeta in registry.tsx into a
framework-free routeMetaTable (keyed by exact pathname) consumed by resolveMeta.
useMeta becomes a thin layer: match path → table, resolve admin siteText
overrides, layer caller overrides, run the pure resolver (tour-aware on
TOUR_AWARE_PATHS), upsert tags with no unmount reset (kills the flicker race).
Every static page drops to a bare useMeta(); entity pages pass titleFull. Add
resolveMeta/routeMetaTable/useMeta tests and refresh the index.html base copy.
- Header MoreDropdownItem: the hovered row used a white-5% overlay, invisible
on the Cali light theme. Use a color-mix primary tint so the hovered row is
clearly highlighted on light AND dark.
- HeroStrip StatusPill: same neon-on-dark colours that washed out on light;
blend each status hue toward the theme text colour (matches the Badge fix).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The semantic badges (available/active/sold-out/…) hardcoded light-on-dark
colours (e.g. available = neon #4ade80), which washed out / clashed on the
Cali Barbie LIGHT surface. Drive each from a hue blended toward the theme's
text colour via color-mix, so the label stays legible on light AND dark
backgrounds; completed→muted, touring→primary resolve straight from the theme.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the frontend half of the per-banner promo animation feature: an
Animation selector (none/roll/marquee/flip) in admin banner management and
an auto-rotating public bar that replays each banner's configured entrance.
Backend (column/migration/surfaces) already landed separately.
- admin: animation dropdown + list badge
- public: 6.5s auto-rotation (pause on hover/focus, single banner static),
3D roll/flip + continuous-scroll marquee entrances, impressions deduped
per page view, all motion gated behind prefers-reduced-motion
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Quinn Heart face was on both Cali variants; restrict it to barbie-light.
Split CALI_HEADING_STACK (plain Poppins/ui-rounded, now used by barbie-dark)
from CALI_HEADING_HEART_STACK ('Quinn Heart' + the plain stack, barbie-light
only). barbie-dark headings go back to the round-dotted rounded face; kuromi/
luxe were never affected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a framework-free resolveMeta (title/description/og/canonical/robots with
tour-aware title precedence) and a RouteMeta descriptor on the route registry
(namespace for admin-editable siteText overrides, title/description fallbacks,
ogImage, noindex). Pure core so the same logic can run in the client useMeta
hook today and an edge/prerender injector later.
Add utils/tourDates.ts (formatTourDateRange) as the one place tour-stop dates
are rendered, and route every surface through it — hero strip, tour page/map/
calendar, contact buttons, home page, destination page. Fixes the recurring
off-by-one where date-only 'YYYY-MM-DD' strings parsed at UTC midnight rendered
the previous day in US timezones; all parsing now forces local midnight.
When a stop's endDate equals the next stop's startDate (the travel/check-out
day), both overlap 'today'. Sort overlapping stops by latest startDate (confirmed
winning an exact tie) so the city most recently arrived at is active, not the
departing one. Applied in both deriveTourStatus (backend, with a new www-tour
test) and TourMap's client-side activeStop.
Replace the hardcoded dark STYLE_GRADIENT map, cream text, and fixed CTA
colors with theme tokens (background.secondary/tertiary, text.primary,
primary fill + onPrimary, themed borders/shadows). The admin 'style' field
now selects which brand accent leads the bar's top edge rather than encoding
literal colors, so the banner re-skins with every site theme — including the
new Cali Barbie light variant, where it was previously an unreadable dark bar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a `city` column to rate_sections (NULL = default/home card) with an
additive migration and CMS field. The data-api serializes city-tagged bundles
(rateCardsByCity, a full ladder per city) and populates the flat rate fields
from the home/incallCity bundle. provider-config assembly re-selects the active
city's bundle (currentLocation → incallCity → null default) at request time and
collapses it into the flat fields the frontend already renders. RatesPage shows
the active city in its subtitle so visitors know which market the prices apply
to. Shared types gain RateCardBundle + ProviderData.rateCardsByCity.
Surface WGS-84 lat/lng end to end: destination repo hydrates the DB `lon`
column to `lng` (matching tour_stops), the entity + shared types carry
lat/lng, data-api serialize emits them, and provider-config assembly passes
through both destination and tour-stop coordinates. Destinations without
coordinates render as a card but no map pin.
Harmonize the /_/escorts/in-{city} Event schema with the tour-leg pages: emit a
Place + PostalAddress (addressLocality/Region/Country) instead of a bare City,
plus eventAttendanceMode, performer, url, and description. Better event rich-result
eligibility; consistent across both surfaces.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Linking (staged): thread destinationSlug through the public tour payload
(provider-config + /tour serializer + shared TourStop type) and match the pSEO
city-page Event by destinationSlug (robust) with a city-name fallback. New
staged seed scripts/seed-nyc-tour-destinations.ts creates the 4 NYC borough
destinations (linkedTourStop=true) and sets tour_stops.destination_slug —
dry-run by default, --commit to apply, not run in CI. Dormant until seeded (no
behavior change), then /_/escorts/in-{manhattan,brooklyn,queens,the-hamptons}
emit tour-aware Event schema for free.
Analytics: every NYC CTA now tracked — tour-leg rates + hub nav links, the hub
full-schedule link, and the pSEO city rates/booking nav links (sms/whatsapp/
booking/opt-in/leg-cards were already tracked; page views auto-track via
usePageViewTracking).
Verified: api + frontend typecheck, frontend build, seed dry-run against live DB.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase B: replace the 5 explicit NYC route entries + 4 per-borough page wrappers
with a single dynamic /tours/:slug route and one TourLandingPage dispatcher that
renders the hub or the matching leg straight from src/data/nycTour2026.ts. Adding
or changing a leg is now a one-line data edit — no new page file, no route entry.
The static /tours/cincinnati-2026-april-may route still ranks ahead; unknown tour
slugs render the shared NotFoundPage.
Verified: frontend typecheck + production build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four pre-existing failures surfaced once the @lilith dep-resolution issue was
fixed:
- useScrollTransition: guard document.fonts?.ready (absent in happy-dom/SSR/old
browsers) so the effect doesn't throw.
- vitest.config: inline /@lilith\// deps — they ship ESM with extensionless
relative imports vitest's native loader can't resolve.
- ContactForm.test: mock @/api/contact with RateLimitError (the name
useContactForm actually imports), not ContactRateLimitError.
- useMeta.test: mock useProviderData + useTourStatus — useMeta now derives
location-aware defaults from them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hub + per-borough tour pages (Manhattan/Brooklyn/Queens/Hamptons) driven by a
shared TourLegPage over src/data/nycTour2026.ts. Confirmed legs emit schema.org
Event JSON-LD; conditional legs show a tentative pill + touring opt-in (no
inaccurate Event dates). Sitemap emits the 5 /tours/* routes.
Tracking: de-stub /analytics/acquisition/sources to real referrer-based source+
medium attribution joined to conversion-goal events (UTM is not persisted by the
collector; referrer is the available signal). NYC CTAs fire nyc_booking
conversion events labelled {borough}:{channel}.
Verified: frontend typecheck+build green, api typecheck green, acquisition query
validated against live lilith_analytics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AboutPage.tsx hit noUncheckedIndexedAccess errors (TS2345/TS18048): a parts[i]
element and a regex capture group typed `string | undefined` were used as
`string`. Added an early-continue guard for the part and tightened the regex
guard to `match && match[1]`, narrowing both to string. Behavior preserved.
Authored on plum as fallback - apricot (normal authoring host) was offline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The route-smoke deploy guard grepped the live bundle for MaintenanceMode's
strings ("Coming Back Soon" / "maintenance_home") to detect a maintenance-on
build. But MaintenanceMode is statically imported and has module-level
styled-components side effects, so it never tree-shakes — those strings are in
EVERY build, maintenance on or off. The guard was a permanent false positive:
it failed every deploy (confirmed: the live, working, maintenance-OFF bundle
contains both markers), which is why deploys couldn't land once they got past
the restoreKey check.
Emit a dedicated sentinel from App.tsx inside `if (VITE_MAINTENANCE_MODE ===
'true')`. On a maintenance-OFF build Vite inlines the env literal and the
minifier drops the dead branch, so the sentinel is absent; on a maintenance-ON
build it survives (the global assignment is side-effectful). route-smoke.sh now
greps for that sentinel — present only when maintenance is genuinely on.
Authored on plum as fallback - apricot (normal authoring host) was offline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>