AmoleoFamily

Marketing Surfaces

Live

RepoStatusDesktopPhone
Amoleo-PetsAdoptedassets/pets-desktop.pngassets/pets-phone.png
Amoleo-ConnectionsAdoptedassets/connections-desktop.pngassets/connections-phone.png
Amoleo-RissbrookAdoptedassets/rissbrook-desktop.pngassets/rissbrook-phone.png
Amoleo-AccountsAdoptedassets/accounts-desktop.pngassets/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:

Two things that are not the rule, and cost time when treated as it:


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:

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:

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:

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:

  1. Capture against a local development database, never production, and never a staging environment holding a copy of production.
  2. 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.
  3. Invented details throughout — names, vets, policy numbers, phone numbers. Use obviously-fictional-but-plausible; 020 7946 xxxx is the reserved UK drama range and exists for this.
  4. 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:

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:


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.


5. Capturing one, step by step

  1. Seed the demo data (§3) and run the app locally.
  2. Log in and clear every modal (§4).
  3. emulate viewport: 1280x800x2, reload, navigate to the chosen screen, adjust the scrolling container, screenshot to assets/<product>-desktop.png in AmoleoWebsite.
  4. emulate viewport: 390x844x2,mobile,touch, reload, navigate, screenshot to assets/<product>-phone.png.
  5. Point the two <img> tags in index.html at the new files. Give the desktop shot an alt describing what the screen shows, and the phone shot alt="" — it is the same app again and is decorative, but the attribute must be present or a screen reader reads out the filename.
  6. Update the product's entry in screenshot-manifest.json: paths, adopted, captured.
  7. node tools/check-screenshots.mjs.
  8. 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:


7. Things that will otherwise cost an hour


8. What to check afterwards

node tools/check-screenshots.mjs
node tools/check-brand.mjs

Then, on the served page:

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.