Show HN: Launder, a puzzle you can only win by removing SynthID watermarks

sshh121 pts0 comments

Launder WM

. copy.toml arrives in the boot<br>payload and is applied by main.ts through [data-copy] /<br>[data-copy-aria], i.e. by setting textContent and aria-label on<br>elements AFTER JS runs. A crawler or a link unfurler reads the head<br>and leaves; it never runs main.ts. A `data-copy` on a would be<br>filled long after the only reader had gone.<br>2. THE ALTERNATIVE IS A SECOND COPY IN THE SAME DOCUMENT. Putting the<br>description in copy.toml would mean boot.py substituting it into the<br>head — while the identical sentence also ships, verbatim, inside the<br>#launder-boot JSON on the same page, because the whole copy tree is<br>inlined. One string, two places, one document.<br>3. ALREADY WORKS THIS WAY. "Launder WM" has been static here<br>since M0 and copy.toml has never owned it. og:title and og:site_name<br>are the same document identity restated for a different reader, so<br>they follow the string that is already static rather than inventing<br>a second home for the product's name.<br>4. NOTHING BELOW IS RENDERED IN THE PRODUCT. It is never on screen for<br>a player, so it is not the playtesting surface copy.toml exists to<br>make editable without a code change.

The consequence is that these strings sit outside `forge lint-copy`, so<br>the COPY STANDARD is enforced on them by web/tests/boot.test.ts instead:<br>the description may not say "human", may not print a "%" figure, and may<br>not call the watermark "secure" (copy.toml's rule 2, §14.4). "A real<br>detector" is the one claim the project does authorise — the key is<br>published, which makes the detector real, not secure — and the<br>description stops there. It does not say what wrote the text and it does<br>not put a number on it.

DELIBERATELY ABSENT, so nobody adds them back by reflex:<br>- og:image. Every unfurler (Facebook, X, Slack, iMessage) fetches the<br>image over the network and decodes it as PNG/JPEG/GIF/WEBP. An SVG is<br>not accepted and a data: URI is not fetchable, so the only way to<br>have one is a checked-in binary, which this repo does not want. A<br>card with a title and a description and no image is a correct card;<br>an og:image pointing at something no crawler can read is a broken one.<br>- twitter:title / twitter:description. X falls back to og:* for both.<br>Restating them is two more strings that can drift from the two above.<br>- apple-touch-icon. See the icon block below: iOS requires a PNG.<br>-->

as an asset reference:<br>a data: URI and an absolute URL are skipped, but `/` is read as a public<br>path, resolved to `web/`, and handed to readFile — which fails the build<br>with `EISDIR: illegal operation on a directory`. `vite-ignore` is the<br>documented opt-out; it makes vite skip the node and strip the attribute<br>itself from dist/index.html, so the tag that ships is the tag written<br>here. The og:url beside it needs nothing: vite only inspects<br>`content` on the og:image/og:video/og:audio family.<br>-->

at all the browser goes and<br>asks for /favicon.ico on every cold visit, which on this server falls<br>through the API routes to the static mount and 404s. A declared icon<br>removes that round trip instead of answering it.<br>- no binary asset, no PNG pipeline, no CDN.

THE GEOMETRY. It is the `wash` glyph from src/game/gate.ts — the game's<br>verb is "wash it out", and the basin is already the product's central<br>image — but it is NOT that path. Pasting the 24px in-app path here would<br>produce grey mush at 16px, for two measurable reasons:

- STROKE. The in-app symbols are 1.35 units on a 24 grid. Rendered into<br>a 16px box that is 1.35 * 16/24 = 0.9 DEVICE PIXELS, i.e. below one<br>pixel, so every line antialiases to a soft grey smear. This icon uses<br>2.2 units = 1.47px at 16, and 2.9px at the 32px size browsers ask for<br>on a retina tab strip. 1.63x the in-app weight.<br>- DETAIL. `wash` is three parts: the basin, a five-node wave with three<br>bumps, and the arc over the rim. At 16px each bump of that wave is<br>1.6px wide and reads as a straight bar, and the arc sits ~4.8 units<br>off the rim, which at a 2.2 stroke leaves under a device pixel of gap<br>— the two merge into one thick smudge. So the arc is DROPPED and the<br>wave is reduced from three bumps to one full period.

What survives is the two irreducible ideas: the vessel and the water. At<br>16px it reads as a basin with a line in it, which is already<br>unmistakable; the wave resolves as a wave from 32px up. Every clearance<br>in the drawing was solved for the 16px case: 2.0 units (1.35px) between<br>the wave and the rim above it, 1.9 (1.27px) to the basin floor below it,<br>and 2.0 to each wall — which is why the wave spans only x 7.6..16.4<br>rather than the full interior.

COLOUR. --ink from tokens.css, both modes, via a media query INSIDE the<br>SVG. `stroke="currentColor"` — what the in-app symbols use — is wrong<br>here: a favicon is its own document with no cascade from the page, so<br>currentColor resolves to the initial black and disappears against dark<br>browser chrome. The presentation attribute carries the light value and<br>the @media rule overrides it, so a browser that renders SVG icons but<br>ignores the query still gets...

copy from wave image toml data

Related Articles