Static landing site for Paper Balloon — the umbrella brand for party games where everyone plays together on the TV/screen and phones are the controllers (scan a QR to play, no install required).
Flagship game: HexStacker Party — live at hexstacker.com and on the Apple TV App Store. Tiny Track Party (kart racer) is playable at tinytrack.paperballoon.games; Powder (skiing) is still in development.
The Android TV builds and the phone controller are in closed testing, gated on
the testers Google Group — a Play
listing 404s for anyone who isn't a member yet. The landing page deliberately
says nothing about that: it links only what a visitor can actually open. Testers
are recruited by link instead, through /beta (/de/beta); delete those pages
and assets/beta.css once the apps are in production.
Neutral ink and paper; the game posters carry the color. Follows the system
light/dark setting (with a toggle), and both schemes share one layout. Tokens
live in assets/theme.css; fonts are self-hosted in assets/fonts/.
One face (Outfit) everywhere. assets/site.css holds what the home, room and
beta pages share: a bar-less header, the blurred-poster glow (behind the room
page's top), pill buttons and the footer panel. The home hero instead shows the
flagship poster on a drawn TV, with a confetti-coloured light behind it in the
light scheme only. Posters are shown whole, never cropped. Coral fills only a
page's one primary button.
The legal pages (assets/legal.css) have no header or footer: the controller
apps show them in-app under their own title bar and hide the page's
.legal-shell > h1, so keep that selector working.
index.html,de/index.html+assets/home.css— home page (EN / DE)room.html+assets/room.js— room join page, served at/<CODE>#<instance>(nginx maps any 6-char base58 path to it); browser fallback for the app deep links in.well-known/. Also the site's 404 page (with status 404), offering code entry, since a stray path is usually a mistyped codegames-manifest.json— drives the room page and is fetched by the controller apps. Keep in sync with the controller repo's bundled copy.artis the 16:9 cover,iconthe square brand mark (nearby-room / rejoin cards in the launcher). The apps match their bundled copy by filename, and cache anything they didn't ship by URL without revalidating — so re-rendered artwork ships under a new name (…-v2.webp) here, in both app bundles and in the home page's herosrcset; never as a?v=bump, which the filename match would swallow.videois a trailer on the game's own server; the apps cache it by full URL forever too, but ship no copy to match, so a re-rendered trailer is a?v=bump onvideo(here and in both app bundles), made only once the new file is live on the game's servercontroller-test.html+assets/controller-test.{css,js}— a stand-in game controller that exercises every touchpoint in the controller repo'sCONTRACT.md, so the launcher can be tested without a real game and a game author can watch each one behave.noindex, and deliberately not in the sitemap. Update it in the same change that alters the contract — it is the only executable statement of that spec, and bump the?v=on its CSS/JS when you do, or the year-longimmutablecache hides the change. See "Reaching it from the app" belowbeta.html,de/beta.html+assets/beta.css— closed-test sign-up steps (group, per-app opt-in links, install, 14 days), shared by link.noindex, not in the sitemapprivacy.html,imprint.html— legal pages (German, umbrella policy for all Paper Balloon infra); English versions inen/assets/— CSS (tokens intheme.css, shared page parts insite.css, one stylesheet per page), 16×9 game posters and square brand icons inartwork/nginx.conf,Dockerfile— the deployed container; fully static otherwise
Serve the repo through the production nginx config, so room links
(/<CODE>), extensionless pages (/privacy) and the security headers (CSP)
behave as deployed. The repo is mounted, so edits show on reload, but CSS/JS
are served immutable: bump their ?v= or disable the browser cache while
iterating.
docker run --rm -p 8000:8080 \
-v "$PWD":/usr/share/nginx/html:ro \
-v "$PWD/nginx.conf":/etc/nginx/conf.d/default.conf:ro \
-v "$PWD/security-headers.conf":/etc/nginx/snippets/security-headers.conf:ro \
nginxinc/nginx-unprivileged:1.27-alpinepython3 -m http.server 8000 is enough for the landing page alone; it knows
none of the nginx routes, so room links and /privacy 404 there.
The launcher only loads a page as a controller if JoinResolver hands it to
joinVerbatim. The apex can never do that: every paperballoon.games /
www.paperballoon.games URL is routed to the sole live game's controllerBaseUrl
instead, whatever its path. A paperballoon.games subdomain does load verbatim,
which is what test.paperballoon.games exists for.
-
Any build — tap https://test.paperballoon.games/PBTEST. The 6-character path is not decoration: it is all the Android App Links intent filter claims, so a shorter or longer one opens the browser instead of the app. Traefik maps it to
controller-test.html(host-scoped, in the private cluster repo — the same alias innginx.confwould shadow that room code on the apex for a real player). -
Debug launcher builds, over the LAN — for iterating without a deploy. Serve this directory and scan the QR with the app's own scanner; private hosts take a debug-only branch in
JoinResolverthat exists for exactly this. Manual code entry can't be used — it caps input at 16 characters and resolves through the relay.python3 -m http.server 8000 qrencode -t ANSIUTF8 "http://$(ipconfig getifaddr en0):8000/controller-test.html"
Either way no room, no relay and no display are involved — joinVerbatim
loads the URL as given, and the room code only labels the home rejoin card (which
is offered unverified when the URL surfaces none).
The test host serves this whole site, so an X-Robots-Tag: noindex middleware
covers it; the page carries a noindex meta of its own.