Media inventory
Every screenshot, video and diagram the documentation calls for, ordered by priority rather than by filename.
0 placeholders: 0 screenshots, 0 videos. Rows 108 and 109 — the API key screen, which shipped on 2026-09-17 — were captured and published the same day; the paragraph below is the record of the work before them.
Every slot that can be filled from this harness is filled — eighty-five captures on the CDN, and twenty-one rows were removed from the table as not capturable from this harness — each with its reason, in the section before the batching plan. The three provider Revenue cards share the Stripe-filled capture. The Revenue widget (row 91) is captured now that the action accepts the type — the panel with Revenue chosen and its publishing box ticked — and the Widgets snippet was re-shot with the widget's public slug replaced by a stand-in, which the first take had printed. The billing page past the band, the invite form and the seat notice, the refused Add-a-site form, and the managed-proxy video. The alerts list in its three states, the X card with a handle and terms, and the sharing card switched off. The Goals card with a rule beside the code-sent goals, and the Funnel tab with a saved funnel — both made for the take and deleted after. The connect flows for Stripe and Search Console are videos of the two halves that are ours, and the Paddle, LemonSqueezy and Polar key screens are our Revenue card with a key typed — the providers' own dialogs are theirs, and the permissions the reader needs are printed on our card. Since the count below was written: the Sharing tab and the board as a viewer sees it (twice), the X, Threads and Search Console cards before connecting, the Revenue card with Paddle, LemonSqueezy and Polar chosen, and the Keyword tab with its Show all window. gsc-impression-breakdowns joined the not-capturable list — nothing renders those facets. The latest batch: the AI card on both tabs, the chart with crawlers and with revenue (which also serves the Google Analytics boundary), the Stripe card before and after connecting with its re-sync line and currency control, the Google Analytics card, the delete-site confirmation, the billing meters, the two troubleshooting Network grids, the Widgets tab off and its snippet, and the pricing page at 250k. Four rows in this group were reclassified from video to screenshot where a still shows the whole thing, and three are now marked as not capturable as written (team-invite-dialog needs an owner session, billing-payment-method and revenue-by-channel describe screens that do not exist). Earlier batches: the settings screens (exclusions, revenue source and its goal picker), the Add-a-goal dialog, the alert editor and the Now video: the install snippet and its platform chips, the Add-a-site form, the first-event flip and the connected card it ends on, the DevTools Network check that three pages share, the populated dashboard with its referrer drill-down, the identity control, the two console messages, the Goals card with its filter and parameters panel, and the dashboard's cards — tiles, Geography and Technology, Sources, channel quality, the heatmap, the AI-vs-human split (which the AI-visibility page shares), the map in both themes, the range picker and the Filters popover. The table keeps every row; the count above is what is still OUTSTANDING, so it falls as captures land.
Row 14, install-snippet-ai-builders, cannot be captured. It asks for an "AI builders" tab that does not exist — src/features/install/platforms.ts has eleven chips and none of them is it, and there is no paste-into-chat prompt anywhere in that feature. The row is describing something that was never built, so it is a PRODUCT decision, not a capture one.
It was 105 and 1 until the rule below was written down. The split had never been decided — each row got whatever its author typed — so eleven rows that describe a sequence were sitting as stills, including every import-progress line and both hover-to-reveal timeline markers.
Nine of these captures are the same shot filed twice. One DevTools panel covers rows for three pages; one Revenue card covers three providers; the install snippet and the connected state are each listed on two pages. They are marked SHARED in the notes column — shoot once, reference from both places. Doing so takes the real capture count to about 97 with no page losing an image.
How a placeholder lives in the docs
Each one is a MediaSlot component in the .mdx source. The body text is the
capture brief — it renders as the placeholder's caption until a real file
replaces it, so it is written for whoever takes the shot:
<MediaSlot kind="screenshot" id="install-snippet-copy">
The Install tab of site settings with the snippet rendered, a site key visible, Copy button.
Light theme.
</MediaSlot>kind is screenshot or video. id is unique across the whole docs set and
is the key this table joins on.
Which one — the rule
A flow is a video. A result is a screenshot.
The test is whether TIME is part of what the reader needs to understand. If the thing only makes sense as a sequence — a form being filled, a state flipping, a connection completing, a value appearing where there was none — a still frame shows the end and hides the part that was being explained. Adding a site, setting up the proxy, writing an alert, pasting the install snippet and watching it connect: all flows.
If the reader needs to recognise a screen rather than follow a process — a populated card, a settings tab, a chart, an error message — a screenshot is better than a video, because it can be scanned, it loads instantly, and it does not ask for 20 seconds of attention to convey one frame.
This rule was written after the fact: the set had 105 screenshots and one video, and the split had never been decided — it was whatever each row's author happened to type. Rows are being reclassified against it.
A video is 15–30 seconds, silent, and shows one flow end to end. Anything longer is two videos.
A hover is not a flow. The world map tooltip, the heatmap cell, the Search Console row and the goal parameter panel all reveal something on hover, and all four stay screenshots: what the reader needs is what the tooltip SAYS, and everyone already knows how to point at a thing. Capture them with the tooltip open. The distinction is between an interaction that teaches (a form, a connection, an import) and one that merely displays.
A video's own frames are not extra screenshots. see-your-first-visit
carried a video of Waiting → Connected and then asked for stills of Waiting and
of Connected — the first and last frame of the clip directly above them. Both
were removed. If a video covers the state, the state does not also need a
photograph.
To find one in the source:
grep -rn 'id="install-snippet-copy"' docs/To find everything still outstanding:
grep -rn "<MediaSlot" docs/ --exclude=MEDIA-INVENTORY.mdThe counts must agree — a slot missing from this table never gets captured:
grep -rho "<MediaSlot" docs/ --exclude=MEDIA-INVENTORY.md | wc -l
grep -c "^| [0-9]" docs/MEDIA-INVENTORY.md--exclude is not optional: this file quotes MediaSlot twice in its own
explanation, and counting those made the totals disagree by two with nothing
actually wrong.
To check this table against reality — the two counts must match:
# slots in the pages
grep -rho "<MediaSlot" docs/ --include="*.mdx" | wc -l
# rows in this table
grep -c "^| [0-9]" docs/MEDIA-INVENTORY.mdBoth greps need --include="*.mdx", or they also match the example above and
this file's own instructions, and report two more slots than exist.
An earlier convention used an HTML comment, and this page went on describing it after the docs had moved to
MediaSlot. The greps it gave returned nothing, which reads as "no work outstanding" rather than as a broken instruction. If the convention changes again, this section changes in the same commit.
Adding a page — the slot comes with it
A new docs page that describes anything a customer can see gets a MediaSlot
in the same change. Not a follow-up: a page describing a screen with no
picture of it is a page the reader has to translate into their own window.
This applies to any page that quotes or walks through:
the analytics dashboard — a card, a chart, a breakdown, a filter, an empty state
site settings — any tab, form or destructive confirmation
the install flow, billing, team, sharing or an integration screen
Then add the row to this table in the same commit, so the capture list stays the complete answer to "what still needs shooting". A slot that exists in a page but not here is invisible to whoever sits down to take the screenshots.
Skip the slot only when the page is purely conceptual — a policy, a definition, an API request body with no screen behind it.
Capture conventions
Apply to everything unless a row says otherwise.
| Viewport | 1440 × 900 for full-page app screenshots. Crop tightly for single cards, dialogs and DevTools panels. |
| Theme | Light, unless the row says dark. Two rows deliberately want dark, to check contrast. |
| Data | Realistic and non-round. 12,483 not 10,000. Vary statuses, dates and magnitudes so a table tells a story. Never Test 1 / Test 2, never Lorem ipsum. |
| Names | example.com for the tracked domain. Real-looking referrers (news.ycombinator.com, reddit.com) are fine. No invented person names in testimonial style. |
| Site key | A plausible fake: ts_a1b2c3d4e5f6. Never a real one. demos/lib/redact.mjs rewrites the real one in the DOM before the shutter, so the capture is of a working screen rather than a censored one. |
| API key | Always visibly fake. Never a real ts_live_ value, even revoked. |
| Chrome | Hide browser bookmarks bars and unrelated extensions. Nothing personal in a tab title. |
| Redaction | No real email addresses, no real customer domains, no real invoice figures. |
| Format | PNG at 2× device pixel ratio. Video as MP4, H.264, 1920 × 1080 — with the brand intro, outro and narration, and with player controls (never loop, which hides them). This row said "no audio" and "1440 × 900"; both were overruled — a clip that can be lifted out of the page and posted anywhere carries its own identity, and a reader who wants silence has a mute button. node demos/encode.mjs <scene> produces it. |
Priority 1 — blocking getting-started and install
These are the pages a new customer reads in their first five minutes. Nothing else matters until these exist.
1a. The first-run flow
| # | id | Type | Appears in | Must show | Capture notes |
| 1 | install-snippet-copy | screenshot | getting-started/install-the-script.mdx | The Install tab of site settings: the snippet rendered, a site key visible, the Copy button | /dashboard/sites/<id>/settings?tab=install — Install is the default tab. The platform selector is a row of chips, not a tab strip, and the first is labelled HTML (there is no chip called "Plain HTML"). Crop to the "1 · Add the tag" section. 1440px, light. The single most reused image — get this one right first. SHARED — the same capture as install-snippet-plain-html. Re-shot 2026-09-15 after the "Self-hosted proxy" chip was removed and a line about the managed proxy was added under the tag. |
| 2 | first-event-connect | video | getting-started/see-your-first-visit.mdx | The setup's tag step, "I have added the tag", the crawler question skipped, the wait — then the flip to Recording visits as a real pageview lands, and the return to the sites list on its own | Re-recorded 2026-09-15 on the site gs-add-site-form made. The pageview is real: a page served from memory at the site's own domain carrying its real tag; the flip is the product's own poll. The final navigation to the sites list is sealed (sealNavigations, which now rewrites every hostname a page prints — measured: the list's cards named two real customers before it did). Site deleted after. 44 s, narrated. |
| 3 | gs-add-site-form | video | getting-started/add-your-first-site.mdx | The whole setup in one take: the form under the five-stop stepper, Add site, the domain step with the managed-proxy card (hostname added, the CNAME record, Check DNS answering that the record is not there yet, Continue for now), the tag step with Next.js chosen, the AI crawlers step opened to the package and the middleware, the wait — a real crawler report, then a real pageview — both marks green, All set, and the return to the sites list | Everything in it is real: the site is created (app.example.com, "Example app"), the hostname is saved, the crawler report is one POST to /api/crawl with a GPTBot user agent, the pageview is a page served from memory at the site's own domain carrying its real tag, and the wizard's own poll flips both marks. The report goes BEFORE the pageview: in this harness the strip's poll does not fire again once the tag's first event is in (probes outside it keep polling), so the report has to be on record when that poll runs. Site keys are sealed by shape on the wire; the sites list is sealed for its hostnames. Site deleted through the Danger zone after, table read back at four. 148 s, narrated. |
| 4 | verify-network-tab | screenshot | getting-started/see-your-first-visit.mdx | DevTools Network grid: the document (200), t.js (200) and events (202, type ping), status column readable | Dark DevTools, cropped to the grid. SHARED — one capture serves verify-network-202 and troubleshoot-network-check. No Response panel: the tag sends with sendBeacon, and no browser gives DevTools a beacon's response body — the tab is blank and the headers pane says "provisional". The row asked for {"ok":true} on screen; that cannot be shown, and the docs no longer promise it. Taken with node demos/devtools-network.mjs, which opens the DevTools frontend as a page and screenshots it at 2×; a three-request page at example.com, because truestat.io's 47 rows cannot be filtered down to two. |
| 5 | dashboard-full-populated | screenshot | getting-started/what-to-look-at-first.mdx | The dashboard scrolled to top: eight tiles (not four — the row asked for a layout that no longer exists), the Visitors chart with the AI crawler series, and the top of the Sources / Pages row | 1440 × 1100 viewport — at 900 the chart ends on the fold. Light. Real numbers from the busiest demo site (about 1,000 visitors / 1,600 views / 20,000 crawler views a month); the "12,483 views" the row wanted would be a dashboard of a site that does not exist. Note markers on the chart draw their author's photograph — hidden with hide: ["svg g:has(> image)"]. The site's own domain becomes example.com; referrers and pages are left alone, they are not the customer's data. |
| 6 | dashboard-drilldown-filter | video | getting-started/what-to-look-at-first.mdx | Referrer tab on the Sources card, the funnel that appears on hover, the filter applied (tiles and chart change, badge 1 on Filters), then the Filters popover listing "Referrer · Google" | Light. A row is not clicked — clicking pins its tooltip; the filter is the funnel at the row's end, and the page was corrected to say so. The filtered reload is warmed once in a second tab before the take: cold, the thirteen filtered queries took ten seconds of skeletons. Same site and same redaction as row 5. |
| 7 | gs-site-quota-refused | screenshot | getting-started/add-your-first-site.mdx | The Add a site form with a domain typed and, under it, the refusal panel: Starter covers 1 site, Pro allows 30 and starts at $9 a month, Move to Pro | Same Starter span as the seat row. The submit is real and the database refuses it (site_quota_exceeded), so nothing is written — the sites count was read back at four. The prices in the panel come from plans.ts. Light. |
1b. The install-page snippet tabs
Six shots of the same screen with a different platform tab selected. Capture them in one sitting — same account, same site, same key, same crop — so they read as one set.
| # | id | Type | Appears in | Must show |
| 8 | install-snippet-plain-html | screenshot | install/plain-html.mdx | The HTML chip selected, snippet with a site key, Copy button SHARED — the same capture as install-snippet-copy. |
| 9 | install-snippet-nextjs | screenshot | install/nextjs.mdx | "Next.js" tab, the App Router variant Re-shot 2026-09-15 after the "Self-hosted proxy" chip was removed and a line about the managed proxy was added under the tag. |
| 10 | install-snippet-wordpress | screenshot | install/wordpress.mdx | "WordPress" tab Re-shot 2026-09-15 after the "Self-hosted proxy" chip was removed and a line about the managed proxy was added under the tag. |
| 11 | install-snippet-webflow | screenshot | install/webflow.mdx | The Webflow · Framer · Squarespace chip — the app has ONE chip for all three, because the instruction is identical. SHARED — the same capture as install-snippet-framer. Re-shot 2026-09-15 after the "Self-hosted proxy" chip was removed and a line about the managed proxy was added under the tag. |
| 12 | install-snippet-framer | screenshot | install/framer.mdx | The same Webflow · Framer · Squarespace chip. SHARED — the same capture as install-snippet-webflow. |
| 13 | install-snippet-gtm | screenshot | install/google-tag-manager.mdx | "Google Tag Manager" tab — the DOM-construction variant, visibly different from the plain snippet Re-shot 2026-09-15 after the "Self-hosted proxy" chip was removed and a line about the managed proxy was added under the tag. |
All: 1440px, light.
1c. Verification
| # | id | Type | Appears in | Must show | Capture notes |
| 15 | install-connected-with-timestamp | screenshot | install/verifying-it-works.mdx | The Install tab's status card in "Recording visits": green, first-event time, "Open dashboard" button | Cropped to the card. Not #4 (that is DevTools); it is the card first-event-connect ends on, taken from the same example.com site once connected. |
| 16 | verify-network-202 | screenshot | install/verifying-it-works.mdx | The events row at 202 in the Network grid | SHARED — the same capture as verify-network-tab. It asked for the Response panel showing {"ok":true}; see row 4 for why that panel is blank for a beacon in every browser. |
Priority 2 — third-party platform UIs
Screens inside someone else's product. Each needs a real account on that platform, which makes them the slowest to produce — start acquiring accounts early even though they rank second.
Keep each platform's own theme; do not force light on a product whose UI is dark.
| # | id | Type | Appears in | Must show | Capture notes |
Priority 3 — the dashboard
One populated test site can produce almost all of these in a single session. Seed it with about a month of varied traffic — several countries, several referrers, a mix of browsers, and a decent spread of crawler families — then capture in one pass so the numbers agree across every shot.
| # | id | Type | Appears in | Must show | Capture notes |
| 25 | dashboard-four-tiles | screenshot | dashboard/metrics.mdx | The eight tiles, two rows of four: Visitors, Revenue, Revenue/visitor, Conversion rate; Views, AI crawlers, Bounce rate, Session time | The row was written for a four-tile layout with a "Verified crawlers" tile that no longer exists; the id is kept because the page links on it. Real numbers from the busiest demo site. Cropped to the union of the first and last tile. Light. |
| 26 | dashboard-breakdown-cards | screenshot | dashboard/breakdowns.mdx | Geography (Country tab) and Technology (Browser tab) side by side | Union of the two <section> boxes at 1440. Light. |
| 27 | dashboard-filters-popover | screenshot | dashboard/breakdowns.mdx | The Filters button with badge 2 and its popover: Country · India, Browser · Chrome, each with ×, and Clear all | India and Chrome because they are real rows on the demo site; Germany and Firefox were the row's example. Union of button and popover. Light. |
| 28 | dashboard-range-picker-open | screenshot | dashboard/date-ranges.mdx | The range picker open: the preset list, Last 30 days selected, Custom at the foot | There is no calendar beside the list — the row asked for one; the calendar opens after Custom is chosen. The page's "Custom ranges" step was corrected to say so. Light. |
| 29 | dashboard-channels-card | screenshot | dashboard/channels.mdx | The Sources card, Channel tab: the ring with Organic Search 57 %, Direct 39 %, and Referral, Social, AI, Paid Search, Campaign, Paid Social | The card is "Sources", the tab "Channel" — not "Traffic sources → Channels". The AI row is present. Light. |
| 30 | dashboard-world-map | screenshot | dashboard/world-map.mdx | The Map tab, the United States hovered: tooltip with flag, name and 190 visitors | The hovered country is the most saturated path, found by reading fills back; it is hovered at a point inside its fill (isPointInFill), because its box centre is Canada. Light. |
| 31 | dashboard-world-map-dark | screenshot | dashboard/world-map.mdx | The same map in the dark theme, same country hovered | Dark. Deliberate — checks the country fill scale reads against a dark ground. Same pick as #30; luminance would have chosen a one-visitor country, because pale is bright. |
| 32 | dashboard-live-now | video | dashboard/live-view.mdx | The range picker opened, Now chosen, the chart in one-minute buckets, the Live badge and the online count | Light, narrated. There is no "updated Xs ago" in this mode — the code refuses it on purpose (a chart moving every thirty seconds under a stamp about the last summary fetch); the header shows "Live". The page was corrected. The busiest demo site had two visitors in the window; that is what thirty minutes of a small site looks like. |
| 33 | dashboard-ai-human-split | screenshot | dashboard/metrics.mdx | The AI-vs-human split bar: Bytespider 48 %, huawei 16 %, other bots 14 %, Human 7.9 %, then thirteen named families | Cropped to the card. SHARED — the same capture as ai-human-split-bar. Real month: humans are the fourth segment. Light. |
| 34 | site-settings-exclusions | screenshot | dashboard/excluding-your-own-visits.mdx | The "What counts" section, all FOUR fields: paths /admin/* /preview/*, hostnames (two real ones rewritten plus staging.example.com), Germany and France, and the IP field with "You are browsing from 203.0.113.7" | Typed, not saved — Save is never pressed. The IP field's Add saves immediately, so no address is added; the line under it is the reader's own address, rewritten to RFC 5737 space by redact.mjs (it arrives after load, so the scene calls d.redact() once it is on screen). Light, 1440 × 1400. |
Priority 3b — integration screens
Both integrations connect to a third-party account, so each needs the before and after of the same card. Frame the pair identically or the reader cannot see what changed.
| # | id | Type | Appears in | Must show | Capture notes |
Priority 4 — the AI panel
The product's differentiator. Same seeded site as Priority 3, with a realistic crawler mix.
| # | id | Type | Appears in | Must show | Capture notes |
| 35 | ai-panel-full | screenshot | ai-visibility/what-this-panel-shows.mdx | The AI & LLMs card, Crawlers tab: the four category tiles, the crawler chart, the verification line, operators ranked (OpenAI 260, Other 100, Anthropic 31, DuckDuckGo 15, Google 12) | Real month. Cropped to the last row — the card is stretched to its neighbour's height and the dock sits in the empty part. Light. |
| 36 | ai-pages-tab | screenshot | ai-visibility/ai-traffic-by-page.mdx | The same card on AI pages: paths ranked by crawler views, / first, robots.txt second | Same crop rule as #35. Light. |
| 37 | chart-humans-vs-crawlers | screenshot | ai-visibility/humans-vs-ai.mdx | The Visitors chart with the AI crawlers tile pressed: crawler area to 2,000 a day, human line flat beneath | Verify against the page: it says the crawler series is drawn on its own right-hand scale, and the legend says "(right scale)", but with the tile pressed the crawler area takes the LEFT axis (3K) and the human line rides on it; the legend entry is a label, not a toggle, and nothing else adds the series. Either the chart or the page is wrong — owner's call. Cropped from the heading to the legend. Light. |
| 38 | ai-human-split-bar | screenshot | ai-visibility/humans-vs-ai.mdx | The split stacked bar with named crawler families labelled | SHARED — the same capture as dashboard-ai-human-split (#33); the labels are legible at that crop. Light. |
Priority 5 — troubleshooting and error states
Every one of these is a failure state, so each has to be produced deliberately against a test site. Several cannot be captured by chance.
| # | id | Type | Appears in | Must show | Capture notes |
| 39 | troubleshoot-network-check | screenshot | troubleshooting/no-events.mdx | Network tab on a working install: t.js 200 and events 202 | The reference "this is what right looks like". SHARED — the same capture as verify-network-tab. |
| 40 | csp-console-error | screenshot | script/content-security-policy.mdx | Console showing the CSP violation for connect-src blocking /api/events | Dark DevTools, cropped to the message. A page with script-src allowed and connect-src not, served at example.com — node demos/devtools-console.mjs. Chromium 151 changed the wording to "Connecting to '…' violates …"; the docs quoted the older "Refused to connect …" and were corrected. |
| 41 | adblock-blocked-request | screenshot | troubleshooting/ad-blockers.mdx | Network grid: the page at 200, t.js red and blocked, no events row | node demos/devtools-troubleshooting.mjs. Playwright aborts the request "blocked by client"; DevTools labels it (blocked:devtools) where an extension's reads (blocked:other) — the caption says so. Dark. |
| 42 | duplicate-events-network | screenshot | troubleshooting/duplicate-events.mdx | Two t.js rows and two events beacons from one load | The row said video; a still shows it. The second tag carries ?v=2: Chromium coalesces two identical script URLs into one request and the panel showed one row while the tag ran twice. Dark. |
| 43 | custom-event-console-error | screenshot | script/custom-events.mdx | Console warning naming a rejected goal — Signup refused for the capital letter | Dark DevTools, cropped. truestat("Signup") on the example.com test page — node demos/devtools-console.mjs. It is a warning, not an error: the tag uses console.warn. |
| 45 | identity-mode-toggle | screenshot | script/cookieless-mode.mdx | The "Visitors are identified by" control: Cookie (selected, default), Cookieless, Strict, and the one-line note under the chips | Light, cropped to the control. It lives on the Install tab, not a settings page, and the chips are labelled Cookie / Cookieless / Strict — the page's long names are the table's, not the screen's. The note is the line under the chips. |
| 46 | site-delete-confirmation | screenshot | privacy/deleting-your-data.mdx | The Delete this site section: the warning, the type-the-domain field, the Delete site button | Not a dialog — inline in the Danger zone. On the example.com site, nothing typed. Light. |
Priority 6 — features not yet built
Do not attempt these until the feature ships. Each is listed against the page that will need it, so nothing is forgotten at launch.
Sharing
| # | id | Type | Appears in | Must show | |
| 47 | sharing-create-link | screenshot | sharing/creating-a-share-link.mdx | The Sharing tab on a published board: the link with Copy, the twelve section checkboxes, the live-view note, label / range / expiry, Save changes | Read as it is; nothing changed. The link field is an <input> whose text is its value property — redaction now writes that too, having shown the real slug on the first take. The row said ten checkboxes; there are twelve. Light, 1440 × 1500. |
| 48 | shared-dashboard-view | screenshot | sharing/what-a-viewer-sees.mdx | The board at /share/<slug> with no session: header with Start free trial, site name and domain, range picker on Last 30 days, tiles, chart, first cards | auth: false, waitUntil: "load" — the board polls and never goes network-idle. The board opens on Today; the scene picks Last 30 days. A /share/ heading is now taken as the site's domain by redact.mjs, which the first take showed was needed. Light, viewport. |
#50 is the important one — the page's whole promise is "here is exactly what your viewer sees", so it must be captured in a genuinely signed-out browser rather than approximated.
Team
| # | id | Type | Appears in | Must show | |
| 49 | team-invite-dialog | screenshot | team/inviting-someone.mdx | The invite form: the seat line, an address typed, Role set to Member, the sentence under it, Send invitation | The form is shown to the OWNER only; the demo membership is an admin, so its role was set to owner by SQL for the take and back to admin after, read back. Nothing was sent. Light. |
| 50 | team-seat-limit-refused | screenshot | team/seat-limits.mdx | The Team tab on Starter with every seat taken: "2 of 1 seat used", then the notice — Starter includes 1 seat, every seat is taken, See plans — where the form would be | The subscription was moved to a Starter version (one seat, one site) by SQL for the take and back to Pro after, with the membership as owner for the same span; both read back. There is no rejected dialog: with the seats full the form is not offered at all, and the page now says so. Light. |
API
| # | id | Type | Appears in | Must show | Capture notes |
Billing
| # | id | Type | Appears in | Must show | Capture notes |
| 52 | pricing-slider-two-tiers | screenshot | billing/plans-compared.mdx | The public pricing page: slider on 250k, Monthly / Yearly toggle, Starter $19 and Pro $39 with their limits | No account (auth: false, no redaction — the page is public). The range input is set by value and told with an input event; the crop runs from the slider block to the foot of the cards. Light. |
| 53 | billing-usage-meter | screenshot | billing/what-counts-as-an-event.mdx | The plan card on /dashboard/billing: Pro, four meters (events, AI crawler events, sites, team members) with counts, limits and the grace marks | Real usage: 0 % of events, 8 % of crawler events — not the "around 70 %" the row wanted; that would be a seeded account. Light. |
| 54 | over-quota-banner | screenshot | billing/at-your-limit.mdx | The billing page past the band: the red events meter at 121% and the notice beneath the meters — reports paused, the count carried past the ceiling, the step that covers it | usage_counters.events_count was set to 1,214,802 (and over_ceiling_count to the part past 1.1M) by SQL for the take and restored to its real value after, the org's quota_exhausted flag refreshed and read back false. The row asked for a dashboard banner with add-on and upgrade buttons; the notice is on the billing page — the site screens redirect there while the lock lasts — and offers one step, not an add-on. The page now says so. Light. |
| 56 | goals-card | screenshot | script/custom-events.mdx | The Goals card: six real goals with people counts, three of them reading "1 · N completions" | Cropped to the rows, not the card — the card keeps a fixed height and half of it is empty below six rows. The differing rows are real: one person, seven completions. Light. |
| 57 | goals-filter-applied | screenshot | script/custom-events.mdx | The Country card narrowed to signup visitors, with the Filters popover open from the dock reading "goal · signup" and the badge 1 on its trigger | There is no filter chip at the top — the row asked for one; an active filter is a badge on the Filters button and a row in its popover. Scrolled to the country card, the toolbar is off screen, so the popover is opened from the dock. Light, viewport. |
| 58 | dashboard-activity-heatmap | screenshot | dashboard/heatmap.mdx | The "When people visit" card over 30 days, the peak cell (Wed 16:00) hovered: 19 visitors, 35 page views, share of period | Thirty days, so the pattern is real. The card is When people visit; "Activity heatmap" is the inner panel's own title. Cropped to the card. Light. |
| 59 | dashboard-channel-quality | screenshot | dashboard/channel-quality.mdx | The "Traffic quality by channel" table: eight channels on bounce rate, pages / session, median session time and return rate, the best of each column in bold | The contrast the row wanted is there unstaged: Referral, 35 sessions, reads 3.71 pages and 58 m against Organic Search's 637 sessions at 1.52 and 2 m 45 s. Cropped to the card. Light. |
| 60 | dashboard-funnel | screenshot | dashboard/goals.mdx | The Funnel tab with a saved funnel: name and step count above the ribbon, three steps with their people counts, the drop-off between each, Edit and New funnel in the header | Defined through the editor for the take — any page, tool_opened, signup — and the funnels row deleted afterwards, the table read back empty. The brief's Pricing → Signup → Paid was tried first: nobody went from the pricing page to a signup in the period and a funnel of zeros shows no drop-off. The cursor is moved off the ribbon before the shot; a hovered step keeps its tooltip open. Light. |
Rows removed from the table — not capturable from this harness
Their slots are gone from the pages too, so the pages no longer promise a picture they will not get. Each line says why, so the row can be put back the day the reason changes.
#14
install-snippet-ai-builders(screenshot,install/ai-builders.mdx) — the "AI builders" chip does not exist —src/features/install/platforms.tshas eleven and none is it (this was already recorded above).#17
webflow-custom-code-head(screenshot,install/webflow.mdx) — Webflow's site settings, on a Webflow account.#18
wordpress-theme-file-editor(screenshot,install/wordpress.mdx) — a screen inside WordPress's admin, on a WordPress install — not ours to open from the harness.#19
gtm-custom-html-tag(screenshot,install/google-tag-manager.mdx) — the Tag Manager editor, on a Google account.#20
framer-custom-code-head(screenshot,install/framer.mdx) — Framer's site settings, on a Framer account.#21
squarespace-code-injection(screenshot,install/head-field-platforms.mdx) — Squarespace's Code Injection, on a Business-plan Squarespace site.#22
wix-custom-code-settings(screenshot,install/head-field-platforms.mdx) — Wix's custom-code dialog, on a Premium Wix site.#23
ghost-code-injection(screenshot,install/head-field-platforms.mdx) — Ghost's admin, on a Ghost install.#24
webflow-custom-attribute-goal(screenshot,install/webflow.mdx) — the Webflow Designer, on a Webflow account.#44
proxy-broken-ip-symptom(screenshot,script/proxying.mdx) — a "what wrong looks like" figure that has to be manufactured — a proxy built to drop the IP, a site pointed at it, traffic from several addresses — against a site whose real numbers would then be wrong; the prose describes the symptom in full.#55
billing-payment-method(screenshot,billing/invoices.mdx) — the billing page has no payment-method block; the card lives in Stripe's portal.#65
x-timeline-markers(video,integrations/x.mdx) — the avatars on the axis are real people's posts, handles and profile pictures; the redaction cannot stand in for a third party's face, and a made-up post is not a capture.#67
threads-timeline-marker(video,integrations/threads.mdx) — asx-timeline-markers— a real author on a docs page, or nothing.#73
goal-with-value(screenshot,api/endpoints.mdx) — goal values arrive only throughPOST /api/v1/goals. That endpoint IS built (it was the first one), but no goal on any demo site carries a value, so the card still has no row to show. Capturable the day a site records a valued goal — this is now a data problem, not a missing feature.#80
gsc-impression-breakdowns(screenshot,integrations/google-search-console.mdx) — the facet queries exist and nothing renders them.#85
revenue-by-channel(screenshot,integrations/stripe.mdx) — no card breaks revenue down by channel with an unattributed row; the Sources card carries revenue per row when revenue is on, and that card is already captured.#95
paddle-import-progress(video,integrations/paddle.mdx) — capturable only while an import is in flight on a Paddle account with history; there is no such account to connect.#96
paddle-resync-history(screenshot,integrations/paddle.mdx) — the connected card with its summary line is the Stripe one already on the Stripe page (stripe-connected-state); putting a card that reads "Connected to Stripe" on the Paddle page would mislead, and there is no Paddle account to connect.#100
lemonsqueezy-import-progress(video,integrations/lemonsqueezy.mdx) — aspaddle-import-progress.#104
polar-attributed-history(screenshot,integrations/polar.mdx) — needs a Polar account whose imported orders carry checkout metadata; there is none.
Batching plan
Roughly how to sequence a capture session.
| Batch | What | Needs |
| A | 1–9, 17–18 | One fresh account, one new site, one test page to paste into. Do the flow once, capturing as you go. Yields the whole first-run story including the video. |
| B | 10–16 | The same account, cycling the Install tab's platform chips. Fast — one screen, seven states. |
| C | 19–26 | Accounts on Webflow, WordPress, GTM, Framer, Squarespace (Business), Wix (Premium), Ghost. The long pole. Start acquiring these while doing Batch A. |
| D | 27–40 | One test site seeded with ~1 month of varied human and crawler traffic. Capture the whole dashboard and AI panel in one pass so numbers agree between shots. |
| E | 41–48 | Deliberate failures: a CSP page, an ad blocker, a double install, an invalid event, a proxy that drops the IP. Each is a small setup. |
| F | 49–57 | After the corresponding features ship. |
Blockers worth knowing before you start
#27 dashboard-four-tiles and #37/#39 (the AI panel) — IP verification is not
running yet, so a live capture shows "Verified crawlers" as 0%. Either seed
verified rows, or accept the 0% and re-capture when verification ships. Do not
retouch the number.
#49–57 (sharing, team, billing, API) — these all said "not built" until 2026-09-03. Sharing, the team screen and billing have since shipped, so these can be captured now. Check the screen before assuming a row is still blocked; this note went stale once already.
#46 proxy-broken-ip-symptom — this one has to be manufactured. Stand up a
proxy that deliberately drops the client IP, point a test site at it, generate
traffic from a few different addresses, and capture the collapse. It is the most
effort of any single image here and it is also the one that stops a customer
destroying their own visitor counts.
| 61 | ga4-dashboard-boundary | screenshot | integrations/google-analytics.mdx | The Visitors chart across the first event: the GA span, the dashed boundary, the legend entry | The same pixels as #37 (this site's range straddles the boundary); a second file because the inventory joins on the filename. Light. |
| 62 | ga4-settings-card | screenshot | integrations/google-analytics.mdx | The Google Analytics card, connected: account, property, Import done — 148 days, views/sessions, range, the Show on the dashboard switch | The row said video; a still shows it. The Google account on the card is the owner's, not the demo user's — every address on the page is rewritten now, not only the first. Scrolled clear of the sticky top bar before measuring. Light. |
| 63 | x-integrations-card | screenshot | integrations/x.mdx | Mentions on X: the attribution switch off (Not tracking), Brand mentions (Not watching) with handle and terms fields | Example site, nothing switched. Light. |
| 64 | x-mentions-terms | screenshot | integrations/x.mdx | The mentions half of the X card: example in the handle field, three terms as tags with the 3/5 counter, Start watching | Example site, nothing saved. The handle is typed LAST and keeps focus: leaving the field runs the check against X, and a made-up handle answers "No account with that handle". So there is no verified tick — the row asked for one, and a real account would be a real handle on a docs page. Light. |
| 66 | threads-integrations-card | screenshot | integrations/threads.mdx | The Threads card, Not connected, with Connect Threads | On the Integrations tab (the docs and this table filed it there; the code renders it from the settings form). Example site. Light. |
| 68 | alerts-editor | screenshot | dashboard/alerts.mdx | The alert editor, "A goal fires" with signup: name and trigger on the start side, subject, message and the variable chips on the end side, the rendered preview beneath | Composed, not created. The preview renders New signup from Germany from the template, which is the point. The account's e-mail in the preview's To line is rewritten (alex@example.com). Light, 1440 × 1300. |
| 69 | alerts-list | screenshot | dashboard/alerts.mdx | The Alerts tab: the allowance line, then three alerts — Last sent with Pause, a paused one dimmed with Resume, a held-back one with its reason | The form can create an alert but cannot make one that has FIRED: Last sent and Held back read alert_deliveries, which only the checker writes. The three alerts and two deliveries were inserted by SQL for the take and deleted after, the table read back at zero. The list is not inside the header card, so the crop spans both. Light. |
| 70 | widgets-panel-off | screenshot | sharing/widgets.mdx | The Widgets tab, switch Off: the description and "Turn it on to preview the three widgets, choose their colours and copy the snippet" | The row (and the page) described type buttons, a preview and a snippet in the off state; off, the tab shows none of them — they appear once the widget is on. Taken on the example.com site, whose widget has never been on. Light. |
| 71 | widgets-snippet | screenshot | sharing/widgets.mdx | The snippet block: "Add this snippet to your website", HTML selected, Copy, the iframe tag | The row said video; a still shows it. Taken on the busiest site (the tab withholds the snippet while the widget is off), with the preview iframe beside it redacted frame by frame — d.redact() walks frames now, because the iframe printed the real domain. Re-shot 2026-09-15: the first take printed the widget's real slug in the src, which is the address of a real site's live numbers; redact.mjs replaces it by shape now (w_a1b2c3d4e5f6g7). Light. |
| 72 | widgets-card-styles | screenshot | sharing/widgets.mdx | The Preview widget in its four surfaces side by side — Plain, Glass, Dome, Panel — each at 330×200, labelled, on a neutral ground | The widget page IS the card, so each surface is the page at 330×200 with surface= in the URL — how the snippet carries the choice; nothing is saved. The four stills are composed with Pillow (demos/out/widgets-card-styles/parts/ holds them). The card's only hostname-shaped text is the site name, replaced by shape before the shot. Light. |
| 91 | widgets-revenue-card | screenshot | sharing/widgets.mdx | The Widgets tab with Revenue chosen: the preview card ($4.00, last 30 days, one bar on a date axis), the Period select, "Publish this revenue" ticked, the snippet | Taken on the busiest site; choosing the type and ticking the box both write the row, restored by SQL after (widget_type = 'analytics', revenue_published = false). The preview is an iframe that mounts again after each save, so the scene seals the wire before the clicks and redacts until no frame prints the real domain — the first three takes each leaked it. Cropped to the panel. Light, 1440 × 1400. |
| 74 | goals-card-mixed | screenshot | dashboard/goals.mdx | The Goals card, one rule-based goal (filter icon, "Rule") among six code-sent goals, each with its people count and conversion rate | The busiest site had no rule, so the take creates one through the dialog (path contains /pricing), shoots, and the rule is deleted afterwards and the table read back — zero rows. The rule is the only write. Light. |
| 75 | add-goal-rule-form | screenshot | dashboard/goals.mdx | The Add a goal dialog, "From a page visit" chosen among the five ways in, a name, match type contains, path /thank-you | Not saved. The match type is a native <select> — the row asked for it open; a native dropdown is drawn by the OS outside the page and no page screenshot contains it. The three options are in the prose. Light. |
| 76 | settings-revenue-source | screenshot | dashboard/what-counts-as-revenue.mdx | "What counts as revenue": A payment provider chosen, Revenue / MRR beneath it with MRR selected, Goals your code sends below | This control saves on click (updateRevenueSource, no Save button) — the scene reads the checked radio values first, restores them by clicking the labels at the end, and fails if the values differ afterwards. The first take's restore did not work and was caught by reading the row back, not by the log. The third option is "Goals your code sends", not "Goal values"; the page was corrected. Light. |
| 77 | gsc-search-terms-card | screenshot | integrations/google-search-console.mdx | The Sources card, Keyword tab, top row hovered: visitors, position, impressions, CTR in the tooltip | "The Search Console card" is the Keyword tab of Sources. The site's bare brand name sat in the list as a search term; redact.mjs now rewrites the domain's first label too (five characters or more). Light. |
| 78 | gsc-search-terms-show-all | screenshot | integrations/google-search-console.mdx | The Show all window: twenty terms with Visitors, Position, Impressions, CTR, sortable | Same take as the card. Light. |
| 79 | gsc-integrations-tab | screenshot | integrations/google-search-console.mdx | The Search Console card before connecting: Connect to Search Console and the three what-arrives / what-Google-withholds / how-far-back columns | Example site. The card's heading is screen-reader only; the crop is the rounded box around it. Light. |
| 81 | stripe-integrations-tab | screenshot | integrations/stripe.mdx | The Revenue card before any connection: Not connected, four provider buttons, the empty Restricted API key field, Connect to Stripe, Create a key in Stripe | On the example.com site. Light. |
| 82 | stripe-connected-state | screenshot | integrations/stripe.mdx | The Revenue card connected to Stripe: Connected, revenue-from date, last payment, Re-sync history with its summary, Report revenue in, Disconnect | Light. SHARED with #83 — the re-sync button and the last-import line are on this card. |
| 83 | stripe-resync-history | screenshot | integrations/stripe.mdx | The connected card's Re-sync history button beside "Last import: 3 payments, back to Apr 16, 2026 · …" | SHARED — the same file as #82. The row wanted "142 payments"; this site has three. Light. |
| 84 | revenue-card-and-chart | screenshot | integrations/stripe.mdx | The Revenue tile ($2.00, −90 %) above the chart with one orange bar on a right-hand $ axis | Press the Revenue tile and the bars join the chart. Two dollars is what this month is; a busier revenue site would show more bars, not a different screen. Light. |
| 86 | revenue-currency-select | screenshot | integrations/stripe.mdx | The "Report revenue in" control, reading Account currency | Not open: it is a native <select>, drawn by the OS outside the page. Cropped with its heading and note. Light. |
| 87 | goal-parameters-panel | screenshot | script/custom-events.mdx | The parameters panel for subscription_ended: mrr_delta_cents expanded with three values and their people · completions counts, the other parameters collapsed | The panel alone, not the card above it. The card keeps a fixed height whatever its row count (measured at 1440 and at 1000), so card-plus-panel is one-third blank. The panel names its goal in its own header. 1000 × 1400 viewport so the whole panel clears the floating dock. Light. |
| 88 | sharing-turned-off | screenshot | sharing/revoking-a-link.mdx | The Sharing card off: dashed border, "Sharing is off", the switch reading Not published, the link with its Copy button | The switch writes on click; the take flips it off, shoots, flips it back and waits for "Your public board is live" — and share_links.is_active was read back true. The row (and the page) said a "Turn sharing on" button; the control is a switch labelled Published / Not published, and the page now says so. Light. |
| 89 | shared-ai-panel-view | screenshot | sharing/sharing-the-ai-panel.mdx | The same board scrolled to the AI & LLMs card | Same take as #above, scrolled. Light, viewport. |
| 90 | settings-revenue-goal-picker | screenshot | dashboard/what-counts-as-revenue.mdx | "Goals your code sends" selected: the site's six goals with checkboxes, each "No value sent — would add nothing", and "Nothing selected, so every goal that carries a value counts" beneath | The row wanted two ticked goals marked "Sends a value"; no demo site sends a valued goal, and ticking one would change a real setting. What is shown is the honest state of this site, with the valueless marking the row also asked for. Same save-and-restore as #76. Light. |
| 92 | paddle-api-key-permissions | screenshot | integrations/paddle.mdx | The Revenue card with Paddle chosen and a key in the field; the note under Connect names the three permissions and never-expire | Paddle's own dialog is Paddle's. What the reader needs from it — which permissions — is printed on our card, so the card with a key in it is the picture. The key is the right shape and means nothing; Connect is not pressed. Light. |
| 93 | paddle-integrations-tab | screenshot | integrations/paddle.mdx | The Revenue card with the Paddle chip chosen: API key field, Connect to Paddle, Create a key in Paddle, the three permissions | Example site; choosing a chip only swaps the fields, connecting is a separate button never pressed. Light. |
| 94 | paddle-revenue-card | screenshot | integrations/paddle.mdx | The Revenue tile and the chart with revenue on — the card a Paddle connection fills | SHARED — the same capture as revenue-card-and-chart. The card does not change by provider, and no Paddle account exists to connect; the Stripe-filled card is what the reader sees. Light. |
| 97 | lemonsqueezy-api-key | screenshot | integrations/lemonsqueezy.mdx | The Revenue card with LemonSqueezy chosen: Store ID and key filled, the Test/Live note under Connect | As #paddle. The row wanted LemonSqueezy's own dialog with its Test/Live switch; the switch is what our note warns about. Light. |
| 98 | lemonsqueezy-integrations-card | screenshot | integrations/lemonsqueezy.mdx | The Revenue card with LemonSqueezy chosen | As #paddle. Light. |
| 99 | lemonsqueezy-revenue-card | screenshot | integrations/lemonsqueezy.mdx | The Revenue tile and the chart with revenue on — the card a LemonSqueezy connection fills | SHARED — the same capture as revenue-card-and-chart. The card does not change by provider, and no LemonSqueezy account exists to connect; the Stripe-filled card is what the reader sees. Light. |
| 101 | polar-token-scopes | screenshot | integrations/polar.mdx | The Revenue card with Polar chosen: Organization ID and token filled, the five scopes in the note | As #paddle. Light. |
| 102 | polar-integrations-card | screenshot | integrations/polar.mdx | The Revenue card with Polar chosen | As #paddle. Light. |
| 103 | polar-revenue-card | screenshot | integrations/polar.mdx | The Revenue tile and the chart with revenue on — the card a Polar connection fills | SHARED — the same capture as revenue-card-and-chart. The card does not change by provider, and no Polar account exists to connect; the Stripe-filled card is what the reader sees. Light. |
| 105 | proxy-managed-setup | video | script/proxying.mdx | Settings → General → Managed proxy on the example site: a subdomain typed, Add hostname, the CNAME record the card produces, Check DNS pressed and answered | The DNS provider is the reader's, and example.com is nobody's, so the record never resolves and the card cannot verify — Check DNS answers "No CNAME record yet", which is the honest state, and the narration says what happens once it is live. The half that is ours is shown whole. Add hostname writes proxy_host; cleared by SQL after and read back null. 46 s, narrated. |
| 106 | stripe-connect-flow | video | integrations/stripe.mdx | On one card, the example site's: a right-shaped key typed, Connect pressed, and the card becomes the connected one — Connected badge, revenue-from date, last payment, Re-sync history, Disconnect | Stripe's own key screen is Stripe's and is not in the clip. The press cannot be real (a made-up key is refused; a real one is not this capture's to use), so the button is disarmed and pressed on camera, and the connected card is BORROWED: the same component the app renders for the site that is connected, read off-camera in a context that records nothing, redacted there, and cross-faded in for the empty card (borrow / swap in scene.mjs). Nothing else on the page changes — the first cut moved to the connected site and showed every other integration connected too, which the owner watched and called wrong. 47 s, narrated. |
| 107 | gsc-connect-flow | video | integrations/google-search-console.mdx | On one card: the three columns before connecting, Connect pressed, and the card becomes the connected one — property, last sync, Disconnect — while the narration says what happens on Google's consent screen in between | Google's consent screen is Google's. Same disarmed press and borrowed card as stripe-connect-flow; the Analytics card beside it stays Not connected, which is the point. 50 s, narrated. |
| 108 | api-key-create-form | screenshot | api/getting-an-api-key.mdx | The New API key form whole: the name weekly-report-script, the nine-row permission grid with Analytics and Websites read-only and Notes carrying both columns, All sites, and expiry at Never expires | The grid is the part a reader cannot guess — a dash where Analytics has nothing to write, and one row showing that ticking Write also ticks Read. Shot at 1440×1500, because the form does not fit 900 and the first take clipped it at both ends and caught the page header. The take DOES submit: the list below needs the key. Both keys it mints are deleted afterwards and the table read back. Light. |
| 109 | api-key-list | screenshot | api/getting-an-api-key.mdx | Two rows: old-status-page dimmed with a Revoked badge and its revoked date, and weekly-report-script active with its permission chips, Edit and Revoke | Both states in one frame, because the page's point is that a revoked key STAYS on the list as the record it existed. Revoking is one-way, so the revoked key is one the scene minted seconds earlier — never an existing one. The clip is computed from those two rows, not the list: the account's own keys sit in the same list and their names and prefixes have no business on a docs page. A prefix is not a credential; the key itself is in no frame, because the one-time notice is dismissed before either shot. Light. |