Marketing Surfaces
Live
| Repo | Status | Desktop | Phone |
|---|---|---|---|
| Amoleo-Pets | Adopted | assets/pets-desktop.png | assets/pets-phone.png |
| Amoleo-Connections | Adopted | assets/connections-desktop.png | assets/connections-phone.png |
| Amoleo-Rissbrook | Adopted | assets/rissbrook-desktop.png | assets/rissbrook-phone.png |
| Amoleo-Accounts | Adopted | assets/accounts-desktop.png | assets/accounts-phone.png |
Filenames only — actual image bytes live in each product's own repo, not reproduced here.
Product screenshots — the shots on the front page
The desktop-and-phone pair shown against each product on www.amoleo.com. They
live here, in the umbrella workspace, because no single app owns them: each one
is captured from a product repo, and all four land in Amoleo-Website. Neither
end is the natural home, so the process is written down in the place that
belongs to both.
Amoleo-Website is the reference implementation. Where this document and that
code disagree, the code is right and this should be corrected.
The shapes and filenames below are also held in machine-readable form in
tools/screenshot-manifest.json, which
tools/check-screenshots.mjs enforces. Change
a value in both, in the same commit. Run the checker after touching a shot or
the markup around it:
node tools/check-screenshots.mjs
Why these matter more than they look. The front page is the only surface in the family where all five products appear at once, so it is where "one brand" either holds or visibly does not. Four screenshots taken on four days at four densities read as four companies. The lockup can be pixel-correct and the page will still look assembled from parts.
And they rot quietly. A brand colour that goes wrong is wrong on a live site
somebody visits; a screenshot that goes wrong is wrong on a marketing page its
owner scrolls past. The app ships for a year, the screenshot keeps showing last
year's layout, and nobody is ever surprised by it — which is the same failure
mode described in CLAUDE.md: mistakes nobody can see.
0. Status — who ships what
| Repo | Site | Desktop + phone shots |
|---|---|---|
Amoleo-Pets |
pets.amoleo.com |
Real. Captured 1 Aug 2026, amoleo theme. |
Amoleo-Connections |
connections.amoleo.com |
Real. Re-captured 7 Aug 2026 in light (was dark, see §2.2's history). |
Amoleo-Rissbrook |
www.rissbrook.co.uk |
Real. Captured 7 Aug 2026. No demo data — Robert Bickley's own research. |
Amoleo-Accounts |
accounts.amoleo.com |
Real. Captured 27 Aug 2026, / root page, light theme (app default is dark). |
Replacing a placeholder is one commit in Amoleo-Website (the two PNGs plus the
<img> tags) and one in this repo (the manifest entry flipping adopted to
true with a captured date). Flip adopted in the same change that swaps the
image — the checker treats a placeholder under an adopted product as a failure
precisely so the two cannot drift.
All four shipped products now carry a real capture (Accounts' as of 27 Aug 2026, §0 above) — a future fifth product still gets a placeholder wireframe until it has a screen worth photographing, same idiom as the other three did before it. That mix is expected, not a bug, and it is visible rather than hidden — which is the point.
1. What a shot has to show
The editorial rule, and the one worth arguing about before opening a browser:
A stranger who reads nothing should be able to say what the product does.
Not what it looks like — what it does. That rules out the login screen, the empty state, the settings page, and the one beautiful screen with no data in it. It usually rules in the screen the product's own users spend their time on.
Concretely, per product:
- Pets — the Overview: several animals, each with a status, above the list of what is due. It reads as "something is keeping track of these for me", which is the product. The single-pet page is the phone shot, because a phone is where one pet actually gets logged.
- Connections — the graph mid-exploration, one actor expanded outward. Enough nodes that it reads as a map rather than a diagram of three things. A graph with four nodes undersells the only feature anyone comes for.
- Rissbrook — a person page or the tree. The one product with no privacy problem to solve first: it is public genealogy of the long dead.
- Accounts — the
/root page: the hero pitch, not the sign-in form itself. Accounts has no account-holder-facing screen the way the other three do (no dashboard, no board, no tree) — its whole product is the identity it quietly hands to the other apps./is the only screen that exists to show a stranger, and it deliberately carries no live sign-in form of its own (Amoleo-Accounts/docs/Decisions.md#14) — the real one only exists behind a redirect from another app, with no cold-start state to photograph.
Two things that are not the rule, and cost time when treated as it:
- It does not have to be the prettiest screen. A shot that flatters and misleads is worse than a plain one that tells the truth, because the click through to the real thing is the moment the promise is tested.
- It does not have to be one frame of a real session. Choosing which pet, which state, and which scroll position is staging, and staging is fine. Fabricating a feature is not. The line is whether the screen shown is a screen the app can actually produce.
2. The two sizes
| Shot | CSS size | File size | Ratio |
|---|---|---|---|
| Desktop | 1280 × 800 | 2560 × 1600 | 16:10 |
| Phone | 390 × 844 | 780 × 1688 | iPhone 12/13/14 |
Both are captured at 2× device pixel ratio. The page displays them at the CSS size, so a 1× capture is soft on every laptop made in the last decade, and soft next to a sharp one is the tell that they were taken by different people on different days.
The width and height attributes on each <img> carry the CSS size, not
the file size — they reserve the box before the image loads so the page does not
shift as it settles. The checker enforces the relationship between the three
numbers, which is the whole reason it can catch a 1× capture.
Two framing constraints from the page's own layout, worth knowing before choosing a scroll position:
- The desktop shot bleeds off the right edge of its container by design, so nothing load-bearing should sit hard right. In the Pets shot the third pet card is clipped; that is the layout working, not a mistake.
- The phone shot overlaps the desktop shot's lower-right corner, so its own bottom third is partly obscured. Put the substance in the top two-thirds.
2.1 The phone shot is the one that goes wrong
Chrome will not make a window narrower than 500px. Resizing the window to 390 gives you 500 and no error — the screenshot simply comes out too wide, with a tablet layout in it, and looks plausible until it is next to a real one.
So the phone shot must be captured through viewport emulation, not a window
resize. With chrome-devtools:
emulate viewport: 390x844x2,mobile,touch
Then reload — a responsive app that decided its layout at first paint will not
re-decide it just because the viewport changed. mobile and touch matter
beyond size: they are what make the app serve its phone navigation rather than a
narrow desktop.
The desktop shot has no such trap, but take it through emulation too
(1280x800x2), for the density and so both shots come from the same mechanism.
2.2 One theme, family-wide: light
Every shot is captured in the app's default light theme. Not the capturer's preference, not whichever looked better on the day.
This is settled rather than chosen, and it is worth knowing why so nobody reopens it:
- Rissbrook has no dark theme. Its dark palette is written, measured, and
deliberately commented out in
src/styles/tokens.css— "the decision was for now". A dark-mode family rule would silently oblige someone to reverse that decision in order to take a screenshot, which is the tail wagging the dog. Light is therefore the only theme all four products can actually satisfy. - The page behind them is dark. Light shots read as windows onto the app and hold a consistent contrast against it. Four apps in four themes would not.
- Connections' light theme was built for exactly this. Its own stylesheet describes Light as "Amoleo · Pets' palette, unchanged, so the two apps are recognisably the same family". The family view of Connections is its light theme, by its own author's account.
This rule has a real cost, and pretending otherwise would get it quietly
reversed later. Connections defaults to dark — its :root is the dark
theme, and light is opt-in via data-theme="light". So its shot is not the
first thing its own users see, and its dark theme is the one tuned for the
graph: navy was dropped to near-black there because it "competed with the drama
nodes, which are themselves blue". Capturing Connections in light means checking
that the graph still reads well, not just flipping an attribute.
That cost was weighed against a front page mixing light and dark panels, and lost. Consistency across the four wins.
Three specifics:
Pets has fifteen themes —
amoleo(the default) plus thirteen cat and dog palettes andmidnight, its dark one. The shot takesamoleo. A demo account that has never opened the theme picker is already there, which is one more reason to seed a fresh account rather than reuse one somebody has played with.Connections must be told.
data-theme="light"on<html>, set before the capture — its default is the other one. The graph reads its colours from the same tokens, so it re-themes with the rest of the page rather than needing anything separate.Set the colour scheme explicitly at capture time. A browser profile inherits the machine's
prefers-color-scheme, so an app that respects the OS setting will hand you a dark screenshot on a dark-mode machine without ever being asked. Withchrome-devtools, pin it:emulate colorScheme: lightDo this even when the app appears to be light already — "appears" is doing the work in that sentence, and the failure only shows up on someone else's laptop.
The checker measures each shot's mean brightness and fails one that is plainly
dark (§6, R7). It cannot tell amoleo from ginger — that part is on you.
3. Demo data — never real records
No screenshot on the marketing site shows real user data. Not the owner's own, either.
This is not only a privacy rule, though it is that. It is also that real data is bad at this job: a real account is lopsided, half-abandoned in places, and full of the specific — a cat's actual name, a real vet's phone number, a genuine medical problem. Seeded data can be made to show the product working as intended without showing anyone's life.
The rule in full:
- Capture against a local development database, never production, and never a staging environment holding a copy of production.
- Seed a dedicated demo account for the purpose. Do not reuse the QA leftovers already in the dev database — they exist to prove a bug is fixed, and they look it.
- Invented details throughout — names, vets, policy numbers, phone numbers.
Use obviously-fictional-but-plausible;
020 7946 xxxxis the reserved UK drama range and exists for this. - Keep the demo account after capturing. Re-shooting a year later against a different set of numbers means a screenshot that no longer matches its neighbours in the archive.
3.1 Every product with an account has a seed script
A product whose screenshot requires signing in must ship a demo-data script at
tools/demo-data/<id>-demo.mjs. Not a note, not a paragraph in a README — a
script that runs.
This is the rule that "keep the demo account" turns into when it meets reality. Connections' first screenshot was captured from a hand-built board that existed only in whoever's database it was built in. When the session expired, the shot became unreproducible: the board could not be re-photographed in a different theme, at a different size, or after a redesign, without somebody rebuilding it by hand from memory. A screenshot you cannot retake is a screenshot you cannot correct, and this whole document is about correcting screenshots.
All of them follow the same shape, and new ones should be copied from the nearest existing one rather than reinvented:
| Talks to the app's published HTTP API | Not the database. A schema change then surfaces as a 400 with a message attached rather than as a document that silently no longer matches. The cost is that the app must be running; the gain is that it cannot write something invalid. |
| Register-or-login at the top | So the first run creates the account and every later run reuses it. |
| Idempotent | A second run reports what already exists instead of doubling it. Both current scripts print on board already / weights exist lines rather than staying silent, so a re-run visibly does nothing. |
| Header comment carrying the run command | Including which servers must be up and on which ports. |
| Says it is local-only, and why the password is in the repo | It is a throwaway account on a development database, which is exactly why it must never exist anywhere real. |
Products that need no seed script get demoData: null in the manifest rather
than leaving it ambiguous — Rissbrook because it's public with no account
system at all, and Accounts (despite being a real, adopted login system) for
a different reason: its own root / page carries no AuthCard by design
(Amoleo-Accounts/docs/Decisions.md #14), and the actual sign-in form only
exists at /interaction/:uid, reached via another app's /authorize
redirect, which has no stable cold-start state to seed and photograph. This
paragraph used to say "Accounts has no code" — true when it was written, not
since; corrected 3 Sept 2026. check-screenshots fails a product that
declares a script it does not have (R8).
3.2 The demo username appears in the screenshot
Pick a handle a real enthusiast would pick. It is on the marketing page, at readable size, in the header of two of the four shots.
amoleodemo — the first pass at this — reads as a test account, because it is
one. It tells a visitor they are looking at a rehearsal. A handle in character
does the opposite: it costs nothing, and it quietly says a person uses this.
The convention:
- In character for the product.
ILoveCats90for Pets,soyeon-is-my-biasfor Connections. Enthusiasm plus a little specificity. - No
demo,test,qa,exampleoramoleoin the name. Those are the words that break the effect. - Nobody real. Not a real person's handle, and not a name that could be mistaken for one. Fictional-but-plausible, the same rule as the vet's phone number.
- Fit the product's own username rules, and check how it is displayed.
Pets uppercases the handle in its header, so
ILoveCats90shows asILOVECATS90. Connections lowercases and slugifies — it doubles as the public profile URL — so hyphens do the work that capitals would:soyeon-is-my-bias, notSoyeonIsMyBias, which would silently becomesoyeonismybias.
The email follows the handle at @amoleo.example, which is a reserved TLD and
cannot receive mail.
3.3 Pets
cd ../PetWeightTracker && npm run dev # Express :3000, Vite :5173
node tools/demo-data/pets-demo.mjs # from Amoleo-Family
tools/demo-data/pets-demo.mjs. Account
ILoveCats90. Three cats — Mochi, Pepper and Willow — with eighteen fortnightly
weigh-ins each, flea and worming schedules, and recent doses logged so nothing
shows as overdue.
Pets needs a local MongoDB on :27017 (petweight-mongo, devpetweight), which
its own repo documents.
3.4 Connections
cd "../K-Drama Connections" && npm run dev:server # API :4000
cd "../K-Drama Connections" && npm run dev:client # Vite :5173
node tools/demo-data/connections-demo.mjs # from Amoleo-Family
tools/demo-data/connections-demo.mjs.
Account soyeon-is-my-bias. Five K-dramas with heavy cast overlap — Lee Min-ho
carries three of them on his own — because a graph whose nodes do not connect is
a picture of the one thing this product is not for.
Titles are resolved by searching rather than by hard-coded TMDB ids. The ids would be faster and would rot silently; a search that stops matching fails loudly and prints what it found instead. It needs the server's TMDB credential configured, which that repo documents.
Two things this app needs that Pets does not:
npm run devalso launches Electron. Usedev:serveranddev:clientseparately for a browser capture.- The client stores its token as
kdc.token. Worth knowing before restoring a session by hand.
4. Getting the app into the right state
Everything below is the difference between a shot that took ten minutes and one that took an hour. All of it was learned on the Pets shot; most is not specific to Pets.
- Dismiss the modals first, and know they exist. Pets opens a push -notification prompt on first login and a PWA install prompt after that. Both are centred, dimmed, and entirely reasonable in the app — and both make the screenshot a picture of a dialog box.
- Seed towards the calm state, not the alarming one. Pets initially showed every medication overdue in red, because the seeded medications had start dates in the past and no doses logged against them. It was honest and it sold nothing. Logging recent doses turned it into "all caught up" with a spread of upcoming dates — the same app, showing what it is for rather than what it does when neglected.
- The app may not scroll the document. Pets scrolls an inner container
(
.app-container, and.pet-detail-viewwithin it), sowindow.scrollTodoes nothing and a full-page screenshot returns one viewport. Find the scrolling element and set itsscrollTop; the checker cannot help you here. - Avoid mid-scroll frames. A sticky header with cut-off text bleeding under it looks broken rather than continuing. Frame from a natural top: a card edge, a section heading, the top of the page.
- Carousels do not respond to
scrollLeft. Pets' phone pet-switcher is an Embla carousel driven by transforms. Navigating straight to the pet's own URL centres it properly; forcing the scroll position leaves it visibly half-way between two items.
5. Capturing one, step by step
- Seed the demo data (§3) and run the app locally.
- Log in and clear every modal (§4).
emulate viewport: 1280x800x2, reload, navigate to the chosen screen, adjust the scrolling container, screenshot toassets/<product>-desktop.pnginAmoleoWebsite.emulate viewport: 390x844x2,mobile,touch, reload, navigate, screenshot toassets/<product>-phone.png.- Point the two
<img>tags inindex.htmlat the new files. Give the desktop shot analtdescribing what the screen shows, and the phone shotalt=""— it is the same app again and is decorative, but the attribute must be present or a screen reader reads out the filename. - Update the product's entry in
screenshot-manifest.json: paths,adopted,captured. node tools/check-screenshots.mjs.- Serve the site and look at it. The checker verifies shape, never content — it cannot tell a good screenshot from a picture of an error page.
6. What the checker actually checks
Six rules, all of them things that have gone wrong or obviously would:
| Rule | |
|---|---|
| R1 | index.html and the manifest name the same file |
| R2 | A placeholder appears if and only if the product is not adopted |
| R3 | The file exists |
| R4 | width/height attributes match the CSS size |
| R5 | The PNG's pixels are exactly the CSS size × 2 |
| R6 | Desktop alt describes the shot; phone alt is present and empty |
| R7 | Mean brightness is above minMeanLuminance — i.e. it is a light-theme capture |
| R8 | A product with a demo account declares a seed script, and that script exists |
R5 is the one that earns the file: it is what catches a phone shot taken by resizing a window into Chrome's 500px floor, which is invisible in review.
R7 decodes the PNG by hand — it walks the IDAT stream and reverses the row filters — because the format keeps no summary of its own pixels. It handles 8-bit truecolour, non-interlaced: what Chrome emits. Anything else reports as "could not measure" rather than guessing, since a wrong answer there would fail a correct screenshot, which is worse than not asking. It caught Connections' first shot on the day it was added.
Not checked, and deliberately:
- Whether the screenshot is any good. No script can see that the app was mid-error, showing a placeholder avatar, or displaying somebody's real cat.
- Staleness, as a failure. A screenshot of a screen that has not changed is
still correct at any age, so age prints as a note past
staleAfterDays(one year) and never turns the run red. Red has to keep meaning "this is wrong".
7. Things that will otherwise cost an hour
- Chrome's 500px window floor (§2.1). The single biggest trap here.
- A trend chart may not look like a trend. Pets' weight chart has a fixed 0–11 kg axis, so a cat losing a kilogram over eight months renders as a nearly flat line — accurate, and it undersells the product's central feature. The Overview was chosen instead. If a shot needs the app to change first, that is a finding about the app, not a reason to fake the shot.
- Empty states creep into the frame. The Pets phone shot carries a "PHOTO TIMELINE — NO PHOTOS YET" row, because the demo account has no photos. Seed the thing, or frame it out; do not pretend not to have noticed.
- Default avatars read as unfinished. Pets falls back to a coloured emoji when a pet has no photo. It is real UI and it is honest, but three of them in a row is the shot's weakest point. Any product with a photo slot has this problem.
- The page is dark and most apps are not.
www.amoleo.comis a dark page; the Pets shot is light. That contrast is fine — it reads as a window onto the app — but check it on the actual page rather than assuming.
8. What to check afterwards
node tools/check-screenshots.mjs
node tools/check-brand.mjs
Then, on the served page:
- Both shots load, at 1280 and at 390 wide. The lazy-loaded ones are below the fold — scroll to them, which is exactly what nobody does by accident.
- The desktop shot's right-edge bleed clips nothing that mattered.
- The phone shot's bottom third being partly covered loses nothing that mattered.
- No real names, no real contact details, no real medical information.
- The
alttext describes the screen rather than naming the product.
Generated from docs/product-screenshots.md + tools/screenshot-manifest.json + tools/repos.json (status). Edit there and re-run npm run build in docs-site-src/ — never hand-edit a page.