CSE 134B Web Client Languages
  1. Presentation
  2. CSS
  3. Layout
  4. The Box Model and Layout

Presentation · CSS 06

The Box Model and Layout

Every element becomes one or more rectangular boxes, and layout is the browser working out where each box goes and how big it is. CSS does not draw a page; it hands the browser constraints and lets it solve them for whatever screen, zoom level and language the reader brings.

This page starts with the single box: its parts, how its size is measured, and the spacing around it. Then it widens out to the algorithms that place boxes relative to one another: normal flow, positioning, flexbox and grid. It ends by building the same classic page layout twice, once with flexbox and once with grid, as live demos you can open and change.

1. Layout as an algorithm

Most CSS properties configure one box: its color, its font, its padding. Layout properties configure relationships between boxes. To turn those relationships into positions and sizes, the browser runs a layout algorithm at each level of the box tree, and which algorithm it runs depends on the parent's display value.

Boxes, not elements

The browser lays out a box tree, not the DOM. The two are close but not identical. Most elements generate exactly one box. An inline element that wraps across lines generates a fragment on each line. display: none generates no box at all, and display: contents generates none for the element itself while its children take part in the parent's layout. The pseudo-elements ::before and ::after add boxes that have no element behind them.

The four primary algorithms

AlgorithmContainerDimensionsBest for
Normal flowdisplay: block (the default)One: blocks stack, inline content wraps in linesDocuments: prose, headings, lists, figures
Flexboxdisplay: flexOne axis, chosen by the authorA row or column of items sharing space: toolbars, nav bars, button groups
Griddisplay: gridTwo: rows and columns togetherPage layouts, card grids, dashboards, forms with aligned labels
SubgridA grid item with grid-template-rows: subgrid or columnsBorrows the parent grid's tracksLining up the insides of separate cards or components

Each algorithm answers the same question with different inputs: given a container with some available space, and children with sizing and placement properties, where should each child go and how big should it be? Normal flow asks each child for its size and stacks them. Flexbox asks for flex factors and shares out the space along one axis. Grid solves a two-dimensional system of tracks. Layout is constraint solving, and choosing a method means choosing the vocabulary that fits the problem.

2. The box model and box-sizing

Every box has four concentric areas. The content holds the text or the children. Padding surrounds the content and shows the box's background. The border is drawn around the padding. The margin is transparent space outside the border that keeps other boxes away: it is space, not surface, and the background stops at the border edge.

The box model: content, padding, border and marginFour nested rectangles. The innermost is the content area, sized by inline-size and block-size. Around it is padding, then the border, then a dashed margin area. The four sides are labelled block-start at the top, block-end at the bottom, inline-start on the left and inline-end on the right. Below, two measurements compare box-sizing: with content-box, inline-size measures only the content; with border-box, it measures from border edge to border edge.marginpaddingcontentinline-size × block-sizeborderblock-startblock-endinline-startinline-endblock-sizeinline-size (box-sizing: content-box)inline-size (box-sizing: border-box)
The four areas of a box, named by logical side. What inline-size measures depends on box-sizing.

Sizing in logical terms

The size properties come in physical and logical pairs. inline-size is the size along the line (the width, in English), block-size the size along the stacking direction (the height). Each has min- and max- forms. They are widely available, and they keep a component correct when its writing mode changes, so this section uses them.

What a size measures

Set inline-size: 20rem, add 1rem of padding and a 1px border, and the question is whether the box is 20rem wide or a little over 22rem. The answer depends on box-sizing:

ValueThe size setsPadding and border
content-box (the initial value)The content areaAdded outside it, so the box grows past the size you set
border-boxThe area out to the border edgeTaken from inside it, so the content shrinks to make room

Early Internet Explorer measured boxes the border-box way while the specification said content-box, and authors wrote brittle "box model hacks" to serve different numbers to different browsers. The box-sizing property ended the argument by letting the author choose, and nearly every modern style sheet chooses border-box in its reset layer: the size you set is the size you see.

@layer reset { *, *::before, *::after { box-sizing: border-box; } }

Borders

A border has a width, a style and a color, and the border shorthand sets all three: border: 1px solid var(--rule). A border with no style (solid, dashed, double and so on) is not drawn at all. Borders do not inherit. border-radius rounds the corners, which once took sliced images and later vendor prefixes, and is now plain, widely available CSS.

Logical forms exist for every side: border-block-end draws a rule under a heading in any writing mode, and border-inline-start draws the accent bar on a quotation on whichever side the text begins.

3. Margins, padding and logical properties

Margin and padding take one to four values. The four-value form runs clockwise from the top: top, right, bottom, left. Missing values copy the opposite side, so margin: 1rem 2rem means 1rem above and below and 2rem on each side. Margins may be negative, which pulls a box beyond its normal position; padding may not.

The deck this course grew from teaches the four physical sides individually, and it is worth seeing what that looks like, because it is still how most of the CSS on the web is written:

#p2 { margin: 5px 10px; } #p3 { margin-top: 10px; margin-bottom: 20px; margin-left: 30px; margin-right: 50px; } p.pull { margin-left: -2cm; }

Why logical properties matter

margin-left assumes a left-to-right language. Translate the page into Arabic or Hebrew and the inline direction flips, but a physical margin stays on the left, now on the wrong side of the text. The logical properties name the side by its role in the flow instead of by its place on the screen:

PhysicalLogicalFollows
margin-top, margin-bottommargin-block-start, margin-block-endWriting mode
margin-left, margin-rightmargin-inline-start, margin-inline-endWriting mode and direction
Top and bottom togethermargin-block, padding-blockBoth block sides at once
Left and right togethermargin-inline, padding-inlineBoth inline sides at once
border-left and the restborder-inline-start, border-block-end, …The same
width, heightinline-size, block-sizeWriting mode
top, right, bottom, leftinset-block-start, inset-inline-end, …, and the inset shorthandThe same
@layer components { article { padding-block: var(--space); padding-inline: calc(var(--space) * 1.5); margin-block-end: calc(var(--space) * 2); border-inline-start: 0.25rem solid var(--accent); } }

That card is correct in English, Arabic and vertical Japanese with no overrides: the accent bar always marks the side where reading begins. Writing logical properties from the start costs nothing; converting a physical style sheet later means rewriting every spacing rule. Keep physical directions for things that really are physical, such as a drop shadow that suggests light from above.

Centering a block

An auto inline margin takes up the leftover space, so a block with a maximum size and margin-inline: auto sits in the middle of its container. That one pair of declarations makes the classic centered reading column, with no layout system involved.

main { max-inline-size: 65ch; margin-inline: auto; padding-inline: 1rem; }

Margin collapse

In normal flow, adjacent block-direction margins do not add up. They collapse, and the larger one wins. A paragraph with margin-block-end: 1rem followed by one with margin-block-start: 1.5rem ends up 1.5rem apart, not 2.5rem. This was designed for prose, where authors wanted one paragraph gap rather than two, and it surprises everyone who expects space to add.

  • Sibling margins collapse: the larger wins.
  • A first or last child's margin collapses through its parent when no border or padding separates them, so the child's margin appears outside the parent.
  • Inline margins never collapse, and margins inside flex and grid containers never collapse.

To stop a parent's margins from collapsing with its children, give it display: flow-root, which starts a new block formatting context (a region whose insides lay out independently of the outside). Better still, for spacing between components, use a flex or grid container with gap and leave the collapse rules to the prose they were designed for.

4. display, outer and inner

The display property does two jobs. Its outer type says how the box takes part in its parent's layout: as a block that starts a new line, or inline within a line. Its inner type says which algorithm the box runs for its own children. display: flex is shorthand for "block outside, flex inside", and the two-value syntax spells that out: display: block flex. The two-value form is widely available, but the single keywords remain more common and read fine.

ValueOuterInnerUse it for
blockblockflowThe default for p, section, ul
inlineinlineflowThe default for a, em, span
inline-blockinlineflow-rootA box that sits in a line but takes a size and block padding
flow-rootblockflow-rootA block that contains its floats and stops margin collapse
flex, inline-flexblock, inlineflexOne-dimensional layout of the children
grid, inline-gridblock, inlinegridTwo-dimensional layout of the children
list-itemblockflow, plus a markerThe default for li
contentsno box of its ownLetting a wrapper's children join the grandparent's layout
noneno box at allRemoving the element from layout and from the accessibility tree

Values such as run-in and marker, which appear in older references, never became reliable across browsers and are not used today.

Changing display does not change meaning

Setting display: block on a link changes its box, not its role: it is still a link to a screen reader and to the keyboard. The reverse caution applies to display: contents. Browsers have had bugs where an element given display: contents lost its semantics in the accessibility tree, so test it with assistive technology before using it on buttons, lists, tables or headings.

5. Normal flow, floats and positioning

Normal flow is the layout every page has before any layout is written: block boxes stack in the block direction and fill the inline size, inline boxes run along lines and wrap. It reflows for any screen, any zoom level and any text size, which makes it the right layout for most of a document. The first demo has no layout system at all. Its only layout rules are max-inline-size and margin-inline: auto on the article.

Floats: for wrapping text

A floated box is shifted to the start or end of its line and taken out of normal flow. The line boxes of the content that follows get shorter so that text wraps around it. That is what floats were designed for: a figure or a pull quote with prose flowing beside it, the way magazines set images. The clear property makes an element start below any floats before it, and a container with display: flow-root grows to contain its floats.

For about fifteen years, floats were also how the web built columns, because nothing else could put two blocks side by side. Every float layout needed a "clearfix" so containers would wrap their floated children, equal-height columns took tricks, and source order was bent to suit the floats. That is the pattern to recognize in old code and not to write:

#sidebar { float: left; width: 200px; } #content { float: left; width: 600px; } .clearfix::after { content: ""; display: table; clear: both; }

Floats are for wrapping text around something. Page layout belongs to flexbox and grid, which the rest of this page covers.

Positioning

The position property changes how a box relates to the flow, and the inset properties (inset-block-start, inset-inline-end and their shorthand inset) move it.

ValueIn flow?Offsets are measured from
static (default)YesOffsets are ignored
relativeYes: its original space is keptIts own normal position
absoluteNo: siblings act as if it were not thereThe nearest positioned ancestor, or the initial containing block
fixedNoThe viewport; it stays put while the page scrolls
stickyYes, until a scroll thresholdIts scroll container; it sticks while its own container is in view

The common pairing is a relative parent with an absolute child. The relative parent does not move; it only becomes the containing block that the absolute child measures against. In the demo, the tag is pinned to the block-start, inline-end corner of the figure, takes up no space in the text, and overlaps whatever is beneath it.

figure { position: relative; figcaption { position: absolute; inset-block-start: 0; inset-inline-end: 0; } }

Older tutorials reach for positioning often. Use it when the goal really is to take something out of the flow (a badge on a card, a dropdown menu, a skip link) or to fix it in place, as with sticky table headers. For arranging the main regions of a page, absolute positioning breaks as soon as the content, the text size or the screen changes; flexbox and grid adapt.

Anchor positioning, which tethers a positioned box to another element with anchor-name and position-anchor, is the modern answer for tooltips and popovers. It has reached the major engines only recently, so check its Baseline status and treat it as an enhancement over a working fallback.

Stacking and z-index

When boxes overlap, later boxes in the source paint over earlier ones. z-index changes that order for positioned boxes and for flex and grid items. Leave gaps between values (10, 20, 30) so a new layer can slot between two existing ones. The subtler rule is that z-index only compares boxes within the same stacking context. Properties such as opacity below 1, transform, and a positioned box with a z-index each start a new context, so a child with z-index: 1000 can still sit under a neighbor of its parent. isolation: isolate creates a context on purpose, which keeps a component's layers from leaking out.

6. Overflow, min and max sizes, clipping

Content does not always fit the box it is given. A long word, a wide table, a reader's 200 percent text size, a German translation: each can push content past the edge. CSS lets content overflow visibly by default, on the principle that it is better to see content spill than to lose it.

overflow

ValueEffect
visible (default)Content spills out and may overlap whatever follows
autoScrollbars appear only when the content overflows
scrollScrollbars are always present, needed or not
hiddenThe overflow is cut off, but the box is still a scroll container that scripts and focus can scroll
clipThe overflow is cut off and the box is not a scroll container at all

Prefer auto when content might not fit, for example on a wide data table inside a narrow column. hidden is the one to be careful with: content the reader cannot reach is content lost, and it often appears only when someone zooms. An ancestor with overflow: hidden also becomes the scroll container that a position: sticky descendant sticks within, which is the usual reason a sticky header mysteriously stops sticking. Scrolling regions also print badly, since paper does not scroll, so a print style sheet should let them expand. For text, overflow-wrap: anywhere lets a long URL or identifier break rather than overflow.

Minimum and maximum sizes

min-inline-size and max-inline-size (and their block equivalents) set the limits a box may shrink or grow to as its container changes. They are underused and among the most useful properties for adaptive design: a column that grows with the window but stops at a readable measure, an image that never overflows its container. When a minimum and a maximum conflict, the minimum wins.

@layer base { img, video { max-inline-size: 100%; block-size: auto; } main { max-inline-size: 65ch; } aside { inline-size: clamp(12rem, 30%, 20rem); } }

clamp(MIN, PREFERRED, MAX) folds the three into one value; the Values page covers it with min() and max().

Clipping

The original clip property cut an absolutely positioned box down to a rectangle written as rect(top, right, bottom, left). It is deprecated, and it only ever worked on absolutely positioned boxes:

#photo { position: absolute; left: 20px; top: 20px; clip: rect(10px, 90px, 90px, 10px); }

Its replacement, clip-path, works on any element and takes shapes: inset() for rectangles, plus circle(), ellipse() and polygon(). The basic shapes are widely available.

figure img { clip-path: inset(0.5rem round 1rem); }

7. Flexbox

Flexbox arranges items along one axis. A container with display: flex turns each of its direct children into a flex item. The main axis is the direction the items run, set by flex-direction; the cross axis is perpendicular to it. Flexbox properties talk about main and cross rather than horizontal and vertical, because flex-direction: column swaps them.

Flex axes: main and crossA flex container with flex-direction row holds three items placed at the start of the main axis, with free space after them. An arrow along the bottom marks the main axis, running in the inline direction, labelled justify-content. An arrow down the right side marks the cross axis, labelled align-items. The items stretch to the full cross size of the container.display: flex; flex-direction: row123free spacemain axisjustify-contentcross axisalign-itemsWith flex-direction: column, the two axes swap.
The main axis follows flex-direction; justify-content works along it and align-items across it.
header nav ul { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; padding: 0; list-style: none; }

flex-wrap: wrap lets items move to a new line when the main axis runs out of room, and gap sets the space between items and between lines. gap replaced the old habit of putting margins on every child and a negative margin on the container to cancel the outer ones.

How flex items share space

Each item has three sizing inputs, usually set together with the flex shorthand:

PropertyMeaningDefault
flex-basisThe item's starting size along the main axisauto: its content size
flex-growIts share of leftover space0: does not grow
flex-shrinkIts willingness to give up space when there is too little1: shrinks in proportion

The algorithm, in outline: size each item at its basis, add them up with the gaps, and compare with the container. If space is left over, hand it out in proportion to flex-grow. If there is too little, take it back in proportion to flex-shrink, weighted by each item's size. Then clamp each item to its minimum and maximum.

  • flex: 1 means 1 1 0%: ignore content size and share the space equally.
  • flex: auto means 1 1 auto: start from the content size and share the extra.
  • flex: none means 0 0 auto: keep the content size, no matter what.
  • flex: 0 0 10rem means a fixed 10rem column that neither grows nor shrinks.

Alignment

PropertySet onAligns
justify-contentContainerItems along the main axis, sharing out free space: start, center, end, space-between, space-around, space-evenly
align-itemsContainerItems along the cross axis: stretch (default), start, center, end, baseline
align-selfItemOne item along the cross axis, overriding align-items
align-contentContainerWrapped lines along the cross axis

Older code uses flex-start and flex-end; the plain start and end mean the same in a normal flex container and are shared with grid. Together, two alignment properties solve the problem that defeated CSS for most of its history, centering a box both ways:

.center { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } body { display: flex; justify-content: center; align-items: center; min-block-size: 100svh; }

The order property can rearrange flex items visually without touching the HTML. Use it rarely. Keyboard focus and screen readers follow the source order, so a page whose visual order disagrees with its DOM order sends sighted keyboard users jumping around the screen. If the order matters, change the HTML.

8. Grid and subgrid

Grid arranges items in two dimensions at once. Unlike flexbox, it has no favored axis: the container defines rows and columns of tracks, and items are placed in the cells where tracks cross, spanning as many as they need. display: grid makes a grid container, and grid-template-columns and grid-template-rows define its tracks.

Tracks and the fr unit

Track sizeMeaning
10rem, 30%A fixed or percentage size
autoSized by the content in the track
min-content, max-contentAs narrow as the content allows, or as wide as it wants without wrapping
1frOne share of the space left after the other tracks are sized
minmax(a, b)No smaller than a, no larger than b
repeat(3, 1fr)The same track, three times
repeat(auto-fit, …)As many tracks as fit, with empty ones collapsed

One line of grid makes a responsive card layout with no media query: as many columns of at least 15rem as fit, sharing the leftover space. The inner min() keeps a single column from overflowing a container narrower than 15rem.

main > ul { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr)); gap: 1rem; padding: 0; list-style: none; }

Behind that line is a real solver: an item spanning several tracks constrains their sum, so the browser sizes fixed and content tracks first, grows them to fit the items, then hands the remaining space to the fr tracks.

Lines and areas

Grid lines are numbered from 1 at the start edge, and negative numbers count back from the end, so -1 is always the last line. An item can be placed between lines, with span for an extent. More readable still, grid-template-areas draws the layout as a map: each string is a row, each word a cell, and repeating a name across cells makes one area span them. A period leaves a cell empty.

Grid lines and named areasA grid of three columns sized 10rem, 1fr and 12rem, and three rows sized auto, 1fr and auto. Column lines are numbered 1 to 4 across the top, with the last line also numbered minus 1. Row lines are numbered 1 to 4 down the left. Named areas fill it: header spans the whole first row, nav, main and aside share the middle row, and footer spans the whole last row.headernavmainasidefooter1234 · -1123410rem1fr12remauto1frautocolumn tracks
Lines are numbered from 1, and from -1 at the end; named areas are drawn by grid-template-areas.
main > h1 { grid-column: 1 / -1; } main > figure { grid-row: 2 / span 2; }

The implicit grid

When there are more items than explicit cells, the grid adds rows (or columns, with grid-auto-flow: column) on its own. grid-auto-rows sizes those extra tracks, so a feed of unknown length still lines up: grid-auto-rows: minmax(10rem, auto).

Alignment in two dimensions

Grid uses the flexbox alignment vocabulary, with fixed axes: justify-* always works in the inline direction and align-* in the block direction. justify-items and align-items place items within their cells; justify-content and align-content place the whole grid within its container when it is smaller. The place-items shorthand sets both, so display: grid; place-items: center is the shortest centering in CSS.

Subgrid

A row of cards, each with a heading, a paragraph and a link, looks ragged when one heading wraps onto two lines and its card's paragraph starts lower than the others. Each card is its own box, so nothing ties the parts of one card to the parts of the next. Subgrid lets a grid item adopt its parent's tracks instead of defining its own, so every card's heading sits in the same row track. Subgrid is widely available.

main > ul { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr)); gap: 1rem; > li { display: grid; grid-row: span 3; grid-template-rows: subgrid; } }

Each li spans three of the parent's rows and lays its own three children (heading, paragraph, link) into them. A tall heading in one card now grows the shared row, and every card follows.

Multi-column

One more layout model flows text rather than placing items. column-width: 20rem splits an element's content into as many columns of about 20rem as fit, reading down one and on to the next, like a newspaper. It suits short lists better than long articles, where readers would scroll back up for each column.

9. The holy grail twice: flex, then grid

The "holy grail" is the classic page: a header across the top, three columns (navigation, main content, a sidebar) in the middle, a footer across the bottom, the middle row filling the screen even when the content is short. With floats it was famously hard. With flexbox and grid it is short, and building it both ways shows what each algorithm is for.

With flexbox: two containers

Flexbox works in one dimension, so the layout needs two flex containers: the body as a column (header, middle, footer), and a row inside it for the three columns. That row needs an element to be its container, so nav and aside have to go inside main.

body { --nav-width: 10rem; --aside-width: 12rem; display: flex; flex-direction: column; min-block-size: 100svh; > main { flex: 1; display: flex; > nav { flex: 0 0 var(--nav-width); } > article { flex: 1; } > aside { flex: 0 0 var(--aside-width); } } }

The outer column's main grows with flex: 1 and pushes the footer down. Inside it, the side columns hold their basis and the article takes the rest. The side columns run the full height of the row because the default align-items is stretch, which floats could never do.

With grid: one container

Grid works in two dimensions, so one container does it all, and the CSS reads like a sketch of the page:

body { --nav-width: 10rem; --aside-width: 12rem; display: grid; grid-template-areas: "header header header" "nav main aside" "footer footer footer"; grid-template-columns: var(--nav-width) 1fr var(--aside-width); grid-template-rows: auto 1fr auto; min-block-size: 100svh; > header { grid-area: header; } > nav { grid-area: nav; } > main { grid-area: main; } > aside { grid-area: aside; } > footer { grid-area: footer; } @media (width < 40rem) { grid-template-areas: "header" "nav" "main" "aside" "footer"; grid-template-columns: 1fr; grid-template-rows: auto auto 1fr auto auto; } }

Comparing the two

Flexbox versionGrid version
ContainersTwo, nestedOne
Markupnav and aside moved inside main to form the rowAll five regions are siblings in body, as their meaning suggests
SizingEach item claims space with flexThe container declares the tracks
RearrangingChange directions on both containersRewrite one map

The markup row matters most. In the flex version, layout dictated the HTML: the site navigation sits inside main only because flexbox needed a row container. Grid lets the HTML say what each region is and lets the CSS place it. That is the house style's first rule, structure first, made possible by the layout system.

A good rule of thumb: flexbox when content decides the size (a nav bar whose links are as wide as their text), grid when the container decides it (a page or a card grid with planned tracks). Real pages use both, and normal flow inside both.

Choosing an algorithm

The problemReach for
Prose, headings, lists and figures in reading orderNormal flow, with a measure and some spacing
Text wrapping around a figureA float
A row or column of items sharing spaceFlexbox
Page regions, card grids, aligned formsGrid
Lining up the insides of sibling componentsSubgrid
Badges, menus, sticky headersPositioning
A short list flowed into newspaper columnsMulti-column

The mistake to avoid is reaching for grid or flexbox where normal flow already works. Most of a page is a document, and flow handles documents with less code than anything else.

All nine demos

Each demo is a standalone page sharing two small style sheets, layout-vars.css (tokens) and layout-global.css (reset and type, in layers). Every layout rule lives in the demo's own style element, explained in a comment in its head.

  1. Normal flow: block, inline, and a centered column
  2. Float: text wrapping a figure, and clear
  3. Relative position: nudged, with its space kept
  4. Absolute position: pinned inside a relative box
  5. Flexbox centering
  6. Flexbox page: header, main and footer
  7. Flexbox holy grail
  8. Grid page: header, main and footer as named areas
  9. Grid holy grail, remapped by one media query

Media queries and container queries, which make layouts like these respond to their context, are the subject of the next page.

10. Summary

ConceptKey points
Layout as an algorithmThe parent's display picks the algorithm: normal flow, flexbox, grid or subgrid. Each solves the same question with different constraints, in logical terms.
The box modelContent, padding, border, margin. Size with inline-size and block-size, and set box-sizing: border-box in the reset so the size you set is the size you see.
Logical spacingUse margin-inline, padding-block, border-inline-start and inset, so layouts survive translation. Block margins collapse in flow; gap avoids the issue.
displayAn outer type (block or inline) and an inner type (flow, flex, grid). flow-root contains floats; contents needs accessibility testing; changing display never changes meaning.
Flow, floats, positioningFlow is the default and usually right. Floats are for wrapping text, not page layout. Position to take something out of the flow, relative to a positioned ancestor; z-index works within a stacking context.
Overflow and limitsPrefer overflow: auto to hiding content. Use min- and max- sizes or clamp() instead of fixed sizes. clip-path replaces clip.
FlexboxOne axis. flex sets basis, grow and shrink; justify-content aligns on the main axis, align-items on the cross. Add min-inline-size: 0 when an item refuses to shrink. Avoid order that fights the source.
Grid and subgridTwo dimensions of tracks. fr, minmax() and repeat(auto-fit, …) make responsive grids; lines and named areas place items; subgrid aligns the insides of cards.
The holy grailFlexbox needs two nested containers and bends the markup; grid uses one container and a map that one media query can redraw. Use each where it fits, with normal flow inside both.