Replace the stacked sign-in card with a responsive public shell, a local monochrome compute field, restrained grain and reduced-motion-safe drift. Share the anonymous header with Learn so public navigation stays consistent, and carry the corrected spacing through registration and profile setup.
Three ~28s tracks Karti generated, re-encoded from 160kbps to 96kbps and
pulled from -15 LUFS to -26. They were mastered at foreground level; the
failure mode of background music in a tool someone has open for eight hours
is not "too quiet". Playback volume is a further 0.14 on top.
AUTOPLAY DOES NOT MEAN AUTOPLAY. Every current browser refuses audio with
sound until the page has had a real user gesture — Chrome sometimes relents
for a site with a high Media Engagement Index, Safari essentially never does
on a first visit. So `play()` rejects on mount, and the naive version of this
looks like a bug: the control says playing and nothing is audible. This tries
immediately, and on refusal arms a one-shot listener and starts on the first
click or keypress. Verified in Chrome: paused on load, playing 2.2s after the
first click. That also happens to be the kind behaviour for someone who opened
six tabs at once.
Mounted in Shell, NOT in App. The anonymous Learn page renders outside Shell,
and a share-code visitor opening a link someone sent them should not get
unexpected audio — that is the one context where it reads as a fault rather
than as character. Asserted by there being no <audio> element on that page at
all.
Preference is localStorage and deliberately not mirrored to the server, on the
same reasoning as the sidebar: whether you want music depends on whether you
are wearing headphones, not on who you are.
Also pauses on tab hide, because music from a tab nobody is looking at is the
thing people hunt through twenty tabs to kill.
The element is rendered rather than `new Audio()` so it is inspectable in
devtools and in a test; `preload="none"` means a user who mutes it never
downloads a track.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The app is pre-launch and shared by link with a handful of people at Prime
Intellect. It should not be accumulating a search footprint yet.
Three layers, because each covers a gap the others leave:
- robots.txt asks well-behaved crawlers not to fetch at all.
- The <meta name="robots"> tag covers the HTML document for anything that
fetched anyway.
- X-Robots-Tag covers everything that is NOT the HTML document — og.png,
the manifest, the built assets — which the meta tag cannot reach.
noarchive and nosnippet are there so a cache or an excerpt cannot outlive
the page once this is reversed.
Deliberately NOT stripped: the og:/twitter: tags. Link unfurlers are not
crawlers — they fetch on behalf of the person pasting the link, and a
rendered card is exactly what we want when this is shared.
The real gate remains authentication: / returns the sign-in screen and every
/api/ route returns 401. This only stops the app being indexed.
To go public: delete robots.txt, drop the meta tag, drop the header.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Social and icons. A 1200x630 card, apple-touch-icon, and maskable PWA icons,
generated from an HTML template by a script so the mark, wordmark and tagline
cannot drift from the product. The apple-touch-icon referenced in index.html
was a 404 until now. Icons are drawn on an opaque plate with inset because iOS
rounds corners and Android may apply a circle — an edge-to-edge mark loses its
ears to that crop. og:image is absolute, which is the single most common
reason a card unfurls blank.
PIG_INVITE_CODE was a lie. Signup validates against the invites table, so
setting the variable only flipped a label in the UI — an operator would set it,
hand the code to a colleague, and watch them be rejected. It is now reconciled
into a real invite row at boot: setting it issues, changing it rotates and
revokes the predecessor, and removing it revokes. Verified all three, plus that
a restart with an unchanged code does not duplicate.
Two bugs found while doing that:
- `uses_remaining` was jsonb, so the SQL decrement could never have worked.
Now integer. The generated migration failed because Postgres has no implicit
jsonb->integer cast, so the USING clause is hand-written.
- Redemption keyed off `redeemedAt`, which would have made every reusable
invite single-use — a confusing way to lock a team out. Availability now
comes from `usesRemaining`, and redemption records who used it most recently
without consuming it.
Sign-in gains a password option alongside the magic link, defaulting to
password since that is the daily path. PIG stores neither; both are handled by
the identity provider and PIG only ever sees the resulting token.
autocomplete is set so password managers and iOS can fill.
Verified: full migration chain applies to a fresh Postgres, the CSP hash for
the inline theme script is unchanged by the rebuild.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
apps/web — React, Vite, Tailwind, shadcn-idiom components. Mobile Safari is a
first-class target, not an afterthought:
- Two navigation treatments rather than one compromise. A bottom tab bar on
phones, because the top of a large phone is out of thumb reach; a persistent
sidebar from lg upward, so an iPad in portrait gets it too.
- Safe-area insets throughout, so the tab bar clears the home indicator and the
last row of a list is actually reachable.
- Inputs are pinned to a 16px minimum, which is the correct fix for Safari
zooming on focus. user-scalable=no is not used: it breaks pinch-zoom for
everyone and recent iOS ignores it anyway.
- The pipeline board becomes a stage picker on phones. An eight-column board
scrolling horizontally on a 390px screen is technically responsive and
practically useless.
Theming: users pick an accent and the whole interface re-tints. Accent values
live once, in @pig/core, and are written onto the root element at runtime —
there is no CSS copy to drift from the TypeScript. Preferences are stored
server-side so they follow a person between laptop and phone, mirrored into
localStorage only so the pre-paint script can avoid a white flash. Status
colours stay fixed regardless of accent: if "at risk" re-tinted to whatever
someone picked, the signal would be gone.
Seed data is public research, every record carrying a confidence grade and a
source URL. No email addresses are seeded or inferred — none are published, and
guessing them from a name and a domain is unreliable and rude. Authorship is
not promoted to employment: contributors, residency participants and alumni are
recorded as what the evidence actually shows, and a name that could not be
sourced at all is listed as unresolved rather than invented.
Three defects found and fixed by actually running it rather than assuming:
1. The seed was not idempotent. onConflictDoNothing() with no target is a no-op
without a matching unique constraint, so a second run duplicated 27
contacts. There is deliberately no unique index on (account, name) — two
people at one company can share a name — so idempotency is enforced in the
seed instead of by bending the schema.
2. /capacity scrolled sideways on a phone. Grid items default to
min-width:auto and `truncate` sets nowrap, so a long title became
unshrinkable content and widened the track. Fixed with min-w-0 on every
truncating grid child.
3. The idle-capacity alert silently failed to fire at exactly 80% utilisation,
losing a float comparison against a 0.2 threshold. Moved to 0.15, which is
also a more sensible line for "worth attention".
The worked example is tuned to teach rather than to flatter: 70% sold at a 53%
markup lands at +6.7% margin with 20% still idle, so both the healthy number
and the alert are visible. Drop the sold share to 55% and the same block goes
underwater — that sensitivity is the argument for the product.
Verified in a real browser at 393px and 1440px, light and dark: zero horizontal
overflow on every route, zero console errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>