# 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/08-imagery-and-isometric
Part of: Lopifi — 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 — by design. But the places where it *does* have imagery (empty states, onboarding, concept explanation, the payment card, brand marks) carry disproportionate weight, because they're the only pictures in an otherwise typographic product.

> **Implemented.** This document is the specification *and* the 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. The section below on [what the implementation learned](https://lopifi.com/md/docs/foundations/08-imagery-and-isometric#what-the-implementation-learned) records the 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 renders these as metaphors (a piggy bank, a rocket) which are noise. Isometric renders them as **diagrams with volume**: you can literally show a flow from one object to another, in space, without perspective distortion.

That's the rule that decides when to use it: **isometric explains a mechanism; it does not 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
```

Practical construction: draw on a square grid, scale `Y` to 86.602% (`cos 30°`), rotate 30°, skew −30°. Or 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 it composites correctly when two illustrations sit side by side.

### 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. This is why two independently-drawn illustrations stack coherently — they're on the same lattice, same as the layout.

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 centres the result. A hand-set `viewBox` is how an object ends up clipped at one edge and floating at the other, and it is the single 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**, consistent with the elevation model in [elevation](https://lopifi.com/md/docs/foundations/04-elevation-borders-shadows) and consistent across every 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 perceptually the same step at every hue. That's why the whole illustration set looks like one hand drew it.

**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 get wrong and obvious once seen:

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 is exactly what makes a stack read as floating.
2. **A ground plane casts nothing.** The plinth is the surface. Offsetting a copy of it just draws a second plinth half a step away, which reads as a manufacturing defect.

### Palette

Illustrations use **the product palette**, not an illustration palette. This is the single decision that keeps them from looking like stock art.

Three tones per tone-family, as tokens, so a scene is themed by the page rather than redrawn for 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, rather than as the surface.

Rules:
- **One accent object per illustration.** Same discipline 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 as two SVGs, or one SVG using `currentColor` + CSS custom properties (preferred — one file, 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 face-to-face; the tonal step defines the edge. Outlines make it look like a diagram from 2016.

The exception: a `0.5px` line at `--border` on the *silhouette only* (not internal edges) when an illustration sits on a surface with insufficient contrast — typically a light-mode illustration on a white card. Use it as a fix, not a style.

### The object library
Every scene is assembled from one vocabulary — plane, plinth, slab, column, wall — on one lattice. That shared vocabulary is what lets two independently authored scenes sit side by side without looking like they came from different sets.

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**, which is not a stylistic choice: solids are painted back to front by their near corner's `x + y + z`, and the moment 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** — and 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
```

Never a looping float, never a rotating coin, never a bouncing arrow. An empty state that animates forever is a distraction on a screen the user is trying to leave. Respect `prefers-reduced-motion` by rendering the final frame with a 160ms opacity fade only.

The 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. Still capped, still one-shot, still paused off-screen.

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

Three things this specification originally got wrong, kept here because each one produced a drawing that looked subtly broken rather than obviously broken.

**A CSS length inside an SVG is a user unit, not a pixel.** The entrance in the motion catalog is `translateY(8px)`, which is a restrained nudge on a card and throws an object clean off a ten-unit illustration canvas. Isometric entrance is therefore its own keyframe, `ledger-iso-enter`, expressed in the coordinate system actually in force.

**A drawing that never changes should not ship JavaScript to draw itself.** `IsometricSpot` is a server component. Reduced motion is handled by the theme's global reset rather than by a hook, which is the only reason it can be.

**Detail belongs on a face you can see.** The vault's dial was originally 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; anything meant to read as 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

If a user sees an illustration every day, it has stopped being an explanation and become wallpaper.

---

## Logo treatment {#logo-treatment}

Bank logos, merchant logos, and integration marks appear constantly in financial UI — in transaction rows, account lists, payment method pickers. They are the most common 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 decapitate wordmarks and square logos alike. A rounded square is the only container shape that treats every third-party asset fairly. (People are circular; brands are rounded squares — that distinction also lets the eye separate "who" from "where" at a glance in a transaction row.)

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

1. **Fixed container, contained fit, ~20% inset.** Never let a logo touch its container edge.
2. **Neutral background always.** 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, full color at ≥ 40px. Subtle, and it makes the table dramatically 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 derived from a stable hash of the name (pick from the `--viz-*` scale, so it's on-palette). Deterministic: the same merchant is always the same color.
4. Generic icon by category (`landmark`, `credit-card`, `shopping-bag`)

Never a broken-image icon. Never an empty box. The monogram is good enough that it's 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 exactly the kind of 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 is what makes the stack legible; without it the overlaps mush.
- Never a colored ring for status. A presence dot, bottom-right, 25% of diameter, 2px surface ring.

---

## Photography
**None.** Lopifi ships zero photographic assets. Stock photos of people shaking hands over laptops are the fastest way to make a financial product look untrustworthy. If a consumer needs photography, it goes in their marketing site, not in these components.

---

## Visual hierarchy — the general rules

Beyond imagery: how any Lopifi surface ranks its content. Six mechanisms, in order of strength:

| 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 it will feel flat regardless of styling.
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 rather than emphasize.** The instinct is to make the important thing bigger; the better move is usually to make everything else `--text-secondary`. Quieter surfaces have clearer hierarchy.
4. **Labels lose to values.** Always. A label is `label`/`--text-secondary`; its value is `body`/`--text-primary` or a figure token. Any 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 wearing a color costume.
7. **Icons never outrank their labels.** One step down in color, matched in size band.
8. **The number wins.** Whenever a chrome decision and a figure's legibility conflict, the figure wins. That's the whole product.
