Responsive

The four breakpoints, how the 12-column grid collapses, touch-target sizes, and the mobile navigation pattern.

The rest of the foundations describe values that do not change with viewport width. This page describes the four points at which the layout does.

Breakpoints

Four breakpoints, declared in brand/tokens/variables.css and normative here:

NameMin widthThe layout it describes
sm640pxLarge phone, landscape phone
md768pxSmall tablet — the first two-column layout
lg1024pxTablet landscape, small laptop — sidebars appear
xl1280pxDesktop — the 1100px measure is fully margined

Below sm is not a breakpoint; it is the base. Write the narrow layout first and add width with min-width queries, so an unstyled or unsupported viewport gets the single-column layout rather than a clipped desktop one.

/* Base — single column, 360px and up. */
.panel-row { display: grid; gap: var(--space-4); }

@media (min-width: 768px) {
  .panel-row { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
  .panel-row { grid-template-columns: 1.5fr 1fr; }
}
Breakpoint ruler — base · 640 · 768 · 1024 · 1280
base
1 col
640
1 col
768
2 col
1024
+ sidebar
1280
margined

How the grid collapses

The grid is 12 columns inside the 1100px measure. It does not stay twelve columns all the way down — it resolves to four column counts:

FromColumnsGutterPage padding
base416px16px
md 768px816px24px
lg 1024px1224px32px
xl 1280px1224pxauto — the measure caps at 1100px

Below md, every multi-column region becomes one column. There is no two-column layout on a phone: two 160px columns are two unreadable columns. Regions stack in source order, so the DOM must already be in reading order — do not rely on order or grid-area to fix a sequence that is wrong in the markup, because that breaks the focus and reading order.

Spacing steps down one rung when the grid collapses: a --space-6 (32px) section gap on desktop becomes --space-5 (24px) below md. Radius, type scale, and border weights do not change — a card is a 9px card at every width.

Touch targets

44×44px minimum for anything tappable, at every viewport — the floor is a finger, not a breakpoint.

The default control heights are 32 / 40 / 48px, so the sm and md sizes are below the floor on their own. Two ways to resolve it, in order of preference:

  1. Grow the hit area, not the control. Keep the 40px visual button and give it a transparent 2px vertical extension, or wrap it in a 44px-tall row. The measurement stays normative; the target clears the floor.
  2. Use lg (48px) on touch-primary surfaces. Correct for the primary action on a phone, where a 40px button next to a 48px one reads as demoted.

Never shrink to sm (32px) for a touch target. Spacing between adjacent targets is at least --space-2 (8px), so a mis-tap does not fire the neighbour.

/* Preferred: 40px control, 44px target. */
.pj-btn--md {
  min-height: 40px;
  position: relative;
}
.pj-btn--md::after {
  content: "";
  position: absolute;
  inset: -2px 0;   /* 40 + 2 + 2 = 44 */
}

Mobile navigation

Use a bottom tab bar for application surfaces. Use a top drawer for documentation and marketing surfaces. The choice follows the shape of the navigation, not the shape of the device:

  • An application has a small, flat set of destinations — the dashboard example has five. Five or fewer flat destinations fit a tab bar, which keeps the current location permanently visible, sits in the thumb arc, and costs no taps to reach. That is worth the 56px of permanent screen it occupies.
  • Documentation has a deep, nested tree. A tab bar cannot express it, and flattening the tree to fit one is worse than a drawer. A drawer behind a labelled disclosure control costs one tap and can show the whole hierarchy.

The failure mode is a tab bar with six or more items, or one that hides the primary destination behind “More”. If the destinations do not fit, the surface is a drawer surface.

Both patterns carry the same obligations: the current destination is marked with an accent underline or fill plus the label, never colour alone; the drawer traps focus while open, closes on Esc, and returns focus to its trigger — the same contract as a modal.

Bottom tab bar — 5 destinations, current marked with fill and label
screen content
Overview Agents Alerts Account

Tables, code, and diagrams

Three content types cannot reflow, and each has one answer:

  • Tables scroll horizontally inside their own container with the first column pinned — never the page. See wide tables.
  • Code blocks scroll horizontally. Do not soft-wrap code: a wrapped line changes what the code appears to say.
  • Diagrams get a scrollable container or a re-drawn narrow variant. Do not scale a diagram down until its labels are unreadable — an 8px label is not a responsive diagram, it is a broken one.

Worked example

Mobile onboarding — three screens in a device frame: an onboarding panel on midnight, an engagement list of stacked cards, and a finding-review screen with two side-by-side actions. It is the dashboard below md: same tokens, same components, one column, tab bar instead of sidebar.

Do

Write the base layout first and add width with min-width queries. Put regions in the DOM in reading order. Give every tappable thing a 44px target.

Don't

Keep two columns below 768px, reorder regions with CSS to fix source order, scale a diagram until its labels are illegible, or let a wide table scroll the whole page.