Building AI Analyst discoverability into search results
I added four entry points to the AI Analyst feature across the search interface, each targeting a different user state.
The three states
The /search page now shows exactly one of three AI prompts depending on what the user is looking at. A full CTA card appears on the bare search page before any query is entered. It reads "Ask instead of searching" with the 1.4M documents and 2 free questions copy, plus an "Ask the AI Analyst" button. I placed it between the search box and Suggested Searches.
When a search returns results, a slim one-liner appears above the results list: "Not finding it? Ask the AI Analyst in plain English →". When a search returns zero results, a strong block appears with "No documents matched that search" and a full button. That's the highest-intent moment on the site.
I also added a static orange "AI" badge to the AI Analyst nav link, matching The Wall's LIVE badge but without animation.
Verifying the marketing claims
Before writing the copy, I verified both factual claims independently. I checked that ANON_DAILY_SEARCHES = 2 in auth.py and ran SELECT COUNT(*) FROM documents against the production database. It returned 1,435,616 rows. Both claims were accurate.
I also verified the AI guardrails the user cited as justification instead of accepting the claim on faith. I checked the hardened rules in build_system_prompt() and the persistent presence-is-not-evidence disclaimer in the ask template. I concluded no reservations and built.
CSS and gating
I hoisted the shared CSS out of the Jinja branch. My first version put the .search-ai-cta styles inside the {% if not query %} block, which meant the zero-results block would have rendered unstyled. I restored search.html from backup and applied a single clean patch from baseline so the committed diff reads as one coherent change.
I gated the slim one-liner behind {% if results %}. It was previously above the whole results card, so it would have rendered on zero-results pages too and double-stacked with the new strong block.
I tinted the accent surface instead of reusing var(--card-bg). The CTA sits inside an existing .card, so reusing card-bg gives zero contrast. I used rgba(221,107,32,0.06) with a dark theme override to #1a1208, matching the existing newsletter banner.
Playwright fixes
The Playwright click on .hamburger-btn timed out at 30 seconds. The age-gate overlay was intercepting pointer events. I fixed it by pre-setting epsteinscan_age_verified=true on the browser context. That also cleared the gate's body.style.overflow='hidden', which made the horizontal-overflow assertions meaningful for the first time.
One test failed because my assertion threshold was under 45px and the measured height was 46. I diagnosed it with Range.getClientRects(). Every drawer link is 46px whether badged or not, and AI Analyst renders on exactly one line, identical to The Wall's LIVE-badge link. I corrected the assertion to compare against the existing LIVE link instead of a magic number.
Production verification
All curl commands to the public domain returned HTTP 000 from both the server and my local machine. I wasted two 40-second timeout rounds assuming Cloudflare was challenging the curl user agent. The actual cause was that the domain is epsteinscan.org, not .com. I had the wrong TLD. I confirmed via grep server_name /etc/nginx/sites-enabled/.
I ran the full headless Chrome verification suite at six viewport widths in both dark and light themes on dev and against the production process. Zero failures both runs.