๐Ÿงญ

Design language

Notion, faithfully. Warm ink, small radii, quiet chrome, instant hover.

๐Ÿ“
Every value below is a token in src/app/globals.css. Nothing in a component should ever hard-code a hex, a radius, or a shadow.

0. Brand vs. chrome

Your logo is pills, gradients and saturated color. Notion is hairlines, 3px corners and muted ink. Both are right โ€” they just govern different halves of the product.

The split: Notion owns the chrome โ€” sidebar, menus, forms, settings. The brand owns the canvas and the landing page. And the three pills in your mark are already a legend: purple routes, blue services, amber data.

#6B46F5routes
#1270F8services
#FAC249data
POST /auth/loginroutes/auth.py
GET /meroutes/auth.py
POST /billing/hookroutes/billing.py
AuthServiceservices/auth.py ยท 4 refs
Usermodels/user.py
Sessionmodels/session.py
routeservicedata
๐ŸŽจ
A node is a Notion-shaped card โ€” 6px radius, hairline, card shadow โ€” with a brand pill as its type marker. That gets your logo into the product without turning the whole UI into gradients.

1. Ink, not gray

The foundation is #37352F โ€” a brown-black. Every gray in the product is that same ink at low alpha, which is why the surfaces read as paper instead of glass. A cool neutral gray anywhere in this app is a bug.

primaryHeadings, body, anything you read.
secondarySidebar rows, labels, metadata.
tertiaryPlaceholders, section labels, icons at rest.
ghostDrag handles, dividers, disabled.

2. Surfaces

Depth comes from tint, not from shadow. Shadows are reserved for things that genuinely float โ€” popovers and modals.

page
The canvas. Nothing floats above it without reason.
sidebar
Sunken tint. Signals 'navigation', not 'panel'.
raised
Cards, popovers, menus.
code
Code blocks and diffs.
hover
Ink at 5.5%. The most-used value in the system.

3. Type

Inter, tightening as it gets bigger. Body sits at 16px with 1.5 line-height and never drops below 14px for anything a user reads.

Page title ยท 40/700
Heading 1 ยท 30/600
Heading 2 ยท 24/600
Heading 3 ยท 20/600
Body ยท 16/400 โ€” the default for prose in the reading column.
UI ยท 14/400 โ€” sidebar rows, buttons, inputs, table cells.
Caption ยท 12/500 โ€” section labels and metadata.
Mono ยท 13px JetBrains Mono โ€” file paths, symbols, diffs.

4. Controls

Buttons are quiet. secondary is the workhorse โ€” a hairline box on the page. primary is rationed to one per view, and most actions are ghost.

5. Hover is the interface

Nothing shows chrome until the cursor is near it. Rows fill at 20ms; handles, menus and add-buttons fade in at 100ms. Hover the rows below โ€” and the left gutter of any block on this page.

routes/auth.py
services/billing.py
models/user.py

6. Color, used sparingly

Nine hues, each with a text value and a background tint. They label things โ€” a node type, a kind of change โ€” they never decorate.

gray
brown
orange
yellow
green
blue
purple
pink
red
๐ŸŸข
Route added โ€” POST /v1/billing/webhook
โš ๏ธ
This node has no tests covering it.
๐Ÿ”ด
Circular import between services and models.

7. Radius & elevation

Radii stay small โ€” 3px is the hover-chip value and the single most recognizable dimension in the system. Shadows are layered hairline + soft + wide, never one big blur.

card
popover
modal

8. Code

@router.post("/v1/billing/webhook")
async def stripe_webhook(request: Request):
    event = verify_signature(await request.body())
    await handle(event)
    return {"ok": True}
๐Ÿง 
The one-line rule: if a surface needs a border to be legible, the spacing is wrong.