Files
karti de33a03524 Add the web app, seed data, and user-selectable theming
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>
2026-08-12 19:16:43 -07:00

15 lines
872 B
XML

<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<style>
:root { --ink: #09090b; --paper: #ffffff; }
@media (prefers-color-scheme: dark) { :root { --ink: #fafafa; --paper: #09090b; } }
</style>
<path d="M7.4 8.6 5.1 3.9c-.2-.5.3-1 .8-.8l5.2 2.2z" fill="var(--ink)"/>
<path d="M24.6 8.6 26.9 3.9c.2-.5-.3-1-.8-.8l-5.2 2.2z" fill="var(--ink)"/>
<path d="M16 5.2c6.3 0 11.2 4.3 11.2 10.4 0 6.5-5 11.2-11.2 11.2S4.8 22.1 4.8 15.6C4.8 9.5 9.7 5.2 16 5.2z" fill="var(--ink)"/>
<ellipse cx="11.6" cy="13.4" rx="1.5" ry="1.8" fill="var(--paper)"/>
<ellipse cx="20.4" cy="13.4" rx="1.5" ry="1.8" fill="var(--paper)"/>
<rect x="11.3" y="17.6" width="9.4" height="6.2" rx="3.1" fill="var(--paper)"/>
<ellipse cx="14.2" cy="20.7" rx="1.15" ry="1.5" fill="var(--ink)"/>
<ellipse cx="17.8" cy="20.7" rx="1.15" ry="1.5" fill="var(--ink)"/>
</svg>