---
title: "Imagery, isometric & visual hierarchy"
description: "Lopifi has almost no imagery inside its data surfaces. Where it does — empty states, onboarding, concepts, brand marks — isometric explains a mechanism."
canonical: "https://lopifi.com/docs/foundations/imagery-and-isometric"
---

# Imagery, isometric & visual hierarchy

> Lopifi has almost no imagery inside its data surfaces. Where it does — empty states, onboarding, concepts, brand marks — isometric explains a mechanism.

Source: https://lopifi.com/docs/foundations/imagery-and-isometric
Part of: Lopifi — Planned: US GAAP books on autopilot, checked by a CPA (https://lopifi.com)

---

# Imagery, isometric & visual hierarchy

<!-- description: Lopifi has almost no imagery inside its data surfaces. Where it does — empty states, onboarding, concepts, brand marks — isometric explains a mechanism. -->

Lopifi has almost no imagery inside its data surfaces, on purpose. Where it does (empty states, onboarding, concept explanation, the payment card, brand marks), those images carry a lot of weight, because they're the only pictures in an otherwise typographic product.

> **Implemented.** This document is both the spec and a description of what ships: `IsometricSpot` in `src/ledger/components/isometric-spot.tsx`, distributed as `@lopifi/isometric-spot`. Eight scenes, seven placements on this site, one per entry point. [What the implementation learned](https://lopifi.com/md/docs/foundations/imagery-and-isometric#what-the-implementation-learned) records three things this spec originally got wrong.

---

## The isometric system

### Why isometric at all
Financial concepts are structural. Money moves between accounts, a period rolls up into a statement, a transaction matches a bank line. Flat illustration turns these into metaphors (a piggy bank, a rocket), which are noise. Isometric draws them as **diagrams with volume**, so you can show a flow from one object to another in space, without perspective distortion.

That sets the rule for when to use it: **isometric explains a mechanism; it doesn't decorate a screen.**

### The projection

**True isometric, 30°.** All three axes equally foreshortened.

```
X axis   30° below horizontal, going right
Y axis   30° below horizontal, going left
Z axis   vertical
```

To construct it, draw on a square grid, scale `Y` to 86.602% (`cos 30°`), rotate 30°, and skew −30°. In a design tool, apply the standard isometric transform per face.

**Not 2:1 dimetric.** The pixel-art 2:1 (26.565°) projection is for sprite work where you need whole-pixel steps. We're vector at multiple DPRs, so use the true 30°. It's geometrically correct and composites cleanly when two illustrations sit side by side.

<!-- plate:imagery-projection -->

### The isometric grid
The illustration grid derives from the layout grid: **8px base cell**, transformed into the isometric plane. Every object's footprint is a whole number of cells. That's why two independently drawn illustrations stack coherently: they share the layout's lattice.

Canvas sizes (all on the 8px grid):
```
inline      96 × 96      inside a card, next to text
spot       160 × 160     empty states — THE DEFAULT
feature    240 × 240     onboarding steps, feature rows
hero       400 × 400     marketing, first-run
diagram    fluid × 240   flow diagrams, multi-object
```

The canvas is square and **fitted to the scene**, not fixed. The component computes the projected bounds of every solid, pads them, and centers the result. A hand-set `viewBox` is how an object ends up clipped at one edge and floating at the other, and it's the most common reason a set of illustrations stops looking like a set.

### Shading

**Three tones per object. No gradients, no cast shadows, no ambient occlusion.**

Light comes from **above and to the left**, matching the [elevation](https://lopifi.com/md/docs/foundations/elevation-borders-shadows) model and every other illustration in the system.

| Face | Treatment | OKLCH operation |
|---|---|---|
| **Top** | Lightest | base `L + 0.10` |
| **Left** | Base | base |
| **Right** | Darkest | base `L − 0.10` |

Because tokens are in OKLCH, "10% lighter" is the same perceptual step at every hue, so the whole set looks like one hand drew it.

<!-- plate:imagery-shading -->

**Ground shadow:** a flat isometric rhombus (the object's own footprint) at `--iso-shadow`, offset down-right by an eighth of a cell. No blur, no stacking, one per object.

Two rules that are easy to miss and obvious once you see them:

1. **The shadow is drawn at the object's base height, not at zero.** A block resting on a plinth casts *onto the plinth*. A shadow that falls through to the floor makes a stack look like it's floating.
2. **A ground plane casts nothing.** The plinth is the surface. An offset copy of it just draws a second plinth half a step away, which looks like a manufacturing defect.

### Palette

Illustrations use **the product palette**, not an illustration palette. That one decision keeps them from looking like stock art.

Each tone family has three tones, as tokens, so the page themes a scene instead of someone redrawing it:

```
--iso-top / --iso-left / --iso-right                    the accent object
--iso-neutral-top / --iso-neutral-left / --iso-neutral-right   everything else
--iso-ground-top / --iso-ground-left / --iso-ground-right      the plane it sits on
--iso-shadow                                            the flat contact rhombus
```

The ground ramp sits a step below the neutral ramp. Without that separation, a plinth reads as another object that happens to be underneath instead of as the surface.

Rules:
- **One accent object per illustration.** Same as the UI: the accent is the subject.
- **Max 4 hues total**, including neutrals.
- **Every illustration ships light and dark variants.** Not a filter: a re-render against the dark tokens, because `L + 0.10` for a top face has to invert its relationship to the canvas. Ship two SVGs, or (preferred) one SVG using `currentColor` + CSS custom properties, themed by the page.

```svg
<!-- themed by CSS, one asset for both modes -->
<path fill="var(--iso-top)" d="…" />
<path fill="var(--iso-left)" d="…" />
<path fill="var(--iso-right)" d="…" />
```

### Line work
**No outlines by default.** Faces meet directly, and the tonal step defines the edge. Outlines make it look like a diagram from 2016.

The exception is a `0.5px` line at `--border` on the *silhouette only* (not internal edges) when an illustration sits on a low-contrast surface, usually a light-mode illustration on a white card. It's a fix, not a style.

### The object library
Every scene is built from one vocabulary (plane, plinth, slab, column, wall) on one lattice. That's what lets two independently authored scenes sit side by side and still look like one set.

The eight scenes that ship:

| Scene | What it shows | Where it is used |
|---|---|---|
| `ledger` | Bound ledgers stacked on a plinth | Home page, the Domain section |
| `vault` | A vault with its dial on the visible face | Architecture section, the API object model |
| `flow` | Three columns ascending — a quantity moving | Motion section, the MCP page |
| `grid` | Four modules on one base, one of them ranked | Foundations section |
| `stack` | A field of parts at different heights | Catalog section, the component index |
| `documents` | Sheets cascading into one | Craft section, the docs index |
| `tray` | An empty tray — the first-run empty state | `EmptyState` |
| `terminal` | The surface an agent talks to | Tools section |

Scenes are authored as **non-overlapping footprints**, for a technical reason. Solids are painted back to front by their near corner's `x + y + z`. Once two of them interpenetrate, no single sort key is correct and the scene needs a real depth buffer.

### Motion for isometric
Isometric illustrations may animate **once, on entrance, when the surface is first seen**. They never loop.

```
Objects enter in Z order, back to front
Stagger 60ms · translateY(8px) → 0 · opacity 0 → 1
Duration 320ms · --ease-out-quint
Total sequence capped at 480ms
```

No looping float, rotating coin or bouncing arrow. An empty state that animates forever distracts on a screen the user is trying to leave. For `prefers-reduced-motion`, render the final frame with only a 160ms opacity fade.

One exception: a **flow diagram in onboarding** may animate a dash-offset along a connector once when it scrolls into view (`IntersectionObserver`, `{ once: true }`), because the motion is the explanation. It's still capped, one-shot, and paused off-screen.

### What the implementation learned {#what-the-implementation-learned}

Three things this spec originally got wrong. Each produced a drawing that looked subtly broken instead of obviously broken, which is why they're recorded here.

**A CSS length inside an SVG is a user unit, not a pixel.** The motion catalog's entrance is `translateY(8px)`: a small nudge on a card, but enough to throw an object clean off a ten-unit illustration canvas. So the isometric entrance has its own keyframe, `ledger-iso-enter`, in the SVG's own coordinate system.

**A drawing that never changes should not ship JavaScript to draw itself.** `IsometricSpot` is a server component. It can be only because the theme's global reset handles reduced motion, not a hook.

**Detail belongs on a face you can see.** The vault's dial was first placed on the far side of the block and painted over it, because it was authored last. It looked like a floating rectangle. In this projection only two vertical faces of a solid are ever visible, so anything meant to look attached has to sit on one of them.

### Where isometric is forbidden
- Inside any table, statement, or chart
- On a card that contains data
- As a background, watermark, or texture
- Behind text at low opacity
- Anywhere on a screen a user visits daily

An illustration someone sees every day stops explaining anything and becomes wallpaper.

---

## Logo treatment {#logo-treatment}

Bank logos, merchant logos and integration marks show up everywhere in financial UI: transaction rows, account lists, payment method pickers. They're the biggest source of visual noise in the product, because they're third-party assets we don't control.

### The containment rule
**Every third-party logo is contained, never free.**

```
Container    24 (table row) · 32 (list item) · 40 (card) · 48 (account header)
Radius       8px — NOT full. See below.
Background   --surface-sunken
Border       1px --border-subtle
Logo inset   ~20% of container (a 32px container holds a 20px mark)
Fit          object-fit: contain, centered
```

**Radius 8, not circular.** Circular crops cut off wordmarks and square logos alike. A rounded square is the only shape that treats every third-party asset fairly. (People are circles and brands are rounded squares, which also lets the eye tell "who" from "where" at a glance in a transaction row.)

<!-- plate:imagery-logo -->

### Normalization
Third-party logos arrive at random sizes, with random padding, in random color modes. Normalize them:

1. **Fixed container, contained fit, ~20% inset.** Never let a logo touch its container edge.
2. **Always a neutral background.** Never render a logo on the raw surface; a white-on-white logo vanishes.
3. **Desaturate in dense contexts.** In a 400-row transaction table, 400 full-color logos are visual static. Apply `filter: saturate(0.85)` at row scale and full color at ≥ 40px. It's subtle and makes the table much calmer.
4. **Never recolor to match the theme.** A brand's blue is theirs.
5. **Never apply the container's radius to the logo itself.** Contain, don't crop.

### Fallback ladder
Assume the logo is missing, because it often is:

1. Provided logo URL
2. Cached logo from a resolver (Clearbit, Brandfetch)
3. **Monogram**: first letter(s) of the name, Inter 500, on a background picked from the `--viz-*` scale by a stable hash of the name, so it stays on-palette. The same merchant is always the same color.
4. Generic icon by category (`landmark`, `credit-card`, `shopping-bag`)

Never a broken-image icon or an empty box. A monogram is often better than a bad logo.

### Loading
`loading="lazy"`, `decoding="async"`, explicit `width`/`height` to reserve layout, and a `--surface-sunken` background so the container is visible before the image lands. No fade-in on logos in a table. 400 elements fading in is the decorative motion the frequency law forbids.

---

## Avatars

People, not brands. **Circular**, `--radius-full`.

```
16 (inline mention) · 24 (table row) · 32 (list) · 40 (header) · 64 (profile)
```

- Initials fallback with a deterministic `--viz-*` background, Inter 500, `--text-inverse`.
- **Avatar stacks** (approvers, participants) overlap by 25% of diameter with a 2px `--surface` ring, max 4 shown, then `+N` in a neutral chip. The ring keeps the stack legible; without it the overlaps blur together.
- Never a colored ring for status. A presence dot, bottom-right, 25% of diameter, 2px surface ring.

---

## Photography
**None.** Lopifi ships no photographic assets. Stock photos of people shaking hands over laptops are the fastest way to make a financial product look untrustworthy. Photography belongs on a consumer's marketing site, not in these components.

---

## Visual hierarchy — the general rules

How any Lopifi surface ranks its content. Six mechanisms, strongest first:

| Rank | Mechanism | Notes |
|---|---|---|
| 1 | **Size** | The strongest and cheapest. A 40px figure beats everything on the card. |
| 2 | **Position** | Top-left in LTR is read first. The most important number goes there or is the visual anchor. |
| 3 | **Weight** | 500 vs 400. One step is enough; two steps is shouting. |
| 4 | **Color/contrast** | `--text-primary` vs `--text-secondary` vs `--text-tertiary`. Three ranks, no more. |
| 5 | **Space** | Isolation creates importance. A figure with 24px of clear space outranks one with 8px, at the same size. |
| 6 | **Color (chromatic)** | The accent. Weakest in the list because we spend it once per view. |

### The hierarchy laws

1. **Every surface has exactly one primary element.** If you can't name it in one word, the surface is unranked and will feel flat however it's styled.
2. **Contrast the primary against its *neighbors*, not against an abstract scale.** A 20px figure is primary in a dense card and tertiary on a dashboard hero.
3. **De-emphasize instead of emphasizing.** The instinct is to make the important thing bigger. Usually it's better to make everything else `--text-secondary`. Quieter surfaces have clearer hierarchy.
4. **Labels lose to values.** A label is `label`/`--text-secondary`; its value is `body`/`--text-primary` or a figure token. A surface where a label out-shouts its value is inverted, and uppercase table headers are the usual culprit.
5. **Three ranks per surface.** Primary, secondary, tertiary. A fourth is a sub-surface.
6. **Alignment is hierarchy.** A left edge shared by six elements creates a group with no styling at all. Most "this looks messy" problems are alignment problems, not color problems.
7. **Icons never outrank their labels.** One step down in color, matched in size band.
8. **The number wins.** When a chrome decision conflicts with a figure's legibility, the figure wins.
