Add mobile-first design contract and redesign brief

Scaffold design/ for the frontend's first design contract, extracted
as-built from index.html (document-as-is):
- DESIGN.md: 9-section brand brief (dark venture-CRM look, IBM Plex,
  single #3b82c4 accent) + tokens.tokens.json (DTCG, from :root + an
  inline-style census).
- BRIEF.md: the mobile-first redesign packet. Mobile = 4 surfaces
  (Grid, Pipeline, Reminders, Contacts) in a bottom tab bar; the rest
  desktop-only. Grid view-switching first-class; narrow on-the-go edit
  set (name, contacts, notes/comms/outreach log, stage, reminders) +
  create-investor, all via the canonical grid path (Contacts stays
  read-only). Includes a backend-reality callout: no field-level write
  (whole-grid versioned PUT vs the targeted log-communication path),
  stage is a separate two-call opportunities flow, pill removal has no
  undo, dedup typeahead is client-side.
- brand/ assets, inspiration/ provenance.

Wire the AGENTS.md Design line so any agent reads the contract before
UI work.
This commit is contained in:
Keysat
2026-06-18 21:50:34 -05:00
parent ab0d82ff00
commit 99404db48b
8 changed files with 538 additions and 4 deletions
+5 -4
View File
@@ -75,6 +75,7 @@ Subsystem rules live in `docs/guides/` and lazy-load in Claude Code via `.claude
- **Env:** secrets in `.env` (gitignored); names in `.env.example`. Verified names: `ANTHROPIC_API_KEY`, `SPARK_CONTROL_URL`, `SPARK_CONTROL_VERIFY_TLS`, `QDRANT_URL`, `X_API_KEY`, `CRM_DB_PATH`, `CRM_DEV_DB_PATH`. Also used: `CRM_SECRET_KEY` (beta/prod), `CRM_HOST`/`CRM_PORT`, `CRM_DATA_DIR`; digest mailer: `CRM_DIGEST_SENDER` (DWD impersonation sender) + `SMTP_HOST`/`SMTP_PORT`/`SMTP_SECURITY`/`SMTP_FROM`/`SMTP_USERNAME`/`SMTP_PASSWORD` (SMTP fallback); daily digest (Phase B): `CRM_DIGEST_ENABLED` + `CRM_DIGEST_SEND_HOUR` **only seed the first-boot default** — the live control is the DB policy (`app_settings.digest_policy`, set in Settings → Admin).
- **Config placement:** operational/feature toggles live in the **admin panel**, DB-backed via `app_settings` (read-merge through a `load_*_policy(conn)` helper shared by the API + any scheduler; precedence DB-row → env-seed → default), so they're discoverable and take effect live. Reserve StartOS actions / env for **secrets and deploy-time config** (SMTP creds, API keys, DWD sender). Precedent: `digest_policy` (`GET/PATCH /api/admin/digest/policy`), `fundraising_backup_policy`.
- **Agent/bot API access — three roles now (`admin`/`member`/`bot`).** `require_admin` is the only hard gate; everything else is "authenticated" (member, admin, *and* bot all pass). The **`bot` role** (added v0.1.0:89) is authenticated-but-never-admin: `require_bot_or_admin` gates agent-facing endpoints (e.g. `/api/intake/email-proposals*`) so a bot credential reaches *only* what it needs, never user-management/settings/security. Provision it via Settings → Admin edit-user dropdown (kept out of the teammate-invite form). **Two axes to keep separate as more agent capability lands:** the role controls *reach* (which endpoints); the per-feature human draft→approve gate controls *autonomy* (acting unattended). Money/merge/delete mutations stay behind the approval gate regardless of role. Don't build a finer capability/scope system until real NL-mutation endpoints exist to scope against.
- **Design:** before building or changing any user-facing UI, read `design/DESIGN.md` and `design/tokens.tokens.json` and conform to them. A **mobile-first redesign** is in flight — read `design/BRIEF.md` before any responsive/layout work. (Note: inline `style={{}}` objects can't respond to media queries; responsive layout belongs in the CSS `<style>` block.)
- **Commit style:** imperative subject, concise body explaining the *why*; put the package version in the subject (`… (v0.1.0:NN)`) for shippable changes. **No AI co-author / attribution trailers** — commits are authored by the user.
## Always
@@ -106,13 +107,13 @@ Subsystem rules live in `docs/guides/` and lazy-load in Claude Code via `.claude
## Current state
_Phase 0 + Phase 1 built; **box live at v0.1.0:94; repo at v0.1.0:94** (reminders W1 + NL-query W2 deployed 2026-06-18; v94 = NL-query matched-only fix). **The fundraising grid + email capture is the canonical system of record.** Active thread: **W2 natural-language query** (backend + Matrix Q&A live; web "Ask" box next). Deploy/feature history: git log + `start9/0.4/startos/versions/`; longer-term backlog/debt: `ROADMAP.md` / `EVALUATION.md`._
_Phase 0 + Phase 1 built; **box + repo live at v0.1.0:94** (reminders W1 + NL-query W2 deployed 2026-06-18; v94 = NL-query matched-only fix). **The fundraising grid + email capture is the canonical system of record.** Active thread: **W2 natural-language query** backend + Matrix Q&A live; web "Ask" box is the last piece. Feature/deploy history: git log + `start9/0.4/startos/versions/`; longer-term backlog/debt: `ROADMAP.md` / `EVALUATION.md`._
- **W2 — natural-language query (read-only): BACKEND + MATRIX Q&A LIVE (deployed v0.1.0:93, 2026-06-18); web "Ask" box next.** `backend/nl_query/` — 12 curated parameterized queries + a slot validator (the trust boundary; no generic SQL) + a **local-Qwen** translator (question→{intent,slots} via Spark Control; nothing leaves the box, **no Claude, no redaction** — the simplification Grant chose). `POST /api/query/nl` (also accepts direct `{intent,slots}`) + `GET /api/query/catalog`, `require_bot_or_admin`, audited (`entity_type='nl_query'`) — **live on the box** (verified 400/200 post-install). Soft-delete-correct per table (gotcha: `fundraising_*` has **no `deleted_at`**`graveyard` is the axis; emails via a live `eam` sighting). Guide: `docs/guides/nl-query.md`. **Step 5 (Matrix Q&A) DONE + DEPLOYED** — thin client in `backend/matrix_intake/query.py` (trigger grammar + answer rendering) + `crm_client.nl_query` + `bot.py` wiring, read-only (no approval gate), tested in `test_query.py`. **Two entry points (room-per-purpose model):** a **dedicated Q&A room** (`MATRIX_QUERY_ROOM=!RGlJEObVaIUtUVcHtx:matrix.gilliam.ai`) where every message is a question, **and** the `?`/`@bot` trigger in the intake room as a cross-room convenience. Bot rebuilt + running on the Spark (logs: `answering questions in room …`). **End-to-end verified from inside the bot container** (3 questions → correct intents, live box, no errors; `investors_cold` hits the 500-row cap so Matrix shows 30 + a refine note). **Remaining: the actual in-room Matrix smoke (a human typing a question) — not yet done.** **Matched-only fix (v0.1.0:94, LIVE 2026-06-18):** `comms_by_user` + `email_counts_by_user` were counting/listing the user's *entire* captured sent corpus, not just investor-linked email (missing the `EXISTS email_investor_links` gate that `recent_emails`/`query_email_activity` use) — fixed + regression-tested, deployed to the box in v94. **Next: step 4 web "Ask" box (Communications tab)** the last thin client.
- **W2 — NL query (read-only): LIVE on the box** (backend + Matrix Q&A shipped v93; matched-only fix v94). Curated parameterized catalog + slot validator (trust boundary; no generic SQL) + **local-Qwen** translator (`backend/nl_query/`; nothing leaves the box, no Claude/redaction). `POST /api/query/nl` + `GET /api/query/catalog` (`require_bot_or_admin`, audited). **Matrix Q&A client deployed** (`backend/matrix_intake/query.py` + `crm_client.nl_query`, read-only, no approval gate): **dedicated Q&A room** `!RGlJEObVaIUtUVcHtx:matrix.gilliam.ai` (every message is a question) **+** the `?`/`@bot` trigger in the intake room (cross-room convenience). Email/comms intents are **matched-only** (investor-linked email only — v94 fixed `comms_by_user`/`email_counts_by_user` which had counted the whole sent corpus). Guides: `docs/guides/nl-query.md` + matrix-intake. **Remaining: (a) the actual in-room Matrix smoke — a human typing a question (not yet done); (b) step 4 web "Ask" box (Communications tab), the last thin client.**
- **W1 — reminders & follow-ups: LIVE (deployed v0.1.0:93, 2026-06-18).** First-class tickler tied to the grid (migration `0006` — applied cleanly on the box per logs; CRUD `GET/POST/PATCH/DELETE /api/reminders`; derived `reminder_status` grid column; Reminders page + dashboard card + digest section; the `last_activity_at` recency rollup that W2 reuses). `0006` was verified up/down against a copy of `crm.db` before install. Deferred **W1b** = nurture-gap auto-suggested reminders.
- **W1 — reminders & follow-ups: LIVE (shipped v93).** First-class tickler tied to the grid (migration `0006`; CRUD `/api/reminders`; derived `reminder_status` grid column; Reminders page + dashboard card + digest section; the `last_activity_at` recency rollup W2 reuses). Deferred **W1b** = nurture-gap auto-suggested reminders.
- **Done & live (detail in git log / ROADMAP):** email-proposal Matrix review + `bot` role (box v91); grid-driven Pipeline (v88); Matrix intake bot (Spark `matrix-intake` container); Gmail capture (DWD) + propose→approve + daily digest; Thesis Workshop + Architect (Claude, dual-approval); outreach drafts + radar. All draft-only.
- **Tests:** **35/35 backend green** (`python3 backend/run_tests.py`; +`nl_query/` + matrix `test_query.py` suites), `py_compile` clean; render-smoke gates `make`.
- **Tests:** **35/35 backend green** (`python3 backend/run_tests.py`), `py_compile` clean; render-smoke gates `make`.
- **Next (priority order):** 1) **in-room Matrix smoke** of the Q&A room (type a real question; confirm the answer renders well on mobile — broad questions like "cold investors" hit the 500-row cap → 30 shown + refine note) + the intake `?`/`@bot` trigger; 2) **W2 step 4** web Ask box (last NL-query client); 3) **W3** bot grid-mutations behind the Matrix approval gate (local-Qwen parse); 4) **W1b** nurture-gap reminders; 5) Grant + Jonathan freeze v2.0 canonical; 6) in-room smoke of the intake disambiguation numbered-pick grammar; then P2 debt (reports comms-aggregate soft-delete sweep, `?limit=abc` crash, auth regression test, oversized StartOS icon).
- **Open / risks:** W2 translation only **happy-path-validated** (typos/ambiguous/no-match phrasings shake out in live use); **Claude/Architect path still unverified live on the box**; v2.0 reserve-asset spine is the *working approved* spine but **not canonical** (needs dual sign-off); doc drift — `crm-overview.md` + `EVALUATION.md` still call `lp_profiles` live.
+247
View File
@@ -0,0 +1,247 @@
# Design brief — Ten31 CRM mobile-first redesign
*The input packet for a Claude Design (or equivalent) round-trip. Goal: make the phone a
first-class, **preferred** surface for the Ten31 CRM without losing the existing look. This
is a **layout / information-architecture / interaction** redesign — the visual language is
captured in `design/DESIGN.md` + `design/tokens.tokens.json` and is **preserved**. Posture:
**preserve-but-refine** — keep the brand DNA; small mobile-warranted tweaks (type scale,
density, touch sizing) are welcome, a visual reskin is not.*
---
## 0. The one instruction that matters most
**Preserve the visual language; redesign only layout, navigation, and touch interaction.**
The current app is already a coherent, deliberate dark venture-CRM look (see `DESIGN.md`).
Do **not** reinvent the palette, typography, or component styling. Where mobile genuinely
warrants it, you may refine — bump body type from 13px toward 1516px, loosen touch density,
add bottom-sheet patterns — but the colors, the IBM Plex faces, the bordered-panel +
tinted-badge idiom, and the single `#3b82c4` accent stay.
## 1. Goal
The Ten31 CRM is the fund's system of record (~150 LPs, 250+ prospects, the capital-raise
pipeline), used by a ~5-person team. Today it's desktop-first; the team increasingly works
from phones — before/after investor meetings, on the move. Make **mobile the primary,
preferred surface**: every common on-the-go task is thumb-reachable and fast, and desktop
becomes the wide-screen enhancement rather than the baseline.
**Mobile is a focused subset, not the whole app** (decided 2026-06-18). Mobile carries only
the on-the-go core; everything else stays desktop-only.
- **Mobile surfaces — the only four:** **Fundraising Grid** (with fast switching between its
saved *views*), **Pipeline**, **Reminders**, **Contacts**. Every other screen — Dashboard,
Thesis, Thesis Workshop, Outreach, Communications, Email Capture, System Status, Feedback,
Instructions, Settings — is **desktop-only and simply absent** from the mobile UI (keep only
a minimal account/logout control).
- **Mobile editing — core records + quick capture (expanded 2026-06-18):** read everything;
editable on mobile is **investor name**, **contacts (name + email)**, **notes / communication
/ outreach log** (logging activity, not composing/sending — see §3a Backend reality),
**pipeline stage**, and **reminders***and* **creating a new investor**
(name + one or more contacts + type/stage). Still **desktop-only**: commitments/amounts, the
full 20+ column set, column structure, bulk ops, and CSV. So mobile is "create and manage the
core investor record + log activity," not the full spreadsheet.
- **Create/edit investors go through the Grid — the canonical write path — never Contacts.**
Per the app's model, the `fundraising_*` grid is the system of record (investor row → contact
"pills" → commitments) and the **Contacts tab is a read-only directory auto-populated from
it**. So the **"+ add investor"** entry point and all name/contact/email edits live on the
**Grid** (its card list and detail); **Contacts stays read-only** on mobile — do not put an
add/edit affordance there.
- **Guard against duplicate investors on create.** Adding an investor from a phone is a
dupe-generation risk; the app already has entity resolution/merge. The mobile "+ add
investor" flow should **check for an existing match first** (search-as-you-type on name)
before creating a new row.
- **The Grid is card/detail on a phone, never a spreadsheet.** A row is one investor; mobile
shows an investor card list → full-screen detail with the editable set above, plus a create flow.
## 2. Layout (global, mobile-first)
- **Base = mobile; enhance up.** Author layout for a 375px column first, then add `min-width`
breakpoints for tablet/desktop. (Implementation note for later: the app's ~1300 inline
`style={{}}` objects can't respond to media queries — responsive layout must live in the
CSS `<style>` block / utility classes. The design tool doesn't need to solve this, but the
brief should assume layout, not inline style, carries responsiveness.)
- **Navigation → a 4-tab bottom bar; everything else is desktop-only.** Replace the 250px
sidebar with a **bottom tab bar of exactly four**: **Grid · Pipeline · Reminders ·
Contacts**. The other ten destinations are **not** present on mobile — there is no "More"
feature menu. Keep only a minimal **account control** (e.g. a top-bar avatar/menu) for
profile + logout. The bar respects `env(safe-area-inset-bottom)`.
- **Grid views are a first-class mobile control (high priority — second only to the tabs).**
The Grid's saved *views* (Main, Follow-up, All Investors, … and a growing set of
filter-based cuts) are how the team reads the investor list different ways. Surface a **view
picker at the top of the Grid screen** — a tappable current-view header that opens a
**bottom-sheet list of views**. Use a sheet/dropdown, not a fixed segmented control, because
the set of views grows over time. Switching a view re-filters the card list in place.
- **Overlays → bottom sheets.** Today's centered modal (500px) and right slide-over (400px,
which overflows a phone) become **drag-to-dismiss bottom sheets** or full-screen detail
views on mobile.
- **Touch + safe areas.** 44px minimum touch targets; sticky bottom nav respects
`env(safe-area-inset-bottom)`; content gets bottom padding so the nav never overlaps it.
## 3. Per-screen briefs
Take the design tool through these **one screen at a time** (mobile re-layout is screen-by-
screen, not a global token swap). Ordered by value and difficulty.
### 3a. Fundraising Grid — the crux (do first)
The core feature and system of record: a 20+ column, ~3,000px-wide editable table. A row =
**one investor** (contact "pills" + per-fund commitments). On a phone it is a **card list**,
never a wide table.
- **View switching, up top (high priority):** a tappable current-view name → **bottom-sheet
view picker** (Main / Follow-up / All Investors / … a growing set), plus search. Switching
re-filters the card list in place.
- **Card:** ~5 at-a-glance fields — **name · type badge · committed amount · pipeline stage ·
last contact**. Tap → **full-screen investor detail** (today's slide-over, promoted).
- **Detail + edit:** the full field set is grouped into sections and **read-only**, *except*
the editable set: **investor name**, **contacts (name + email — the contact "pills")**,
**notes / communication / outreach** (a text area or "log a note" entry), **pipeline stage**
(a picker / segmented control), and **set/update a reminder** (date + note). Each edit happens
in a **bottom sheet**, one field at a time — no spreadsheet grid. Commitments/amounts and the
rest of the columns stay read-only on mobile.
- **Add investor (`+` on the Grid):** a create flow capturing **investor name + one or more
contacts (name, email) + type + stage** — the minimum to start a record; the rest is filled
later on desktop. **Search-as-you-type on name first** and offer existing matches before
creating, so a phone-added investor doesn't duplicate one already in the grid.
- The full multi-column spreadsheet (commitments/amounts, column reorder, bulk/CSV) stays
**desktop-only**.
> **Backend reality (read before designing the edit interactions — the write layer is not
> what "edit a field in a sheet" implies):**
> - **There is no field-level write. The grid is one JSON blob saved wholesale.**
> `PUT /api/fundraising/state` takes the *entire* grid and rejects with **409** if the global
> `version` moved under you (5 people edit live). So a naive "edit one field" = load the whole
> grid → mutate one row → PUT it all back → race everyone else. **Mobile single-investor edits
> (name, pills, add-investor, log-note) should instead go through the targeted, server-side,
> one-row path** `POST /api/fundraising/log-communication` (it finds/creates a single row,
> appends a note, and can create a new investor + first contact in one call via
> `create_investor_if_missing`, with no whole-grid version race) — or a new narrow per-row
> PATCH. Do **not** model these as whole-grid saves. The Matrix bot already uses this path.
> - **"Pipeline stage" is not a grid field** — it lives on the separate `opportunities` table and
> editing it is a **two-call flow with a precondition**: the row must first be *linked* to a
> pipeline opp (`POST /api/fundraising/pipeline/link`, which **requires ≥1 contact on the row**),
> *then* `PATCH /api/opportunities/{id}/stage`. The grid only *displays* stage read-only. So the
> detail-sheet "change stage" control needs a "not in pipeline yet → add to pipeline" state, and
> it shares the *same* opportunities endpoint as the Pipeline tab (3c) — consistent with that, not
> with the grid blob.
> - **Removing a contact pill has no tombstone/undo** — the `fundraising_*` tables are rebuilt on
> every save; the JSON blob is canonical. Don't promise soft-delete/undo semantics for pill
> removal (unlike comms/reminders, which *are* soft-deleted).
> - **Dedup typeahead is client-side** — filter the already-loaded rows; there is no investor-search
> endpoint, and the app's `entity_merge` is an admin-only *after-the-fact* reconciliation, not a
> create-time guard.
> - **"Notes / communication" = the `log-communication` path above (immediate write).** **Outreach
> *composition*** is a different, **gated** feature (agent drafts → human edits → human sends; the
> Outreach screen is desktop-only) — only the investor's outreach *log/notes* belong on mobile.
> - All of these are **plain-authenticated immediate writes** — a human **member** can do them on a
> phone; there is no draft→approve gate on a *human's own* edit (that gate is for *agent*-originated
> actions). "Agents draft, humans send" constrains the bot, not this UI.
### 3b. Contacts — lowest-risk transform (good pattern validator)
A **read-only** per-person directory (auto-populated from the Grid — no create/edit here),
today a table + tabs (All / Investors / Prospects) + a detail slide-over.
- **Mobile pattern:** a **list of contact rows** (initial/avatar · name · organization · type
badge · last contact) with the tabs as a top segmented control and search pinned → tap →
**full-screen read-only detail** (contact info, linked investor, communication history).
- Pure browse→detail, no edit — validate the list+detail+sheet pattern here before the Grid.
### 3c. Pipeline (Kanban) — re-think the horizontal board
Today: horizontal kanban columns, one per stage (count + total per column), tap card → edit.
Horizontal columns don't work on a phone.
- **Mobile pattern (lead option):** **swipe between stage columns** — one full-width stage at
a time, snap-scrolling, a stage indicator/segmented control at top, vertical list of
investor cards within each stage. (Alternative to weigh: a vertical accordion of collapsible
stages.) Tap a card → the same investor detail sheet as the Grid. **Editing pipeline stage**
is one of the mobile-editable fields, so make stage changeable here too (a stage picker on the
card, or drag/swipe a card to the next stage).
### 3d. Reminders — a primary tab + an edit surface
A follow-up/tickler list tied to investors (one of the mobile-editable areas).
- **Mobile pattern:** a **list grouped by urgency** (overdue → due-soon → later), each row
showing title, investor, due date (urgency-colored: overdue `#e06c6c`, due-soon `#e0b341`),
and assignee. **Quick actions** (done / snooze / edit) via swipe or a row menu; a **`+`**
creates one. Tap → a **bottom-sheet edit** (title, investor, due date, note, assignee). This
is also the editor reached from an investor detail's "set a reminder" action.
## 4. Brand description (~120 words)
Ten31's CRM is a *trustworthy instrument* — a dense, dark, data-forward venture-fund
workspace for a small team handling sensitive LP relationships. The voice is serious,
discreet, and precise: cool blue-greys, a single confident blue accent, IBM Plex's
engineered-but-humane type, monospace for every number and date. It feels like a well-made
financial terminal, not a consumer app — restraint and legibility over decoration. The
mobile version should feel like the *same instrument in your pocket*: calmer and roomier for
touch, but unmistakably the same tool. It is **not** playful, colorful, skeuomorphic, or
trend-chasing; **not** a second bright color or a borderless/flat reskin. Quiet confidence,
information density made thumb-friendly.
## 5. Inputs to bring to the cloud tool
- **Point at:** `frontend/` (the single `index.html` holds the whole UI — point at the
directory, not the repo root).
- **Upload:** `design/DESIGN.md`, `design/tokens.tokens.json`, `design/brand/ten31-logo-white.svg`,
`design/brand/ten31-favicon.svg`, and **screenshots of each screen** below at desktop width
(Fundraising Grid, Contacts, Pipeline, Dashboard, a modal, the slide-over) so the tool sees
the as-built look it must preserve.
- **Web-capture:** the running app URL if convenient (it's behind auth on the Start9 box; a
set of screenshots is the reliable path).
## 6. Prompt blocks (paste into the design tool, one per screen)
**Global frame (paste first):**
> Redesign this dark venture-CRM web app to be mobile-first and mobile-preferred. **Preserve
> the existing visual language exactly** (see the uploaded DESIGN.md + tokens: dark blue-grey
> palette, single `#3b82c4` accent, IBM Plex Sans/Mono, bordered panels, tinted badges) —
> change only **layout, navigation, and touch interaction**. Small mobile-warranted refinements
> (body type 13→1516px, looser touch density, bottom sheets) are welcome; no visual reskin.
> Mobile carries only **four surfaces in a bottom tab bar — Grid, Pipeline, Reminders,
> Contacts**; every other screen is desktop-only and absent here (keep just a minimal top-bar
> account/logout menu). Convert centered modals and the right slide-over into **drag-to-dismiss
> bottom sheets / full-screen detail views**. 44px touch targets, safe-area-aware sticky bottom
> nav. Design for a 375px phone first.
**Fundraising Grid:**
> This is a 20+ column editable data table; each row is one investor (contact pills + per-fund
> commitments) — unusable as a wide table on a phone. Design a mobile **investor card list**:
> card shows name, type badge, committed amount, pipeline stage, last contact. At the top, a
> **tappable current-view name that opens a bottom-sheet list of saved views** (Main, Follow-up,
> All Investors, and a growing set of filtered cuts) plus search; switching a view re-filters
> the list. Tapping a card opens a **full-screen detail**: most fields read-only **except** the
> editable set — **investor name**, **contacts (name + email)**, **notes/communication/outreach**,
> **pipeline stage**, and **set a reminder** — each edited in a bottom sheet. Add a **`+` to
> create a new investor** (name + one or more contacts with name/email + type + stage), with
> search-as-you-type on name to surface existing matches before creating (avoid duplicates).
> Commitments/amounts and the full column set stay read-only / desktop-only.
**Contacts:**
> A read-only people directory (no create/edit here). Today it's a table with All/Investors/
> Prospects tabs and a detail panel. Design a mobile **contact list** (initial, name,
> organization, type badge, last contact), tabs as a top segmented control, search pinned;
> tapping a row opens a **full-screen read-only detail** with contact info, linked investor,
> and communication history.
**Pipeline (Kanban):**
> A kanban board of pipeline stages (one column per stage, each showing count + total),
> cards are investors. Horizontal columns don't fit a phone. Design a **swipe-between-stages**
> mobile view: one full-width stage at a time with snap-scrolling and a stage segmented
> control at top, a vertical list of investor cards within each stage; tapping a card opens
> the investor detail sheet. Stage is editable here (a stage picker on the card, or drag a card
> to the next stage). Also sketch a vertical-accordion alternative for comparison.
**Reminders:**
> A follow-up/tickler list tied to investors. Design a mobile **reminders list grouped by
> urgency** (overdue, due-soon, later); each row shows title, investor, due date (urgency-
> colored), assignee, with quick actions (done / snooze / edit) via swipe or a row menu and a
> `+` to create. Tapping a row opens a **bottom-sheet edit** (title, investor, due date, note,
> assignee).
---
## 7. After the round-trip (Phase C reminder)
Export the **"Handoff to Claude Code" bundle** + screenshots, drop them in
`design/_imports/<date>/`, then distill back into the contract: update `DESIGN.md` §8
(Responsive behavior) with the real mobile-first system, add mobile component states (bottom
nav, sheets, card list) to §4, and bump the mobile type scale in `tokens.tokens.json` if it
changed. The gap between the new contract and the current `index.html` is the implementation
backlog → capture it to `ROADMAP.md` (incl. the inline-style→CSS migration that makes
responsive layout possible at all). Do not silently reskin existing code in the same pass.
+125
View File
@@ -0,0 +1,125 @@
# Ten31 CRM — Design contract (DESIGN.md)
*The durable brand brief. Any agent (or person) building or changing user-facing UI reads
this file and `design/tokens.tokens.json` first and conforms to them. Extracted **as-built**
from `frontend/index.html` on 2026-06-18 (document-as-is); this is a faithful record of the
look that grew in the code, not an aspirational redesign. The mobile-first redesign in
`design/BRIEF.md` builds **on top of** this contract — it changes layout/navigation/touch,
not the visual language below.*
## 1. Visual theme
A dense, professional, **dark** venture-CRM workspace — calm, data-forward, slightly
"terminal/financial." Cool blue-greys throughout, a single saturated blue as the only vivid
accent, and IBM Plex's engineered-but-humane character. Monospace (IBM Plex Mono) carries
all numbers, dates, and codes, reinforcing the data-tool feel. The mood is *trustworthy
instrument*, not consumer-app playful: restraint, legibility, and information density over
decoration. It is used by a ~5-person fund team handling sensitive LP data, so it should
read as serious and discreet.
## 2. Color palette
Canonical values live in `design/tokens.tokens.json`. Summary:
- **Backgrounds (darkest → lightest):** base `#0b1118` → panel `#111a27` → elevated
`#152233` → hover `#1b2a3a`. Recessed input/table-header surface: `#0d1622`.
- **Borders:** default `#263548` (the workhorse — borders, dividers, every table grid line),
strong `#35506a` (hover/emphasis).
- **Text:** primary `#e5edf5`, secondary `#c7d3e0`, muted `#8ea2b7` (most common),
subtle `#70859b`.
- **Accent (the one brand color):** `#3b82c4`. Hover/gradient-end `#2f6ea9`, tint
`#3b82c422`, on-tint text `#93c5fd`. Used for primary actions, active nav, links, focus.
- **Semantic:** success `#10b981`, warning `#f59e0b`/`#fcd34d`, due-soon `#e0b341`,
danger `#dc2626`/`#e06c6c`, error-text `#fca5a5`. Badges render semantic color at low
alpha for the fill with a lighter tint for the text.
White (`#ffffff`) appears only as text on accent fills and in the brand mark.
## 3. Typography
- **Families:** `IBM Plex Sans` (UI/body), `IBM Plex Mono` (numbers, dates, badges, logs,
nav icons). Loaded from Google Fonts; weights 400/500/600/700 sans, 500/600 mono.
- **Scale (as-built):** 11px micro/table-header/badge · 12px help/meta · **13px body / table
/ inputs (desktop base)** · 14px nav · 16px section title · 18px modal title · 20px page
title · 24px login title & KPI value.
- **Treatments:** global letter-spacing `0.01em`; table headers uppercase with `0.08em`
tracking; badges uppercase `0.5px`; numbers use `font-variant-numeric: tabular-nums`.
- **Mobile note:** 13px body is comfortable for desktop and tight on a phone — the redesign
bumps the mobile base toward 1516px. See `BRIEF.md`.
## 4. Component styling
- **Buttons:** primary = top-to-bottom gradient `#3b82c4 → #2f6ea9`, white text, radius 6px,
padding 10×16, lift on hover (`translateY(-1px)` + soft blue shadow). Secondary = flat
`#1b2837`. Danger = `#dc2626`.
- **Cards / sections:** panel `#111a27`, 1px `#263548` border, radius 8px, padding 1620px,
composite drop shadow + inset top highlight. Hover lifts 1px and strengthens the border.
- **Tables:** recessed header `#0d1622`, uppercase muted 11px headers, sticky header +
sticky first column, 1px grid lines, row hover `#172435`, sticky aggregate footer.
- **Forms:** inputs on `#0d1622`, 1px border, radius 6px, accent focus ring; label 13/500,
help 12px muted, error 12px `#fca5a5`.
- **Badges:** radius 4px, 11px/600 uppercase, low-alpha semantic fill + tinted text.
- **Overlays:** modal (centered, radius 12, max-width 500, blurred backdrop) and slide-over
(right drawer, 400px) for detail/edit.
- **Other:** kanban cards (radius 6), toasts (bottom-right), accent spinner, shimmer
skeletons, left-marker timeline for activity feeds.
## 5. Layout
Desktop shell = **fixed 250px left sidebar + flexible main content** with a top header bar
(page title + user). Content max-width 1400px, padding 20px. Primary content patterns:
wide data table (Fundraising Grid), KPI grid + timelines (Dashboard), kanban columns
(Pipeline), list + detail drawer (Contacts/Reminders), two-column (Thesis). Auto-fit grids
are used for KPI cards (`minmax(180px,1fr)`) and kanban columns (`minmax(300px,1fr)`).
## 6. Depth / elevation
Elevation is built from three stacked cues, not just one shadow: (a) a **layered radial-
gradient page background** (two soft blue glows top-left and top-right over `#0b1118`),
(b) panel-color steps (base → panel → elevated), and (c) composite drop shadows with a
1px inset white top-highlight (`inset 0 1px 0 #ffffff07-08`) that gives panels a faint lit
edge. Hover states lift elements 1px and deepen the shadow. Keep this restrained, cool, and
low-contrast — depth should be felt, not seen.
## 7. Do's and don'ts
- **Do** keep `#3b82c4` as the only vivid accent; everything else stays in the blue-grey
range. **Don't** introduce a second bright hue for emphasis — use weight, tint, or the
semantic colors.
- **Do** use IBM Plex Mono for every number/date/code. **Don't** set tabular data in the
sans face.
- **Do** reach for tinted-fill + tinted-text badges for status. **Don't** use solid
saturated fills for status chips.
- **Do** keep borders at `#263548` and lean on them for structure (this is a grid-lined,
bordered aesthetic). **Don't** switch to borderless/shadow-only cards.
- **Do** preserve the restrained 1px hover-lift motion. **Don't** add bouncy or long
animations (and honor `prefers-reduced-motion`, already wired).
## 8. Responsive behavior
**Current (as-built): desktop-first.** Breakpoints are `max-width` (900px, 768px); the
sidebar simply `display:none`s below 768px with no real mobile navigation; wide tables
overflow horizontally; the 400px slide-over overflows a 375px screen. A correct viewport
meta tag is present.
**Target: mobile-first, mobile-preferred — active redesign.** The team increasingly works
from phones, so mobile is becoming the primary surface. The full plan (navigation
re-architecture to a bottom tab bar, table→card transforms, bottom sheets, touch sizing,
type bump) lives in **`design/BRIEF.md`**. Update this section to describe the new
mobile-first system once that redesign lands.
## 9. Agent prompt guide
When building or changing UI here:
- Pull every color, size, radius, and space value from `design/tokens.tokens.json` — do not
hand-pick new hexes. The app still inlines many values; prefer the `:root` CSS variables
(and grow that set) over fresh literals.
- Match the established components above; if you need a new one, compose it from existing
tokens and the bordered-panel + tinted-badge idiom rather than inventing a new visual style.
- Numbers/dates/codes → IBM Plex Mono + tabular-nums. Status → tinted badge. Primary action →
the blue gradient button. Destructive → `#dc2626` and a confirm step.
- **Building anything mobile/responsive?** Read `design/BRIEF.md` first — it holds the
mobile-first layout, navigation, and interaction decisions this section will eventually
absorb.
- Reminder: inline `style={{}}` objects cannot respond to media queries — put any
responsive layout in the CSS `<style>` block (or a utility class), not inline.
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Ten31">
<rect x="2" y="2" width="60" height="60" rx="8" fill="#0b1118" stroke="#ffffff" stroke-width="2"/>
<text x="32" y="41" text-anchor="middle" fill="#ffffff" font-size="24" font-weight="700" font-family="Georgia, 'Times New Roman', serif">T31</text>
</svg>

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 722.69 280.85">
<defs>
<style>
.cls-1 {
font-family: LTCGoudyOldstylePro-Bold, 'LTC Goudy Oldstyle Pro';
font-size: 192px;
font-weight: 700;
}
.cls-1, .cls-2, .cls-3 {
fill: #fff;
}
.cls-2, .cls-4 {
stroke-width: 3px;
}
.cls-2, .cls-4, .cls-3 {
stroke: #fff;
stroke-miterlimit: 10;
}
.cls-4 {
fill: none;
}
.cls-5 {
letter-spacing: -.06em;
}
</style>
</defs>
<text class="cls-1" transform="translate(120.54 208.45)"><tspan class="cls-5" x="0" y="0">T</tspan><tspan x="120.96" y="0">en31</tspan></text>
<g>
<polygon class="cls-3" points="95.52 140.42 54.54 154.4 54.54 126.45 95.52 140.42"/>
<line class="cls-2" x1="0" y1="140.42" x2="60.54" y2="140.42"/>
</g>
<rect class="cls-4" x="97.1" y="1.5" width="527.95" height="277.85"/>
<g>
<polygon class="cls-3" points="721.15 140.42 680.16 154.4 680.16 126.45 721.15 140.42"/>
<line class="cls-2" x1="625.62" y1="140.42" x2="686.16" y2="140.42"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+17
View File
@@ -0,0 +1,17 @@
# Inspiration / reference — Ten31 CRM design
This contract was **extracted as-built** (document-as-is), so the de-facto reference is the
product's own code and brand mark, not an external inspiration set.
- **De-facto design reference:** `frontend/index.html` — the single-file React UI. The
embedded `<style>` block (`:root` vars + component CSS) and ~1300 inline `style={{}}`
objects are the source the contract was harvested from on 2026-06-18.
- **Brand mark / intended palette:** `../brand/ten31-logo-white.svg` (white wordmark),
`../brand/ten31-favicon.svg` (T31 mark), `../brand/ten31-inverted-square.png` (app icon).
White mark on dark `#0b1118` encodes the intended palette: white/light text on a dark cool
ground, with the single `#3b82c4` blue accent.
For the **mobile-first redesign** (`../BRIEF.md`), drop any phone-app screenshots whose
layout/navigation/touch ergonomics you like into this folder (e.g. mobile CRMs, finance apps
with good card/list + bottom-sheet patterns) — they inform layout only; the visual language
stays as captured in `../DESIGN.md`.
+96
View File
@@ -0,0 +1,96 @@
{
"$description": "Ten31 CRM design tokens (W3C DTCG). Extracted as-built from frontend/index.html :root + an inline-style census, 2026-06-18. The app currently inlines these values (CSS :root vars + ~1300 inline style objects); this file is the canonical source going forward. Some real values (composite shadows, the radial-gradient page background) do not map to DTCG primitives and are documented as strings.",
"color": {
"bg": {
"base": { "$type": "color", "$value": "#0b1118", "$description": "Page background (darkest layer). Also the de-facto theme color; use for a future PWA manifest theme_color." },
"panel": { "$type": "color", "$value": "#111a27", "$description": "Cards, sections, modals, sidebar, slide-over." },
"elevated": { "$type": "color", "$value": "#152233", "$description": "Elevated/hover panel state." },
"hover": { "$type": "color", "$value": "#1b2a3a", "$description": "Generic hover background." },
"input": { "$type": "color", "$value": "#0d1622", "$description": "Form input + table-header background (recessed)." }
},
"border": {
"default": { "$type": "color", "$value": "#263548", "$description": "All borders, dividers, table grid lines. The most-used non-text color." },
"strong": { "$type": "color", "$value": "#35506a", "$description": "Emphasized border / card hover border." }
},
"text": {
"primary": { "$type": "color", "$value": "#e5edf5", "$description": "Headings, primary content." },
"secondary": { "$type": "color", "$value": "#c7d3e0", "$description": "Body text, labels." },
"muted": { "$type": "color", "$value": "#8ea2b7", "$description": "Hints, metadata, table headers. Highest-frequency text color." },
"subtle": { "$type": "color", "$value": "#70859b", "$description": "Very secondary labels / inactive tabs." }
},
"accent": {
"default": { "$type": "color", "$value": "#3b82c4", "$description": "Primary action, active nav, links, focus ring. The one vibrant brand color." },
"strong": { "$type": "color", "$value": "#2f6ea9", "$description": "Accent hover / gradient endpoint." },
"soft": { "$type": "color", "$value": "#3b82c422", "$description": "Accent at ~13% alpha — tinted badge/active backgrounds." },
"light": { "$type": "color", "$value": "#93c5fd", "$description": "Accent text on dark tinted backgrounds (badges, pills)." }
},
"semantic": {
"success": { "$type": "color", "$value": "#10b981", "$description": "Money / positive values." },
"success-text": { "$type": "color", "$value": "#6ee7b7" },
"warning": { "$type": "color", "$value": "#f59e0b", "$description": "Advisor / warning." },
"warning-text": { "$type": "color", "$value": "#fcd34d" },
"due-soon": { "$type": "color", "$value": "#e0b341", "$description": "Reminder due-soon urgency." },
"danger": { "$type": "color", "$value": "#dc2626", "$description": "Destructive action button." },
"danger-soft": { "$type": "color", "$value": "#e06c6c", "$description": "Overdue / error emphasis." },
"danger-text": { "$type": "color", "$value": "#fca5a5", "$description": "Inline error text." }
},
"constant": {
"white": { "$type": "color", "$value": "#ffffff", "$description": "Text on accent fills; brand mark." }
}
},
"font": {
"family": {
"sans": { "$type": "fontFamily", "$value": ["IBM Plex Sans", "Avenir Next", "Segoe UI", "sans-serif"] },
"mono": { "$type": "fontFamily", "$value": ["IBM Plex Mono", "monospace"], "$description": "Numbers, dates, badges, logs, nav icons." }
},
"size": {
"xs": { "$type": "dimension", "$value": "11px", "$description": "Table headers, badges, micro-labels." },
"sm": { "$type": "dimension", "$value": "12px", "$description": "Help text, metadata." },
"md": { "$type": "dimension", "$value": "13px", "$description": "Body / table cells / inputs (current desktop base). NOTE: bump toward 1516px for mobile body — see BRIEF.md." },
"lg": { "$type": "dimension", "$value": "14px", "$description": "Nav items." },
"xl": { "$type": "dimension", "$value": "16px", "$description": "Section titles." },
"2xl": { "$type": "dimension", "$value": "18px", "$description": "Modal titles." },
"3xl": { "$type": "dimension", "$value": "20px", "$description": "Page/header title." },
"4xl": { "$type": "dimension", "$value": "24px", "$description": "Login title, KPI values." }
},
"weight": {
"regular": { "$type": "fontWeight", "$value": 400 },
"medium": { "$type": "fontWeight", "$value": 500 },
"semibold": { "$type": "fontWeight", "$value": 600 },
"bold": { "$type": "fontWeight", "$value": 700 }
}
},
"space": {
"2xs": { "$type": "dimension", "$value": "4px" },
"xs": { "$type": "dimension", "$value": "6px" },
"sm": { "$type": "dimension", "$value": "8px" },
"md": { "$type": "dimension", "$value": "12px", "$description": "Most common padding/gap unit." },
"lg": { "$type": "dimension", "$value": "16px" },
"xl": { "$type": "dimension", "$value": "20px", "$description": "Sidebar/header/content padding." },
"2xl": { "$type": "dimension", "$value": "24px", "$description": "Modal padding." }
},
"radius": {
"sm": { "$type": "dimension", "$value": "4px", "$description": "Badges." },
"md": { "$type": "dimension", "$value": "6px", "$description": "Buttons, inputs, kanban cards." },
"lg": { "$type": "dimension", "$value": "8px", "$description": "Cards, nav items, sections." },
"xl": { "$type": "dimension", "$value": "12px", "$description": "Modals." },
"pill": { "$type": "dimension", "$value": "999px", "$description": "Pills, skeleton lines." }
},
"shadow": {
"$description": "Real composite shadows from the as-built CSS; kept as raw strings (multi-layer + inset highlight don't map to a single DTCG shadow token).",
"card": { "$type": "shadow", "$value": "0 14px 26px rgba(2,12,24,0.28), inset 0 1px 0 #ffffff07" },
"card-hover":{ "$type": "shadow", "$value": "0 10px 20px rgba(7,17,30,0.35)" },
"button-hover": { "$type": "shadow", "$value": "0 6px 14px rgba(12,40,68,0.35)" },
"modal": { "$type": "shadow", "$value": "0 24px 56px rgba(1,8,17,0.5), inset 0 1px 0 #ffffff08" },
"slide-over":{ "$type": "shadow", "$value": "-12px 0 32px rgba(4,10,18,0.45), inset 1px 0 0 #ffffff07" },
"toast": { "$type": "shadow", "$value": "0 10px 24px rgba(4,12,22,0.45)" }
},
"motion": {
"fast": { "$type": "duration", "$value": "120ms", "$description": "Press/transform feedback." },
"base": { "$type": "duration", "$value": "150ms", "$description": "Hover color/shadow." },
"panel": { "$type": "duration", "$value": "300ms", "$description": "Slide-over / toast entry." }
},
"_unmappable": {
"$description": "Documented-but-not-tokenized: the page background is a layered radial-gradient ('radial-gradient(1200px 600px at 15% -10%, #1a3c5e44, transparent 60%), radial-gradient(1000px 500px at 90% 0%, #27496b33, transparent 58%), #0b1118') — see DESIGN.md §Depth/elevation."
}
}