# Layout and craft

Small rules that keep Zenox interfaces consistent, each with an ID to cite in reviews and prompts.

These rules cover the details that separate a consistent interface from one that only uses the
right components. Each has an ID: cite it in a review comment or an agent prompt
(`zx/heading-case`) instead of explaining it again.

## Rules

### `zx/body-size` Running text is `text-sm`

Body copy, buttons, form controls, table cells, help text and descriptions are `text-sm`
(12.5px). Larger sizes are for titles and figures only. `text-xs` (11px, the floor) is for
captions, badges and axis labels: never a sentence, a description or a code block, and never the
only place a user reads something they must act on.

```tsx
// Good
<p className="text-sm text-text-secondary">Scans run every night at 02:00.</p>

// Avoid
<p className="text-base">Scans run every night at 02:00.</p>
<p className="text-xs text-text-muted">Scans run every night at 02:00.</p>
```

### `zx/heading-case` Sentence case for every title

Capitalize only the first word and proper nouns. Never set a title in capitals; the caption style
is for short group labels only. See [Writing](/guidelines/writing#capitalization).

```tsx
// Good
<SectionHeading title="Notification channels" />

// Avoid
<SectionHeading title="Notification Channels" />
<SectionHeading title="NOTIFICATION CHANNELS" />
```

### `zx/font-weight` Weight follows the title's role

Page and section headings are `font-semibold`; container titles (Card, Dialog, Sheet, Drawer) are
`font-medium`, so they stay below the section heading above them; compact 14px titles are
`font-semibold`, or they would not stand apart from the medium labels around them. Labels,
buttons and emphasis inside text are `font-medium`. Nothing is `font-bold`: even KPI figures in a
[Stat Tile](/patterns/stat-tile) are semibold. See [Typography](/foundations/typography#title-levels).

```tsx
// Good
<h2 className="text-lg font-semibold">Exposure</h2>
<CardTitle>Microsoft 365 connector</CardTitle>
<p>Credentials for <span className="font-medium">acme.com</span> were found.</p>

// Avoid: a container title as heavy as the section heading above it
<CardTitle className="font-semibold">Microsoft 365 connector</CardTitle>
<h2 className="text-lg font-bold">Exposure</h2>
```

### `zx/tracking` Leave letter spacing alone

Do not change `tracking-*` on running text or titles. The only tracked text is the uppercase
caption, which uses `tracking-wider` or `tracking-widest` from the theme.

### `zx/related-spacing` Related text sits closer together

A title and its description are closer to each other than to the content that follows them.

```tsx
// Good
<div className="grid gap-4">
  <div className="grid gap-1">
    <h3 className="text-lg font-semibold">Detection rules</h3>
    <p className="text-sm text-text-secondary">Rules decide which events become alerts.</p>
  </div>
  <Button variant="outline">Create rule</Button>
</div>

// Avoid: the description is as far from its title as from the button
<div className="grid gap-4">
  <h3 className="text-lg font-semibold">Detection rules</h3>
  <p className="text-sm text-text-secondary">Rules decide which events become alerts.</p>
  <Button variant="outline">Create rule</Button>
</div>
```

### `zx/optical-padding` Vertical padding a step smaller than horizontal

Text carries its own line height above and below, so a box around text looks balanced with a
little less vertical padding than horizontal: `px-4 py-3`, not `p-4`.

### `zx/card-nesting` Never put a card inside a card

Group content inside a card with headings, separators or a [Description List](/patterns/description-list).
A card within a card doubles the borders and the padding and reads as a mistake.

```tsx
// Good
<Card>
  <CardHeader>
    <CardTitle>Asset</CardTitle>
  </CardHeader>
  <CardContent>
    <DescriptionList>...</DescriptionList>
  </CardContent>
</Card>

// Avoid
<Card>
  <CardContent>
    <Card>...</Card>
  </CardContent>
</Card>
```

### `zx/border-or-shadow` A raised surface gets a ring, not a border and a shadow

Floating surfaces (popovers, menus, dialogs) use a shadow with a translucent `ring`, as the library
components do. A solid border under a shadow looks heavy. Surfaces on the page (cards, tables)
use a border and no shadow.

```tsx
// Good
<div className="rounded-lg bg-popover shadow-md ring-1 ring-foreground/10">...</div>

// Avoid
<div className="rounded-lg border border-border bg-popover shadow-md">...</div>
```

### `zx/concentric-radius` Nested corners are concentric

When a rounded element sits inside another with little padding between them, the inner radius
is the outer radius minus that padding. Equal radii make the corners look uneven.

```tsx
// Good
<div className="rounded-xl p-1">
  <div className="rounded-lg">...</div>
</div>

// Avoid
<div className="rounded-xl p-1">
  <div className="rounded-xl">...</div>
</div>
```

### `zx/icon-alignment` Icons align with the first line of text

When text next to an icon can wrap, align the icon with the first line, not the middle of the
block. Wrap it in an element one line tall.

```tsx
// Good
<div className="flex items-start gap-2">
  <span className="flex h-lh items-center">
    <TriangleAlertIcon className="size-4" />
  </span>
  <p className="text-sm">A message that may wrap onto a second line.</p>
</div>

// Avoid
<div className="flex items-center gap-2">
  <TriangleAlertIcon className="size-4" />
  <p className="text-sm">A message that may wrap onto a second line.</p>
</div>
```

### `zx/inline-code` Identifiers in running text are monospace and slightly smaller

Hashes, IPs, file names and keys inside a sentence use `font-mono` at about 90% of the text
size, so they do not look bigger than the words around them. Standalone values in tables and
detail views use `font-mono` at the normal size.

### `zx/sticky-border` Sticky bars have a border

A header or toolbar that stays in place while content scrolls under it gets a `border-b` (or
`border-t` at the bottom), so the edge stays visible against the moving content.

### `zx/overlay-state` Never render an overlay conditionally

Keep dialogs, sheets and popovers mounted and control them with `open`. Rendering them
conditionally skips the exit animation and loses focus restoration.

```tsx
// Good
<Dialog open={open} onOpenChange={setOpen}>...</Dialog>

// Avoid
{open ? <Dialog open>...</Dialog> : null}
```

### `zx/collapse-size` Content keeps its size while it collapses

When a panel animates closed, the content inside keeps its width or height and is clipped, rather
than reflowing as the panel shrinks.

### `zx/heading-action` An action beside a heading is centered on it

The action of a page header, a section heading or a card header sits on the right, vertically
centered on the heading block: the title, or the title and its description together.
[Section Heading](/patterns/section-heading) and `CardAction` do it for you; a hand-built header
uses `items-center`, not `items-start`.

### `zx/control-height` Controls in one row share one size

Buttons, inputs, selects, toggle groups and filter controls placed side by side use the same
`size`, so they share one height: 28px (`sm`), 32px (`default`) or 36px (`lg`). A toolbar of
mixed heights looks broken even when each control is right on its own.

```tsx
// Good
<Input size="sm" className="w-56" placeholder="Search alerts" />
<ToggleGroup variant="segmented" size="sm">...</ToggleGroup>
<Button variant="outline" size="sm">Export</Button>

// Avoid: 32px input, 28px toggle group, 32px button
<Input className="w-56" placeholder="Search alerts" />
<ToggleGroup variant="segmented" size="sm">...</ToggleGroup>
<Button variant="outline">Export</Button>
```

### `zx/selected-state` Selected, hovered and checked look different

| State                                                                                                           | Treatment                                                                                                 |
| --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Hover on a control, a row or a navigation item                                                                  | `bg-surface-muted`                                                                                        |
| Highlighted item of a menu, command list or listbox: the item Enter would choose                                | The selection colors below                                                                                |
| Selected or pressed: a pressed toggle, a segmented choice, an active filter chip, the current sidebar item, tab | `bg-primary-soft` with `text-primary-soft-foreground`, and a primary border or underline where it has one |
| Checked form controls and chosen values: checkbox, radio, switch, the selected calendar day                     | The primary fill                                                                                          |

Use `text-primary-soft-foreground`, not `text-primary`, on `bg-primary-soft`: only the first
reaches 4.5:1 in both themes. A selected item never looks like a hovered one: where an item can
be both (a sidebar, a toggle), hover stays `bg-surface-muted`.

### `zx/one-primary` One filled button per view

See [Action placement](/guidelines/action-placement#principles).

### `zx/color-not-alone` State is never shown by color alone

Every status, severity and validation color comes with text, and with an icon where space allows.
