What we noticed · 1
high confidence[F-01, source severity Blocker] Every paid tier says the shop is shut
Severityhigh
Effortmedium
Confidencehigh
Conversion · Commercial · Commercial
Clicking any of the three paid plans renders "This service is not available yet." with class checkout-status error. curl to /v1/plans returns 404, identical for all three plans.
What the evidence shows
Live reproduction on the production site plus a curl check of the billing API.
Evidence
“app.js:1534 (startCheckout); index.html:12 (empty photobooth-api-base meta); worker/ (written, 14/14 tests passing, not deployed)”
Likely root cause
Appears to be: startCheckout posts to /v1/billing/checkout, but API_BASE resolves to "" because the photobooth-api-base meta tag is empty, so the request goes same-origin to Vercel, which serves static files only. The Worker exists and is tested but is not deployed.
Based on publicly available evidence; not verified.
Where it happens
100% of prospective paying customers, at the moment of trying to pay
Why it matters
The site advertises three prices and can sell none of them; every purchase attempt ends in an unresolvable error.
Worth doing next
Either deploy the Worker and proxy /v1, or — if that is not imminent — replace the price CTAs with an email-capture waitlist. Advertising a price you cannot take is worse than advertising no price.
high confidence
Directly reproduced live and confirmed by a same-day curl check of the billing endpoint.
What we noticed · 2
high confidence[F-02, source severity Blocker] The Business funnel has no contact route at all
Severityhigh
Effortlow
Confidencehigh
Conversion · Commercial · Commercial
All four "TALK TO US" CTAs resolve to https://mybishbash.app/contact, which returns 404. Site-wide there are zero mailto: and zero tel: links.
What the evidence shows
curl -L against the contact URL from all four CTA locations; a full link inventory of both surfaces.
Evidence
“index.html:11 (business-contact-url meta pointing at a page that has never existed)”
Likely root cause
Appears to be: The contact URL was set in metadata before the page it points to was ever built.
Based on publicly available evidence; not verified.
Where it happens
Every Business prospect, the highest-value tier
Why it matters
The entire Business sales funnel terminates in a 404; not one enquiry can currently reach the business.
Worth doing next
Point at a mailto: today — it costs one line and cannot 404. Build the contact page before the Business surface gets any traffic.
high confidence
Directly confirmed by curl across all four CTA locations; the domain itself is live, isolating the fault to this one path.
What we noticed · 3
high confidence[F-03, source severity Blocker] Customise My Booth silently discards a free user's work
Severityhigh
Effortmedium
Confidencehigh
Conversion · Trust · Core journey
Completing all five steps of the Customise My Booth flow and clicking Save produces no visible message and writes nothing to localStorage.
What the evidence shows
Live reproduction: typed a real event title, saved, and confirmed Object.keys(localStorage) returned [].
Evidence
“app.js:1494 (savePersonalSettings: bare early return with no user-facing consequence); app.js:1690-1691 (openPersonalSettings has no entitlement check at the entrance)”
Likely root cause
Appears to be: savePersonalSettings opens with a bare early return when the user lacks entitlement, while the entry points that open the setup flow carry no entitlement check at all — so free users are invited into a flow whose exit is silently disabled.
Based on publicly available evidence; not verified.
Where it happens
Every free user who engages with the Personal upsell — the most interested segment
Why it matters
The single worst bug in the product also destroys the work of its most engaged, closest-to-paying users, with no explanation.
Worth doing next
Move the gate to the save action itself: let free users configure everything, then convert Save into the paywall moment — they have already done the work and seen their own event name, the strongest possible moment to ask. That turns the worst bug into the best conversion point on the site.
high confidence
Directly reproduced live with a concrete before/after localStorage check.
What we noticed · 4
medium confidence[F-04, source severity High] One Safari-flavoured alert for six different camera failures
Severityhigh
Effortlow
Confidencemedium
VerificationReproduced
Core journey · Accessibility · Core journey
Clicking START PHOTOBOOTH with the camera unavailable fires a native dialog reading "Please allow camera access in Safari and try again." — regardless of the actual cause.
What the evidence shows
Live reproduction of the camera-denied path.
Evidence
“app.js:931 (single catch wraps the whole capture routine without inspecting err.name)”
Likely root cause
Appears to be: A single catch handles NotAllowedError, NotFoundError, NotReadableError, an insecure context, a video.play() rejection and a gallery-write failure identically, with Safari-specific wording regardless of browser.
Based on publicly available evidence; not verified.
Where it happens
Every user whose camera fails to start, concentrated in in-app browsers
Why it matters
The product's real distribution channel is a link pasted into a WhatsApp or Instagram group; those open in in-app browsers that restrict getUserMedia and are not Safari — the most likely real failure mode is a guest being told to fix Safari inside Instagram.
Worth doing next
Branch on err.name, replace the native alert with in-page state, and add an explicit "open in Safari/Chrome" route for in-app browsers.
medium confidence
The failure and its cause are directly evidenced in source; the in-app-browser behaviour itself is explicitly [verify by hand] in the audit, since the audit browser cannot reproduce in-app browser restrictions.
Verification · Reproduced
The behaviour was observed again on re-check.
- How it was checked
- Source review of the single catch handler plus a live reproduction of the camera-denied path in the audit browser.
- What was observed
- The same Safari-specific alert fires for every distinct camera failure cause; in-app-browser behaviour specifically could not be reproduced by the audit tooling.
- Environment
- Audit browser session. The source audit calls for opening the live link inside WhatsApp and Instagram on a real iPhone and a real Android handset before treating the copy fix as verified.
- Last checked
- 9 August 2026
What we noticed · 5
high confidence[F-05, source severity High] A shareable product with nothing to share
Severityhigh
Effortlow
Confidencehigh
Conversion · Discoverability · Discoverability
og:* tag count: 0. twitter:* tag count: 0. No canonical link. Zero JSON-LD blocks, on either surface.
What the evidence shows
Enumerated every <meta> tag on both the Personal and Business surfaces.
Evidence
“index.html:3-17 (hand-authored <head> with no social metadata added)”
Likely root cause
Appears to be: Social metadata was never added when the head was authored.
Based on publicly available evidence; not verified.
Where it happens
Everyone who receives a shared link — the primary acquisition path for a party product
Why it matters
A link pasted into the exact channel this product is distributed through (a group chat) renders as a bare, unbranded URL rather than the rich keepsake preview the product could show.
Worth doing next
Add a full og:/twitter: set with a rendered keepsake as og:image — the product already generates the exact image that should sell it.
high confidence
A complete, direct enumeration of every meta tag on both surfaces.
What we noticed · 6
high confidence[F-06, source severity Medium] /business is metadata-identical to /
Severitymedium
Effortmedium
Confidencehigh
Discoverability · Discoverability
document.title and the meta description on /business are byte-identical to the homepage, and neither surface has a canonical link.
What the evidence shows
Direct inspection of both routes' document head.
Evidence
“vercel.json (both routes serve the same static index.html; the client selects a surface at runtime and nothing updates the head)”
Likely root cause
Appears to be: Both routes serve one static document and pick a surface client-side, with no per-surface head update.
Based on publicly available evidence; not verified.
Where it happens
Search engines and link-preview crawlers indexing either surface
Why it matters
Search results and shared links cannot distinguish the Business surface from the Personal one.
Worth doing next
Set title/description/canonical/og: per surface on route selection.
high confidence
Directly evidenced by inspecting both routes' rendered head content.
What we noticed · 7
high confidence[F-07, source severity Medium] No robots.txt, no sitemap.xml
Severitymedium
Effortlow
Confidencehigh
Discoverability · Discoverability
Both /robots.txt and /sitemap.xml return 404, text/plain, 79 bytes.
What the evidence shows
Direct request to both paths.
Evidence
“No robots.txt or sitemap.xml present on the deployment”
Likely root cause
Appears to be: Neither file was ever added.
Based on publicly available evidence; not verified.
Where it happens
Search engine crawlers
Why it matters
Search engines have no explicit crawl guidance or sitemap for the site.
Worth doing next
Add both at the final domain, not before — they should name mybishbash.app/photobooth.
high confidence
Directly confirmed by request.
What we noticed · 8
high confidence[F-08, source severity Medium] Unbranded, unnavigable 404
Severitymedium
Effortmedium
Confidencehigh
Core journey · Core journey
A mistyped path returns a bare 404, content-type text/plain, 79 bytes — no branding, no link home.
What the evidence shows
Direct request to a nonexistent path.
Evidence
“vercel.json declares three explicit rewrites and no catch-all, so unmatched paths fall through to the platform default”
Likely root cause
Appears to be: No catch-all route or branded error page is configured.
Based on publicly available evidence; not verified.
Where it happens
Any visitor who mistypes or follows a stale link
Why it matters
A lost visitor gets no path back into the product, and this gets more likely once the site moves under a subpath.
Worth doing next
Ship a branded 404 that routes back to the booth.
high confidence
Directly confirmed by request.
What we noticed · 9
high confidence[F-09, source severity High] The domain move strands every existing user
Severityhigh
Efforthigh
Confidencehigh
Architecture · Trust · Architecture
All persistence (settings, gallery, edition counter, access token) is scoped to localStorage/IndexedDB on the current origin. Moving to mybishbash.app/photobooth is an origin change, and browsers scope both storages by origin.
What the evidence shows
Source review of every persisted key and its storage mechanism.
Evidence
“app.js:95-100 (SETTINGS_KEY, ACCESS_KEY, GALLERY_DB, EDITION_KEY, all origin-scoped)”
Likely root cause
Appears to be: The existing migration path is a same-origin key rename with no cross-origin equivalent; one is not possible without an explicit export/import or a server round-trip.
Based on publicly available evidence; not verified.
Where it happens
Every current organiser and returning guest, at the moment of the domain migration
Why it matters
Paid access is recoverable via the existing email restore flow, so money is not lost — but settings, gallery and the edition counter have no recovery path and will simply be gone.
Worth doing next
Ship export/import of settings before the cutover, keep the old origin serving a redirect plus a one-time "move your booth" hand-off, and decide explicitly whether the gallery is worth carrying.
high confidence
Directly evidenced by a complete inventory of every persisted key and its storage scope.
What we noticed · 10
high confidence[F-10, source severity Medium] Legacy grandfathering will silently expire at the new origin
Severitymedium
Effortmedium
Confidencehigh
Architecture · Architecture
legacyProfileAvailable is derived from a localStorage key that will not exist on mybishbash.app.
What the evidence shows
Source review of the grandfathering flag's origin.
Evidence
“app.js:161 (legacyProfileAvailable derived from localStorage.raePhotoBoothLiveSettings)”
Likely root cause
Appears to be: The same origin-scoping issue as F-09, applied to the grandfathering signal specifically.
Based on publicly available evidence; not verified.
Where it happens
Existing legacy free users who currently have Personal unlocked
Why it matters
Whether legacy users keep their unlocked status after the move will be decided by accident rather than on purpose, unless it is made an explicit decision.
Worth doing next
Decide deliberately: either carry grandfathering across in the hand-off, or end it and say so.
high confidence
Directly evidenced; the same origin-scoping mechanism as F-09.
What we noticed · 11
high confidence[F-11, source severity Low] Founding scarcity is asserted, never evidenced
Severitylow
Effortlow
Confidencehigh
Trust · Commercial · Commercial
The page claims "Limited to the first 500 Founding Lifetime customers", but the endpoint that would populate the live remaining count 404s, so the claim never loads real data.
What the evidence shows
Source review of the founding-availability loader and its endpoint.
Evidence
“loadFoundingAvailability() calls /v1/billing/founding, which 404s (same undeployed-Worker root cause as F-01)”
Likely root cause
Appears to be: The undeployed Worker (same as F-01); the static fallback is a reasonable degradation of an otherwise verifiable claim.
Based on publicly available evidence; not verified.
Where it happens
Any visitor who reads the founding-scarcity claim
Why it matters
An unevidenced scarcity claim is weaker than none — especially one that is verifiable by design once the Worker is live.
Worth doing next
Once live, show the real remaining number; until then, remove the unevidenced scarcity line.
high confidence
Directly evidenced; the remaining-count mechanism is explicitly designed to be verifiable once deployed.
What we noticed · 12
high confidence[F-12, source severity High] No upgrade route exists at any point of denial
Severityhigh
Effortmedium
Confidencehigh
Conversion · Commercial · Commercial
The only paths to pricing are the plan-choice button and one post-checkout scroll; nothing in the settings screen, the booth, or the free branding notice links to pricing.
What the evidence shows
Source review of every link and CTA that could plausibly lead to pricing.
Evidence
“app.js:1581, app.js:1689 (the only two paths to pricing in the entire product)”
Likely root cause
Appears to be: Denial is implemented as silent absence rather than as a named offer, at every point a free user hits a locked capability.
Based on publicly available evidence; not verified.
Where it happens
Any free user who hits a locked capability anywhere except the two named entry points
Why it matters
The product misses the moment a user has just discovered a reason to pay, repeatedly.
Worth doing next
Every locked capability should name its plan and link to it.
high confidence
Directly evidenced by a complete inventory of pricing-linking CTAs.
What we noticed · 13
high confidence[F-13, source severity Blocker] Selling to UK consumers with no terms, privacy policy or refund notice
Severityhigh
Effortmedium
Confidencehigh
Trust · Compliance · Compliance
/privacy and /terms both 404, and neither is linked from either footer, while the site advertises £30-£100 purchases and the Business surface describes collecting email addresses and consent records.
What the evidence shows
Direct request to both legal-page paths; full footer link inventory on both surfaces.
Evidence
“No terms, privacy or refund/cancellation notice present anywhere on the site”
Likely root cause
Appears to be: The compliance layer was never built — not a considered trade-off.
Based on publicly available evidence; not verified.
Where it happens
Every prospective UK purchaser, and every Business prospect whose data the site would collect
Why it matters
UK Consumer Contracts Regulations require pre-contract information and cancellation rights for distance sales, and a privacy policy is required once personal data is processed; Stripe's own account review also expects reachable terms and privacy URLs — this can block the payment integration F-01 is waiting on.
Worth doing next
Publish terms, a privacy policy and a cancellation/refund statement, linked from both footers, before the first live transaction. The product has a genuinely strong position to write from: photos never leave the device on Free and Personal.
high confidence
Directly confirmed by request against both legal-page paths and a complete footer link inventory.
What we noticed · 14
high confidence[F-14, source severity Medium] Privacy claims are made in marketing but nowhere binding
Severitymedium
Effortlow
Confidencehigh
Trust · Compliance · Compliance
The landing page states "Photos stay on your device. There is no backend photo upload." — true and well-evidenced in code — but it exists only as marketing copy, with no policy document behind it.
What the evidence shows
Source review confirming the claim is accurate, alongside confirmation no privacy policy exists to formalise it (see F-13).
Evidence
“Landing-page copy asserting local-only photo storage, with no corresponding privacy policy document”
Likely root cause
Appears to be: The compliance layer (F-13) does not exist yet, so a true, well-evidenced technical claim has nowhere binding to live.
Based on publicly available evidence; not verified.
Where it happens
Any visitor evaluating the privacy claim before trusting the product with photos
Why it matters
The product's best trust asset is currently its least durable — a claim rather than a commitment.
Worth doing next
Promote the claim into the privacy policy, where it becomes a commitment rather than marketing copy.
high confidence
The underlying technical claim was directly verified in source; the absence of a binding document is confirmed by F-13.
What we noticed · 15
high confidence[F-15, source severity Medium] A localStorage key unlocks Personal
Severitymedium
Efforthigh
Confidencehigh
Commercial · Commercial
The Personal gate is client-side and OR'd with a legacyProfileAvailable flag read from a localStorage key any visitor can write from the console; confirmed writable on the live origin.
What the evidence shows
Source review of the gate condition plus a live confirmation the key is writable.
Evidence
“app.js:435, app.js:1495, app.js:1503 (gate condition); app.js:161 (legacyProfileAvailable read from a client-writable key)”
Likely root cause
Appears to be: A legitimate grandfathering decision (recognise genuine legacy users) was implemented with a client-writable signal that trusts any browser claiming to be one.
Based on publicly available evidence; not verified.
Where it happens
Any visitor who opens the browser console on the live product
Why it matters
For a £30 consumer product this is a low-value target and not uniquely weak (all Personal enforcement is client-side, see F-16) — it is listed because the grandfathering intent and its mechanism have drifted apart into an undocumented honour system.
Worth doing next
Grandfather by issuing real entitlements to known legacy users, or accept the bypass explicitly as a documented decision.
high confidence
Directly confirmed live: the key was writable and the gate condition read it as designed.
What we noticed · 16
high confidence[F-16, source severity Medium] Entitlement is a browser variable
Severitymedium
Efforthigh
Confidencehigh
Architecture · Commercial · Architecture
Entitlement is module state hydrated from a localStorage value and refreshed against a restore endpoint; branding policy is selected from it at render time.
What the evidence shows
Source review of the entitlement hydration and branding-selection path.
Evidence
“app.js:162 (entitlement as module state); app.js:407 (branding policy selected from it at render time)”
Likely root cause
Appears to be: Inherent to a local-first static app, and largely the right trade for a £30 party tool — the honest framing is that export attribution is a courtesy, not a control.
Based on publicly available evidence; not verified.
Where it happens
Any determined user attempting to bypass branding or entitlement enforcement
Why it matters
The product pays some architectural cost for an enforcement pretence it cannot actually deliver client-side.
Worth doing next
Accept the trade explicitly and stop paying architectural cost for the pretence — or move export branding server-side for Business only, where white-labelling is worth money. Do not build DRM for a £30 party tool.
high confidence
Directly evidenced by source review of the entitlement and branding-selection code paths.
What we noticed · 17
high confidence[F-17, source severity High] 2.23 MB of PNG is 96% of the page
Severityhigh
Effortlow
Confidencehigh
Performance · Mobile layout · Performance
A single demo image is 2,232,546 bytes of a 2,331,527-byte critical path — every other asset combined is under 100 KB. The same image at JPEG q82 is 335,630 bytes, an 85% reduction, and the service worker pre-caches the PNG, so every install pays the cost.
What the evidence shows
Measured uncached transfer sizes of every critical-path asset, plus a re-encoding test of the same source image.
Evidence
“assets/demo-photos.png (2.23 MB); sw.js:3 (pre-cached); marketing.js:10 (crops to 640 of 1024 downloaded rows — 37% of downloaded pixels never displayed)”
Likely root cause
Appears to be: The demo image was never optimised or cropped to the region actually used, and is shipped uncompressed relative to what the page displays.
Based on publicly available evidence; not verified.
Where it happens
Every mobile visitor, disproportionately at venues on poor data — exactly where a party photobooth link gets opened
Why it matters
The single largest asset on the page delivers zero pixels beyond what a correctly sized, correctly encoded image would, at roughly 10x the necessary weight.
Worth doing next
Crop to the 640 rows actually used and encode as WebP with a JPEG fallback; expect roughly 2.23 MB to drop under 200 KB with no visible change.
high confidence
Directly measured transfer sizes and a direct re-encoding test of the same source image.
What we noticed · 18
high confidence[F-18, source severity Low] Three <h1> elements in one document
Severitylow
Effortlow
Confidencehigh
Accessibility · Accessibility
Three separate <h1> elements exist in the DOM at once ("Your party. Your photobooth.", "Want your brand on the booth?", "Your Celebration") — only one visible at a time.
What the evidence shows
Direct DOM inspection.
Evidence
“All surfaces are .screen sections within a single document”
Likely root cause
Appears to be: All surfaces (Personal landing, Business landing, booth) live as sections of one document rather than distinct documents or dynamically managed headings.
Based on publicly available evidence; not verified.
Where it happens
Screen-reader users and search engines parsing document structure
Why it matters
Weakens the document outline for assistive technology and search indexing.
Worth doing next
Resolve alongside per-surface metadata (F-06).
high confidence
Directly confirmed by DOM inspection.
What we noticed · 19
high confidence[F-19, source severity High] /photobooth without a trailing slash breaks every asset
Severityhigh
Efforthigh
Confidencehigh
Architecture · Architecture
Every asset reference is relative. At https://mybishbash.app/photobooth (no trailing slash) the document base is /, so styles.css resolves to /styles.css — wrong. At /photobooth/ it resolves correctly.
What the evidence shows
Source review of every relative asset reference and the service worker's asset list.
Evidence
“index.html:13-16 (relative href/src); app.js:1755 (register("./sw.js")); all 14 service-worker ASSETS entries”
Likely root cause
Appears to be: Relative paths are the right choice for a portable subpath app, but they make the trailing slash load-bearing, and nothing currently enforces it.
Based on publicly available evidence; not verified.
Where it happens
Everyone, from the moment the migration goes live, if the trailing slash is not enforced
Why it matters
The failure mode is a totally unstyled, non-functional page at first launch on the new domain.
Worth doing next
A permanent redirect from /photobooth to /photobooth/ at the parent domain, plus a <base> tag as a belt-and-braces guard, verified before cutover.
high confidence
Directly evidenced by a complete inventory of every relative asset reference.
What we noticed · 20
medium confidence[F-20, source severity Medium] /business needs a rewrite the parent domain does not yet have
Severitymedium
Effortmedium
Confidencemedium
VerificationReproduced
Architecture · Architecture
Under the subpath, the relative link to /business resolves to /photobooth/business, which the parent domain must rewrite to the same document; there is no catch-all, so any other /photobooth/* path yields the bare 404 of F-08.
What the evidence shows
Source review of the current vercel.json rewrites plus a live navigation check.
Evidence
“vercel.json rewrites exactly /, /business, /business/ — no catch-all; index.html:30 (relative href="business")”
Likely root cause
Appears to be: The current rewrite list enumerates exact paths rather than a catch-all pattern.
Based on publicly available evidence; not verified.
Where it happens
Anyone navigating to /business or any other path once the site moves under /photobooth
Why it matters
The Business surface would 404 under the new subpath unless the parent domain's rewrite is updated first.
Worth doing next
Rewrite /photobooth/(.*) to the deployment and let the client route, rather than enumerating paths in two places.
medium confidence
The rewrite gap itself is directly evidenced; one hard navigation to /business timed out during the audit while curl returned 200 and client-side routing worked, so the audit flags this as instrument noise pending a manual recheck under the subpath.
Verification · Reproduced
The behaviour was observed again on re-check.
- How it was checked
- Live navigation attempt to /business (hard navigation) alongside a curl check and a client-side-routing check of the same URL.
- What was observed
- The hard navigation timed out while curl returned 200 and client-side routing worked correctly — recorded as instrument noise, not a confirmed defect.
- Environment
- Audit browser session. Retained only as a check to repeat manually after the subpath migration, not as a standalone finding.
- Last checked
- 9 August 2026
What we noticed · 21
high confidence[F-21, source severity High] Mobile nav detaches from the header and lands on the hero
Severityhigh
Effortlow
Confidencehigh
Mobile layout · Mobile layout
At 375x812, the audience nav renders 71px below the header it belongs to, in a bordered box overlapping the hero kicker text. Confirmed by programmatic rect intersection.
What the evidence shows
Direct viewport measurement at 375x812 with a programmatic rect-intersection check.
Evidence
“styles.css:364 (.audience-nav{grid-row:2;grid-column:1/-1;position:absolute;top:72px} — for an absolutely-positioned grid item, the containing block is the grid area, not the grid container's padding box, so top:72px double-counts the header height)”
Likely root cause
Appears to be: The absolutely-positioned nav is also a grid item, so its containing block is its grid area rather than the container — a CSS positioning-model mismatch, not a design error.
Based on publicly available evidence; not verified.
Where it happens
Every visitor at 768px or narrower — the majority
Why it matters
A floating, overlapping navigation box sits directly on top of the hero copy for most mobile visitors, on the page whose entire job is to convert a first-time visitor.
Worth doing next
Drop grid-row/grid-column from the absolutely-positioned element, or use top:100% — the intended tab-bar-off-the-header design is correct, only the containing block is wrong.
high confidence
Directly measured and confirmed by programmatic rect intersection, with the exact CSS mechanism identified.
What we noticed · 22
high confidence[F-22, source severity High] Personal sells six months of a one-evening event
Severityhigh
Effortlow
Confidencehigh
Commercial · Content clarity · Commercial
Personal pricing is duration-based (£30/6 months, £50/12 months, £100 lifetime), but the customer has one party — nobody has six months of birthdays, and pricing in months forces buyers to reason about a duration rather than the event they already scheduled.
What the evidence shows
Source review of PLAN_METADATA and a reasoned analysis of how a party host actually evaluates the purchase.
Evidence
“product.js:107-138 (PLAN_METADATA: £30/6mo, £50/12mo, £100 lifetime capped at 500)”
Likely root cause
Appears to be: Pricing was structured around subscription duration rather than the single-event use case the product actually serves.
Based on publicly available evidence; not verified.
Where it happens
Any prospective Personal buyer evaluating which tier matches their one event
Why it matters
The £30 tier is dominated on both price and duration by the £50 annual tier and exists mainly to anchor it, while the whole framing invites an unanswerable question ("will I still want this in month five?").
Worth doing next
Sell the event: "One event, £X" matches how the purchase is actually reasoned about, removes the expiry objection, and makes the upsell to Founding Lifetime a straightforward "host more than twice and this pays for itself." Keep Founding Lifetime unchanged.
high confidence
Directly evidenced pricing structure; the audit explicitly notes this is among the cheapest changes in the whole audit since pricing is presentational metadata by design (product.js's own separation of price from capability).
What we noticed · 23
high confidence[F-23, source severity High] The paywall undersells itself
Severityhigh
Effortlow
Confidencehigh
Commercial · Content clarity · Commercial
The Free pricing card promotes removing branding, but launchFreeBooth resets free users to a default event title of "Your Celebration" — so every free magazine cover is mastheaded "YOUR CELEBRATION" rather than the guest's actual party.
What the evidence shows
Source review of the free-booth default state and the cover-copy generation path.
Evidence
“app.js:1501 (launchFreeBooth resets to DEFAULTS); app.js:2 (eventTitle: "Your Celebration")”
Likely root cause
Appears to be: The pricing card leads with the weaker lever (removing branding) while the stronger, already-built lever (the guest's real event name on the keepsake) is never shown.
Based on publicly available evidence; not verified.
Where it happens
Any free user evaluating whether to upgrade to Personal
Why it matters
The real free/paid difference — a keepsake that says the guest's actual party name, not "Your Celebration" — is a far stronger reason to pay, and it is hidden.
Worth doing next
Show it, don't describe it: put the two covers side by side on the pricing card, the same comparison already built for the welcome screen, applied to the difference that actually sells.
high confidence
Directly evidenced by source review of both the default event title and the pricing-card copy.
What we noticed · 24
high confidence[F-24, source severity Medium] Kicker labels fail AA contrast
Severitymedium
Effortlow
Confidencehigh
Accessibility · Accessibility
The product-kicker pink-on-pill combination measures 3.60:1, below the 4.5:1 required for bold text under 18.66px.
What the evidence shows
Direct colour-contrast measurement of the rendered kicker element.
Evidence
“.product-kicker: rgb(217,45,127) bold 10px on its pink pill background”
Likely root cause
Appears to be: The kicker's pink was chosen without checking contrast against its own pill background at the size actually used.
Based on publicly available evidence; not verified.
Where it happens
Any visitor relying on text contrast to read section labels
Why it matters
A section-labelling element used across the marketing surfaces falls short of AA on every appearance.
Worth doing next
Darken the pink or lighten the pill.
high confidence
Directly measured contrast ratio against the WCAG threshold for the actual rendered size and weight.
What we noticed · 25
high confidence[F-25, source severity Medium] 22 sub-44px touch targets on the interactive demo
Severitymedium
Effortmedium
Confidencehigh
Accessibility · Mobile layout · Accessibility
22 mobile controls measure under 44x44 CSS px, including all nine demo frame/filter chips at 29px tall.
What the evidence shows
Direct measurement of every interactive control on the mobile landing surface.
Evidence
“Demo frame/filter chips: White 42x29, Film 35x29, B&W 39x29 — all pass WCAG 2.2 SC 2.5.8's 24x24 minimum, but fall short of Apple's 44pt guidance”
Likely root cause
Appears to be: The demo chips were sized to fit visually rather than against a touch-target guideline.
Based on publicly available evidence; not verified.
Where it happens
Any mobile visitor using the interactive demo, which does the selling on mobile
Why it matters
The chips that demonstrate the product's actual output — the strongest selling moment on mobile — are the hardest controls on the page to hit accurately.
Worth doing next
Raise the chips to 44px.
high confidence
Directly measured every control; the audit is explicit this is a usability finding against Apple's guidance, not a WCAG 2.2 SC 2.5.8 failure (which these controls pass).
What we noticed · 26
high confidence[F-26, source severity Low] No skip link
Severitylow
Effortlow
Confidencehigh
Accessibility · Accessibility
No skip-to-content link exists on any surface; keyboard users must traverse the full navigation on every surface change.
What the evidence shows
Direct keyboard-navigation inspection.
Evidence
“No skip-link element present in the DOM”
Likely root cause
Appears to be: A skip link was never added.
Based on publicly available evidence; not verified.
Where it happens
Keyboard users navigating between surfaces
Why it matters
Adds unnecessary keyboard traversal for a real, if small, share of visitors.
Worth doing next
Add a standard skip-to-content link.
high confidence
Directly confirmed by DOM inspection.