---
name: Lopifi
description: A finance-native component system. shadcn distribution, accounting-correct primitives.
export: css-tailwind
color:
  canvas: oklch(0.985 0.002 265)
  surface: oklch(1 0 0)
  surface-sunken: oklch(0.97 0.003 265)
  surface-hover: oklch(0.975 0.003 265)
  surface-selected: oklch(0.96 0.01 258)
  border-subtle: oklch(0.935 0.004 265)
  border: oklch(0.9 0.005 265)
  border-strong: oklch(0.82 0.006 265)
  text-primary: oklch(0.21 0.012 265)
  text-secondary: oklch(0.48 0.01 265)
  text-tertiary: oklch(0.52 0.008 265)
  text-disabled: oklch(0.72 0.006 265)
  text-inverse: oklch(0.985 0.002 265)
  accent: oklch(0.5 0.155 258)
  accent-hover: oklch(0.45 0.155 258)
  accent-active: oklch(0.41 0.15 258)
  accent-subtle: oklch(0.96 0.025 258)
  accent-border: oklch(0.88 0.06 258)
  accent-fg: oklch(0.995 0 0)
  positive: oklch(0.52 0.13 155)
  positive-subtle: oklch(0.95 0.035 155)
  positive-flash: oklch(0.9 0.07 155)
  negative: oklch(0.52 0.19 25)
  negative-subtle: oklch(0.955 0.03 25)
  negative-flash: oklch(0.905 0.07 25)
  warning: oklch(0.68 0.15 75)
  warning-subtle: oklch(0.96 0.045 75)
  warning-text: oklch(0.52 0.115 70)
  viz-1: oklch(0.55 0.14 258)
  viz-2: oklch(0.65 0.12 195)
  viz-3: oklch(0.62 0.13 90)
  viz-4: oklch(0.58 0.14 320)
  viz-5: oklch(0.68 0.11 45)
  viz-6: oklch(0.48 0.1 230)
typography:
  sans:
    fontFamily: Inter Variable
    fontWeight: 400
  display:
    fontFamily: Inter Variable
    fontWeight: 400
  numeric:
    fontFamily: Geist Mono
    fontWeight: 400
  body:
    fontFamily: Inter Variable
    fontSize: 14px
    lineHeight: 20px
    fontWeight: 400
  label:
    fontFamily: Inter Variable
    fontSize: 13px
    lineHeight: 16px
    fontWeight: 500
  caption:
    fontFamily: Inter Variable
    fontSize: 12px
    lineHeight: 16px
    fontWeight: 400
  title:
    fontFamily: Inter Variable
    fontSize: 20px
    lineHeight: 28px
    fontWeight: 500
  figure:
    fontFamily: Inter Variable
    fontSize: 28px
    lineHeight: 32px
    fontWeight: 500
    fontFeatureSettings: '"tnum" 1, "case" 1'
spacing:
  base: 8px
  half: 4px
  inset: 24px
  inset-compact: 16px
rounded:
  base: 24px
  xs: 4px
  sm: 8px
  md: 12px
  lg: 16px
  xl: 24px
  '2xl': 32px
---

# Lopifi — design contract

Machine-readable token contract for coding agents. **The prose specification lives in [`docs/`](docs/) and takes precedence** — this file is the extract, not the source.

## Non-negotiable rules

An agent generating Lopifi code must obey all of these.

1. **8px grid.** 4px only inside controls ≤ 32px tall. Table cells at 12px vertical are the one documented exception. Never 6, 10, 14, 18, 20, 28.
2. **24px radius, 24px inset.** Nested radius = `outer − gap`. An element flush against a 24px inset has radius `0`.
3. **Max 3 font sizes and 3 font weights per component.** Weights are 400, 500, 600 only.
4. **Two families.** Inter for every word, Geist Mono for every figure, code and identifier. A figure leaves the figure face only when it is provably solitary.
5. **`font-feature-settings: "tnum" 1, "case" 1`** on every numeric element. `"zero" 1, "ss02" 1` on reference and account numbers.
6. **One accent per view.** Focus rings and prose links are exempt.
7. **No gradients** (except a single-hue area-chart fill). No purple. No glow as an affordance. No emoji.
8. **Border at rest, shadow only when floating.** Shadows are ≥ 2 layers and hue-tinted. Dark mode adds `inset 0 1px 0 oklch(1 0 0 / 0.06)`.
9. **Money is minor-unit integers plus a currency code.** Never a float. Never `toFixed(2)`. Decimals resolved from the currency.
10. **`zero`, `null`, `n/a`, redacted, and loading are five distinct renders.** Never `value ?? 0`.
11. **Negatives:** parentheses in statements and tables, `−` (U+2212) in deltas. Never a hyphen. Alignment preserved via a reserved paren column.
12. **Sign is never conveyed by color alone.** Every signed value carries a glyph, arrow, parenthesis, or position cue. It must read in greyscale.
13. **`polarity` is required** on any component displaying a change. Up is not assumed good.
14. **Never animate a number's value.** Flash the cell background (`--*-flash`, 640ms linear); swap the digits instantly.
15. **Charts animate on first paint only.** Never on data change.
16. **Keyboard-initiated actions are motionless.** So are row hover transforms, sort, filter, and pagination.
17. **Animate `transform` and `opacity` only.** Paint only on small isolated surfaces. Never layout properties.
18. **`ease-out` on entrance. Never `ease-in`.** Interaction feedback ≤ 300ms. Exit = enter × 0.7.
19. **Entrance offset 8px, entrance scale 0.97.** Never `scale(0)`. Never 20–40px.
20. **Popovers use `transform-origin: var(--transform-origin)`.** Dialogs stay centered.
21. **`transition: all` is forbidden.** Name the properties. No inline `cubic-bezier` — use the tokens.
22. **`h-dvh`, not `h-screen`.** `size-*`, not `w-* h-*`. Fixed z-scale, no arbitrary `z-*`.
23. **`env(safe-area-inset-*)` on every fixed element.** 44px minimum touch targets via hit-slop.
24. **Gate every hover effect** behind `@media (hover: hover) and (pointer: fine)`, and give it a keyboard and touch equivalent.
25. **Never block paste.** `inputMode="decimal"` and `type="text"` on amount fields, never `type="number"`.
26. **`aria-label` on every icon-only button.** Never hand-roll keyboard or focus behavior — use Base UI or Radix, and never mix the two in one surface.
27. **Monetary text meets 7:1 contrast.** Never render a monetary value at `--text-tertiary` — the currency symbol, and the cents of a `figure-hero` / `figure-lg` display value, are the only parts of a figure that may use it.
28. **Statement rule lines:** single `1px` above a subtotal, `3px double` below a grand total, spanning the numeric column only.
29. **Never abbreviate** in tables, statements, totals, inputs, or receipts. Abbreviation is for chart axes and KPI heroes, with the exact value reachable.
30. **Every figure carries an "as of".** Every FX rate carries a timestamp.

## Accounting rules

31. **Debit and credit are two columns.** Never one signed column. The balance check is displayed, never assumed.
32. **A posted entry is never edited or deleted — it is reversed.** The delete action is absent, not disabled.
33. **Never negate a ledger amount at render time.** Components render `presentationAmount`; the sign transformation happens once, in the data layer.
34. **Contra accounts render in their natural position**, indented and parenthesized under their parent, netting to a subtotal. Never as a negative asset.
35. **Entity is a required dimension on every transaction.** Every number states its entity scope, currency, basis, and period.
36. **Never sum across currencies or entities without a stated conversion or elimination.**
37. **Translation differences go to equity (CTA); remeasurement differences go to the P&L.** Realized and unrealized FX are never combined.
38. **A missing FX rate is a blocking error, never a silent 1.0.** Rate type (spot/closing/average/historical) is required.
39. **Closed periods block posting.** Cutoff warnings state the consequence and both legitimate remedies.
40. **Every balance sheet account has a supporting schedule and a displayed tie-out.** An unsupported account renders an explicit "Unsupported" state.
41. **Roll-forwards assert opening = prior closing and closing = GL.** The difference is always visible; non-zero owns the screen.
42. **Segregation of duties is enforced, not configurable.** The preparer's approve action is absent.
43. **Approvals bind to a version.** Editing an approved object voids its approvals and shows the change to the re-approver.
44. **Duplicate detection runs before save** on bills, keyed on vendor + their invoice number, and is blocking.
45. **Evidence is required** on bills, expenses, adjusting entries, and reconciliations. Never deleted, only unlinked, and the unlink is audited.
46. **Unapplied cash is a liability.** It never reduces AR and never becomes revenue.
47. **Aging is by due date, not invoice date**, and the aging total ties to its control account.
48. **Statement rule lines carry meaning** and span the numeric column only. Statements carry the full header: entity, statement, resolved period, basis, framework, currency, scale, provenance.
49. **Non-GAAP measures appear below net income, reconciled, with every adjustment named.**
50. **Every metric ships a definition** — formula with values substituted, cohort, period, source. No metric card without one.

## Brand

Machine-readable extract of [`docs/brand/01-identity.md`](docs/brand/01-identity.md), shown at [`/brand`](/brand). One geometry module, `src/brand/mark.ts`, feeds every surface. Never redraw the mark.

```
mark        π. head rule + debit column + credit column (turns at the foot) + full stop
grid        64, stem 8, rule 7 (optical); one quarter circle, r6 inner / r14 outer, about (46, 40)
extents     x 8–57, y 10–54; rule and curve terminal share x 46; artboard viewBox "4 4 56 56"
build       four filled paths (bar, debit, credit, stop); takes currentColor
colour      mark --accent #2060b9 · wordmark --text-primary · in a lockup the stop takes the wordmark's ink
wordmark    lowercase "lopifi", Inter 600, tracking -0.025em, outlined; dotless i's with square tittles
            hung from the ascender; f hook = the mark's curve turned over
lockup      mark height = ascender; gap 18; stems 8.5 (mark) vs 8.1 (letters)
total       a single rule over a double rule, 3 units each, gap 5; the one brand device
clear space 16 units (two stems) on every side
min size    mark 16px · lockup 72px · below 16px use the tile (icon.svg)
tile        32 grid, radius 7; its own cut: rule 4, stop a whole unit clear of the curve
```

Logo rules, in addition to the rules above:

51. **The logo is still by default.** It plays only on a surface about the brand, once, on first sight, with a replay control. Never in the header, never looped.
52. **The logo animates `clip-path`, `opacity` and `transform` only.** Rule left to right (`--duration-4`), columns top to bottom (`--duration-5`), letters on the 40ms stagger (six, 240ms in all), then the stop, set down onto the baseline (`--duration-3`). `--ease-out`. Reduced motion shows the finished mark.
53. **One accent applies to the logo.** Where the view's accent is already spoken for, use the mono mark.
54. **The mark is never a letter in running text, never in a circle, never outlined, shadowed, rotated, stretched or recoloured off-palette.**
55. **The Total (single over double rule) is never decorative.** It closes a lockup and underlines a figure that has been proved.

## Motion tokens

```
--ease-out: cubic-bezier(0.23, 1, 0.32, 1)
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1)
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1)
--ease-standard: cubic-bezier(0.4, 0, 0.2, 1)
                                    (there is deliberately no --ease-in)

--duration-1: 100ms   press
--duration-2: 140ms   hover, tooltip, crossfade
--duration-3: 180ms   dropdown, popover
--duration-4: 240ms   dialog
--duration-5: 320ms   sheet, drawer
--duration-6: 400ms   chart draw-in (first paint only)
--duration-flash: 640ms

spring.snap    { duration: 0.35, bounce: 0 }
spring.gesture { duration: 0.50, bounce: 0.18 }
spring.playful { duration: 0.45, bounce: 0.22 }   bounce never exceeds 0.25

--stagger: 40ms, total capped at 240ms
```

Utilities shipped with the theme, referenced by name from installed components:

```
.press / .press-soft     scale(0.97) / scale(0.99) on :active, 100ms
.ledger-enter            opacity + 8px + 0.97, 240ms ease-out
.ledger-crossfade        opacity + 2px blur, 140ms — the sanctioned value swap
.ledger-breathe          opacity 1 ↔ 0.6, 1600ms, infinite — skeletons and pending
.ledger-flash-*          640ms linear — the only response to a number changing
```

A chart's "first paint" is the first time it is **seen**, not the first time it mounts —
gate the draw-in on `useInViewOnce`, once, never on re-entry.

## Where to look

| Question | Doc |
|---|---|
| How do I space this? | [`docs/foundations/01-grid-and-layout.md`](docs/foundations/01-grid-and-layout.md) |
| What radius? | [`docs/foundations/02-shape-and-radius.md`](docs/foundations/02-shape-and-radius.md) |
| What color? | [`docs/foundations/03-color.md`](docs/foundations/03-color.md) |
| Border or shadow? | [`docs/foundations/04-elevation-borders-shadows.md`](docs/foundations/04-elevation-borders-shadows.md) |
| What size and weight? | [`docs/foundations/05-typography.md`](docs/foundations/05-typography.md) |
| **How do I render this number?** | [`docs/foundations/06-numerics.md`](docs/foundations/06-numerics.md) |
| Should this animate? | [`docs/motion/01-principles.md`](docs/motion/01-principles.md) |
| What are the exact values? | [`docs/motion/02-tokens.md`](docs/motion/02-tokens.md) · [`docs/motion/03-catalog.md`](docs/motion/03-catalog.md) |
| How does this work on a phone? | [`docs/craft/04-responsive-and-input.md`](docs/craft/04-responsive-and-input.md) |
| Is this ready to ship? | [`docs/craft/03-quality-bar.md`](docs/craft/03-quality-bar.md) |
| **How does accounting actually work?** | [`docs/domain/`](docs/domain/) — model, close, statements, schedules, AR, AP, cash, controls, consolidation, metrics |
| What word do I use? | [`docs/domain/11-lexicon.md`](docs/domain/11-lexicon.md) |
| **How do I draw the logo?** | [`docs/brand/01-identity.md`](docs/brand/01-identity.md) · [`/brand`](/brand) |
