# Grid & layout

> Every component in Lopifi is designed in four layers, in order. You do not skip ahead. Layer 4 (the actual type) is the last decision, not the first — which is why the system stays coherent when a new component is added by someone who wasn't in the original room.

Source: https://lopifi.com/docs/foundations/01-grid-and-layout
Part of: Lopifi — US GAAP books on autopilot, reviewed by a CPA (https://lopifi.com)

---

# Grid & layout

Every component in Lopifi is designed in **four layers, in order**. You do not skip ahead. Layer 4 (the actual type) is the *last* decision, not the first — which is why the system stays coherent when a new component is added by someone who wasn't in the original room.

---

## The four-layer method

```
Layer 1  ▸  BASE GRID        8px lattice. Nothing else exists yet.
Layer 2  ▸  SAFE SPACE       Padding and grouping. Where content may live, and what belongs with what.
Layer 3  ▸  STRUCTURE        Text slots, media placeholders, controls. Grey boxes only.
Layer 4  ▸  TYPE             Actual sizes and weights. Max 3 of each.
```

Each layer is a separate artboard in the design file and a separate commit in the build. A component review can reject at any layer without touching the ones after it.

---

## Layer 1 — the base grid

**8px base unit.** Set as tightly as possible to preserve flexibility later — a coarse grid (12, 16) makes small controls impossible; a fine grid (2, 4) stops being a grid at all. 8 is the tightest unit that still constrains.

```
--space-0:   0px
--space-0-5: 4px    /* half-step — restricted, see below */
--space-1:   8px
--space-2:   16px
--space-3:   24px
--space-4:   32px
--space-5:   40px
--space-6:   48px
--space-8:   64px
--space-10:  80px
--space-12:  96px
--space-16:  128px
```

### The half-step rule
`4px` exists but is **restricted to the interior of controls ≤ 32px tall** — badge padding, pill insets, icon-to-label gaps inside a small button, checkbox-to-label. It may never appear in layout: no 4px gaps between cards, no 12px card padding, no 20px section spacing.

Rationale: a 32px control cannot be padded on an 8px lattice without becoming visually wrong (8px vertical padding + 16px line-height = 32px works; but a 24px control needs 4px). Everything above control scale stays on 8.

**Forbidden values:** 6, 10, 12, 14, 18, 20, 28, 36, 44. If a design wants 12px, the answer is 8 or 16, and the question is which grouping was wrong.

### Heights are on the grid too
```
Control heights   24 (xs) · 32 (sm) · 40 (md, default) · 48 (lg)
Table row heights 32 (compact) · 40 (default) · 48 (comfortable)
Header / toolbar  56
App top bar       64
Mobile tab bar    56 + safe-area-inset-bottom
```

`md` (40px) is the default control height because it is the smallest height that comfortably clears a 44px touch target once its 4px of ambient hit-slop is counted — see [responsive & input](../craft/04-responsive-and-input.md#touch-targets).

### Layout grid
- **Desktop / laptop:** 12 columns, 24px gutter, 24px page margin, max content width 1440. Below 1440 the grid is fluid; above it, centered with the canvas continuing.
- **Tablet:** 8 columns, 24px gutter, 24px margin.
- **Phone:** 4 columns, 16px gutter, 16px margin. Note the margin drops to 16 — this is the one place the page margin leaves 24, because 24px margins on a 390px screen cost 12% of the viewport.

---

## Layer 2 — safe space and grouping

Safe space is the padding *and* the proof that things belong together. Two elements 8px apart are one thing; 24px apart they are two things. That is the entire hierarchy mechanism at this layer.

### The inset scale
```
Card / panel inset          24px      ← the system default
Compact card inset          16px      (dense mode, or cards < 240px wide)
Dialog inset                24px
Sheet inset                 24px (16px on phone)
Table cell inset            12px v / 16px h — the one sanctioned 12, see note
Toolbar inset               16px
Popover / menu inset        8px (items get 8v/12h)
```

> **Table cell vertical padding is 12px.** This is the sole layout exception to the half-step rule and it is deliberate: a 40px row with 16px line-height needs 12px above and below. Documented as an exception rather than silently violated. Everything else obeys.

### The proximity ladder
Distance encodes relationship. Use only these five:

| Gap | Meaning | Example |
|---|---|---|
| **4px** | Same object | Currency symbol → figure; icon → its label |
| **8px** | Same group | Label → value; two stacked metadata lines |
| **16px** | Related, separate | Two fields in a form; card header → card body |
| **24px** | Distinct groups | Two cards in a grid; form section → form section |
| **32–48px** | Page sections | Statement section → next section; hero → content |

If you need a sixth, one of your groupings is wrong.

### Safe space is not negotiable at the edge
The 24px card inset is a **hard boundary**. Nothing bleeds into it except three sanctioned cases:
1. **Full-bleed dividers** — a `1px` rule that spans the full card width, used between rows in a list card. Achieved with negative margin, not reduced padding.
2. **Full-bleed media** — a chart or image that touches three edges (see media rules below).
3. **Hover/selection backgrounds** on list rows, which extend to the card edge minus the radius clearance.

Everything else respects 24.

---

## Layer 3 — structure

Grey boxes. No type, no color, no icons. You are deciding **how many things there are and where they sit** — and this is where most components are silently over-designed, because it's very hard to justify a fourth text slot when it's an unlabeled grey rectangle.

Every Lopifi component's structure spec names its slots explicitly:

```
BalanceCard
├── eyebrow      ▸ optional  · 1 line  · truncate
├── figure       ▸ required  · 1 line  · never truncates, shrinks instead
├── delta        ▸ optional  · 1 line
├── media        ▸ optional  · sparkline, full-bleed bottom
└── actions      ▸ optional  · max 2, one primary
```

### Structural rules
- **Max 3 text slots per component.** A fourth is a sub-component.
- **Every slot declares its overflow behavior** — `truncate`, `line-clamp-2`, `shrink`, or `never overflows` (with the constraint that guarantees it).
- **Media placeholders declare aspect ratio, never fixed height.** `aspect-[16/9]`, `aspect-[1.586]` for cards, `aspect-[3/1]` for sparkline strips.
- **Controls declare their count ceiling.** "Max 2 actions, at most 1 primary." Enforce it in the type signature where you can.
- **The figure never truncates.** In a finance component, a truncated number is a bug. It shrinks (via a size step-down) or the container grows. See [numerics](06-numerics.md#overflow).

---

## Layer 4 — type

Covered fully in [typography](05-typography.md). The rule that lives here:

> **Max 3 font sizes and 3 font weights per component.**

Not per page — per *component*. This is the constraint that generates hierarchy, because with only three sizes available you are forced to spend them on the three things that actually matter.

A component that genuinely needs four sizes is two components that should be composed.

---

## Optical alignment

The grid gets you 95% there. These fix the rest, and they are the difference between "aligned" and "looks aligned".

### Icons align to cap height, not line box
A 16px icon next to 14px/20px text sits 2px above the text's optical center if you align boxes. Align the icon's optical center to the text's **cap height** center:
```css
.icon-inline { transform: translateY(-0.5px); } /* 16px icon, 14px/20 Inter */
```
Measure per size pair; document the offset. Lopifi's values:

| Icon | Text | Offset |
|---|---|---|
| 16px | 13px/16 | `-0.5px` |
| 16px | 14px/20 | `-0.5px` |
| 20px | 16px/24 | `-1px` |
| 24px | 20px/28 | `-1px` |

### Right-aligned numbers hug the inset
The last column of any numeric table aligns its **right edge to the card inset**, not to a cell padding inside it. A number that stops 16px short of the card edge while the header above it touches the edge is the most common table-craft failure.

### Currency symbols are optically offset
`$` is taller than lining figures and sits differently from `€` and `£`. When the symbol is set at a lighter weight/color (which it always is — see [typography](05-typography.md#the-currency-symbol)), give it a `4px` gap and let it baseline-align; do not attempt vertical centering.

### Trailing punctuation and parentheses hang
In a right-aligned column containing both `1,234.00` and `(1,234.00)`, the closing paren pushes the digits left by ~4px and destroys decimal alignment. Fix: reserve the paren column always — render a transparent `)` for positive values, or use a fixed-width paren cell. Detail in [numerics](06-numerics.md#negative-alignment).

### Optical centering in circles
An icon inside a circular avatar or a play/chevron glyph needs `+0.5px` to `+1px` toward the direction it points. Chevrons in `>` accordions get `translateX(0.5px)`.

---

## Density modes

Three modes, set by a `data-density` attribute on any container, cascading to descendants.

| | `compact` | `default` | `comfortable` |
|---|---|---|---|
| Row height | 32 | 40 | 48 |
| Card inset | 16 | 24 | 24 |
| Body size | 13/16 | 14/20 | 14/20 |
| Cell h-padding | 12 | 16 | 16 |
| Gap ladder | ×0.75 (rounded to 4) | ×1 | ×1.25 |

`compact` is for power users on large displays reviewing hundreds of rows. `comfortable` is the phone default. **Density is not a visual preference — it's an information-throughput setting**, and it must be persisted per user and respected by every component that renders a list.

---

## Layout composition rules

- **Cards in a grid are equal-height by default.** Use `grid` with `auto-rows-fr`, not flex-with-stretch, so the figure baselines align across the row.
- **Never nest more than two card levels.** A card inside a card inside a card is a hierarchy failure; the inner one should be a section with a rule line.
- **Sticky elements are on the grid too.** A sticky table header is exactly one row height. A sticky total footer is exactly one row height + 1px top rule.
- **`h-dvh`, never `h-screen`.** Mobile browser chrome makes `100vh` wrong.
- **Fixed elements respect `env(safe-area-inset-*)`.** Bottom bars, floating action buttons, sticky totals.
- **Scroll containers own their own padding.** Padding on a scroll parent clips the shadow of scrolled content; put it on the inner track.
