layout: rulings for the leaf-only rule
Seven arguments that keep coming back, each settled once. A sides pad a container and pay for it in negative margins; B sides hand the padding to the leaves. Inspector: amber owns padding, red is a hack.
text wrapping inline elements
Chips, code and links flow inside a paragraph. The paragraph is a leaf; the chips inside it are inline flow, not children to arrange. Pad the paragraph, not the card around it: pad the card and every full-bleed band after it must escape with a negative margin. Drag the width: the paragraph is the only padded node, and inline leaves wrap with the text.
A: the card pads
ship only after just lint passes, then open a pr — review is required before merge.
the note bar escapes the padding to run full bleed.
B: the paragraph is the leaf
ship only after just lint passes, then open a pr — review is required before merge.
paragraph insets, note band runs edge to edge by construction.
inline flow is arrangement inside one leaf. pad the block (the paragraph, the note), never the text and never the container around both. a full-bleed band next to prose means two sibling leaves, not an escaped child.
<div class="lay-shell is-wide"><p class="lay-prose">ship after <code class="lay-chip">just lint</code></p><div class="lay-notebar">full-bleed sibling leaf</div></div>/* globals.css — the shell pads nothing; two sibling leaves */.lay-prose { @apply px-4 pt-4 pb-3 leading-relaxed; }.lay-notebar { @apply bg-amber-500/10 px-4 py-2 leading-relaxed; }
icon + label + badge rows
A list of rows, each an icon, a label and a count. Pad the list and every hover fill, active edge and focus ring stops at the inset or climbs out with negative margins. Make whatever is clickable the leaf: the row button pads itself, so every state travels with the padding.
A: the list pads
- deploys12
- reviews3
- incidents1
each row climbs out with negative margins; the active fill and hover stop at the inset.
B: the row button is the leaf
buttons pad themselves and own a focus ring that stays inside the list.
the row is the leaf, not the list. whatever is clickable pads itself: when rows are buttons, the focus ring travels with the padding instead of being clipped or escaped.
<ul class="lay-rowlist"><li><button type="button" class="lay-rowbtn"><span class="lay-ico"></span><span class="lay-rowlabel">deploys</span></button></li></ul>/* globals.css — the list arranges only; the button is the leaf */.lay-rowlist { @apply flex flex-col overflow-hidden rounded-xl bg-zinc-900; }.lay-rowbtn { @apply flex flex-1 cursor-pointer items-center gap-3 px-4 py-2 …; }
grid gutters
A data grid with a header band. Gutters are arrangement, so they belong to the grid: expressed as gap, never as margins on cells or escapes on a header. When the gutter line itself should be visible, paint the container and let a 1px gap show it through. Resize both from the corner.
A: the grid pads
the header band escapes with negative margins; row separators are borders that stop at the padding.
B: gutters are gap
one grid, one gap, gutters for free: no borders, no escapes.
gutters are the grid's arrangement: express them as gap. when the gutter itself is visible, paint the container and let gap-px show it through. cells never carry margins, headers never escape.
<div class="lay-cells"><span class="is-head">name</span><span>build</span><span>…</span></div>/* globals.css — paint the container, gap-px reveals the gutters,every cell is a leaf that pads itself */.lay-cells { @apply grid w-full grid-cols-3 gap-px overflow-hidden rounded-xl bg-zinc-800; }.lay-cells span { @apply bg-zinc-900 px-3 py-2; }
focus rings that clip
Focus the inputs. A ring draws outside the input's border box, so a flush input clips it and the common fix pushes the input past its frame: the layout now moves for a focus state. The right fix gives the indicator its own room without touching layout: draw it inside, or offset the outline, or give the frame a gap.
A: the layout moves for the ring
negative margins buy the ring its pixels; the frame and the input are now coupled.
B: the indicator insets or offsets
ring-inset draws inside the input; a negative outline offset draws outside without touching layout.
focus indicators are leaves too: they live on the control and get their own room: inset rings, outline offsets, gapped frames. no layout may move to make room for a focus state.
<input class="lay-input is-inset-ring" value="ring inside"><input class="lay-input is-offset-ring" value="outline outside">/* globals.css — both keep the layout intact while focused */.lay-input.is-inset-ring { @apply … focus:ring-4 focus:ring-inset focus:ring-indigo-500; }.lay-input.is-offset-ring { @apply … focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500; }
scroll gutter and snap
Sticky header, six sections, jump buttons. Scroll behavior (snap points, scroll padding, scrollbar gutter) belongs to the scroller, the one node that actually scrolls. A pads the scroller so the sticky header must hack through the padding; B keeps the scroller unpadded, sticks the header at top-0 by construction, snaps to section tops with scroll-margin clearance and reserves the scrollbar gutter.
A: the scroller pads
the sticky header escapes the padding to reach the top edge.
B: the scroller is the leaf
snap points and a stable gutter live on the scroller; targets clear the sticky header with scroll-margin. the header sticks at top-0 by construction.
a scroll container is a leaf: it owns scrollbar gutter, scroll padding and snap. children stick to its edge and never compensate for its padding.
<div class="lay-scroll is-snap"><div class="lay-scroll-head">bs builds</div><div class="lay-scroll-sec">bs1: queue</div></div>/* globals.css — scroll behavior sits on the unpadded scroller;snap targets clear the sticky header via the global [id] scroll-margin */.lay-scroll { @apply flex h-64 flex-col overflow-y-auto rounded-xl bg-zinc-900; }.lay-scroll.is-snap { @apply snap-y snap-mandatory [scrollbar-gutter:stable]; }
collapsible panels
Toggle both. A height animation must measure the box, and a padded box makes the measurement lie: the height has to over-measure to swallow the padding, and children escape it. Animate arrangement instead (grid rows from 0fr to 1fr measure the content) and let the leaves keep their padding at every frame.
A: the box pads and collapses
build #127 rolls out to 10% of traffic.
height must over-measure to swallow the padding, and the tip escapes it.
B: rows collapse, leaves pad
build #127 rolls out to 10% of traffic.
grid-rows 0fr to 1fr measures content; padding rides on the leaves and never enters the math.
the collapsing box is a fill: it animates its arrangement (rows), never an inset. leaves keep their padding at every frame, so open and closed states share one set of numbers.
<div class="lay-fold is-open"><div class="lay-fold-inner"><p class="lay-fold-text">build #127…</p><div class="lay-tip is-end">tip runs full bleed</div></div></div>/* globals.css — the fill animates rows; the leaves never change */.lay-fold { @apply grid transition-all duration-300; }.lay-fold.is-open { @apply grid-rows-[1fr]; }.lay-fold.is-closed { @apply grid-rows-[0fr]; }.lay-fold-inner { @apply flex min-h-0 flex-col overflow-hidden; }
a lone leaf alone in a fill
One button on a band. A single child does not turn the band into a leaf-inset: the fill stays a container, and the leaf still chooses its own inset. When the design wants edge-to-edge, the leaf opts in with a zero inset variant: the owner never forces one and the button never escapes one.
A: the band pads, the button escapes
the button climbs out of padding the band should never have had.
B1: the leaf insets
band is a fill, button is a leaf with its own margin of comfort.
B2: the leaf chooses full bleed
full bleed is the leaf picking a zero inset, not the owner forcing one.
a lone leaf still insets itself; the fill stays a container even with one child. when the design wants edge-to-edge, the leaf opts in with a zero inset variant, not a negative margin.
<div class="lay-bandbtn"><button type="button" class="lay-btn is-primary">deploy now</button></div><!-- or the leaf opts into full bleed --><button type="button" class="lay-btn is-primary is-bleed">deploy now</button>/* globals.css — the band pads nothing; the inset lives on the leaf */.lay-bandbtn { @apply flex rounded-xl bg-zinc-900; }.lay-bandbtn .lay-btn { @apply m-3 flex-1; }.lay-btn.is-bleed { @apply m-0 rounded-xl py-3; }