structural mechanics

eight mechanics: sticky, nested scroll, fixed bars, safe areas, reflow, container queries, stacking, aspect ratios. the wrong side is the padded-container way, the right side is leaf-only. tick the inspector to see who owns padding.

leaf pad negative-margin hack

sticky stacks

two sticky bars in one scroller, then sticky groups with a sticky first column.

wrong · the scroller pads

july 2026
revenue by customer
#1000customer 0north$10open
#1001customer 1north$17open
#1002customer 2north$24open
#1003customer 3north$31open
#1004customer 4north$38open
#1005customer 5north$45open
#1006customer 6north$52open
#1007customer 7north$59open
#1008customer 8north$66open
#1009customer 9north$73open
#1010customer 10north$80open
#1011customer 11north$87open
#1012customer 12north$94open
#1013customer 13north$101open
#1014customer 14north$108open
#1015customer 15north$115open
#1016customer 16north$122open
#1017customer 17north$129open
#1018customer 18north$136open
#1019customer 19north$143open
#1020customer 20north$150open
#1021customer 21north$157open
#1022customer 22north$164open
#1023customer 23north$171open

the sticky bars must out-pad the scroller with negative margins, and their stacking order is fragile.

right · the bars pad

north
revenue by customer
#1000customer 0$10open
#1001customer 1$17open
#1002customer 2$24open
#1003customer 3$31open
#1004customer 4$38open
#1005customer 5$45open
#1006customer 6$52open
#1007customer 7$59open
south
revenue by customer
#1008customer 8$66open
#1009customer 9$73open
#1010customer 10$80open
#1011customer 11$87open
#1012customer 12$94open
#1013customer 13$101open
#1014customer 14$108open
#1015customer 15$115open
east
revenue by customer
#1016customer 16$122open
#1017customer 17$129open
#1018customer 18$136open
#1019customer 19$143open
#1020customer 20$150open
#1021customer 21$157open
#1022customer 22$164open
#1023customer 23$171open

the scroller pads nothing; the group is a container (flex col), the bars are leaves that pad and stick as a unit.

ruling
sticky elements that must span the scroll port cannot live inside padding. make the scroller unpadded and let the sticky bars own their insets.
B markup
<div class="lay-stack-b"> <!-- container: no padding -->
<div class="lay-stackgroup"> <!-- one sticky unit -->
<div class="lay-stackhead">north</div> <!-- leaf: owns the inset -->
<div class="lay-stacksub">revenue</div>
</div>
<table class="lay-table">...</table>
</div>
B css
/* globals.css, mechanics 1 */
.lay-stack-b { flex flex-col h-72 overflow-auto rounded-xl bg-paper-quiet; } /* no p-4 */
.lay-stackgroup { sticky top-0 z-20 flex flex-col; }
.lay-stackhead { bg-indigo-700 px-4 py-2 font-semibold; } /* the leaf pads */
.lay-stacksub { bg-zinc-800 px-4 py-1 text-ink-muted; }

nested scroll

a modal whose list scrolls while the modal stays put. the list is a flex child with min-h-0.

wrong · the modal pads

invites
  • invite 1
  • invite 2
  • invite 3
  • invite 4
  • invite 5
  • invite 6
  • invite 7
  • invite 8

the list needs negative margins to reach the modal's edges, and -mx breaks the scrollbar's right inset.

right · the list pads

invites
  • dana · viewer
  • leo · editor
  • mia · viewer
  • raj · editor
  • zoe · viewer
  • kim · admin
  • ana · viewer
  • tom · editor

min-h-0 + overflow-auto on the list; title and foot are leaves that pad. overscroll-contain keeps the wheel inside the list.

ruling
a scroll area is a flex child with min-h-0, not a padded box's escaped list. without min-h-0 the list ignores max-h and the modal grows; without overscroll-contain the wheel scrolls the page behind the modal.
B markup
<div class="lay-modal"> <!-- container: no padding, no gap -->
<b class="lay-modtitle is-inset">invites</b>
<ul class="lay-modlist">
<li class="lay-modrow">dana · viewer</li>
</ul>
<div class="lay-modfoot-in">
<Button variant="ghost" size="sm">cancel</Button>
<Button size="sm">invite</Button>
</div>
</div>
B css
/* globals.css, mechanics 2 */
.lay-modal { flex max-h-64 w-64 flex-col overflow-hidden rounded-xl; } /* no p-6 */
.lay-modlist { flex min-h-0 flex-1 flex-col overflow-auto overscroll-contain; }
.lay-modrow { px-6 py-1; } /* each row is a leaf */
.lay-modfoot-in { flex min-h-12 items-center justify-end gap-2
border-t border-rule after:w-3 after:content-['']; } /* band: gap + spacer, no button margins */

fixed bottom bar

a bar pinned to the bottom of a scroller: sticky bottom works only without container padding.

wrong · the page pads

cart item 1
cart item 2
cart item 3
cart item 4
cart item 5
cart item 6
cart item 7
cart item 8
cart item 9
cart item 10
cart item 11
cart item 12
cart item 13
cart item 14

the bar needs -mb and a padding fudge to clear; scroll to the end and watch the gap jump.

right · the shell splits

cart item 1
cart item 2
cart item 3
cart item 4
cart item 5
cart item 6
cart item 7
cart item 8
cart item 9
cart item 10
cart item 11
cart item 12
cart item 13
cart item 14

the scroller is a flex child (min-h-0), the bar sits after it: nothing sticks, nothing overlaps.

ruling
a pinned bar is a sibling of the scroller, not a sticky escapee. sticky bottom inside a padded page needs -mb and a padding fudge that jumps at the scroll end; split the shell and nothing can overlap.
B markup
<div class="lay-page-b"> <!-- container: no padding -->
<div class="lay-pagescroll"> <!-- min-h-0 flex child -->
<div class="lay-pageitem-in">cart item 1</div>
</div>
<div class="lay-pagebar"> <!-- sits after the scroller -->
<Button size="sm">checkout</Button>
</div>
</div>
B css
/* globals.css, mechanics 3 */
.lay-page-b { flex h-72 flex-col overflow-hidden rounded-xl bg-paper-quiet; } /* no p-4 pb-20 */
.lay-pagescroll { flex min-h-0 flex-1 flex-col overflow-auto; }
.lay-pageitem-in { shrink-0 px-4 py-2; }
.lay-pagebar { flex min-h-14 items-center justify-end border-t bg-paper/90
after:w-4 after:content-['']; } /* no sticky bottom, no -mb */

safe area

a phone frame with fake insets (--sat / --sab). content clears the notch and the home indicator with max(env, inset) padding on leaves.

wrong · magic offsets

wallet

balance $1,204.00

card 4021

limit $5,000

statement july

home

cards

stats

-mt-11 pt-11 is a hardcoded guess; change the notch and every offset is wrong.

right · inset leaves

wallet

balance $1,204.00

card 4021

limit $5,000

statement july

home

cards

stats

the safe top and bottom are leaves of exactly max(env(), inset) height. on a real device env() takes over and nothing else changes.

ruling
safe areas are content, not corrections: give them their own spacer leaves sized max(env(safe-area-inset-*), fallback).
B markup
<div class="lay-phone"> <!-- container: no pt/pb -->
<div class="lay-phonehead">
<span class="lay-safetop"></span> <!-- spacer leaf -->
<b class="lay-phoneheadtitle">wallet</b>
</div>
<div class="lay-phonetabs">
<div class="lay-phonetabsrow">...</div>
<span class="lay-safebottom"></span> <!-- spacer leaf -->
</div>
</div>
B css
/* globals.css, mechanics 4 */
.lay-phone { relative flex h-[420px] w-56 flex-col overflow-hidden; } /* no pt/pb guess */
.lay-safetop { h-[max(env(safe-area-inset-top),var(--sat))]; }
.lay-safebottom{ h-[max(env(safe-area-inset-bottom),var(--sab))]; }

responsive reflow

one component, three states: desktop table, drawer nav, card list: all from the container's width, not the viewport. drag the slider or resize-x.

wrong · viewport queries

invoicecustomertotalstatus
#1042acme$120paid
#1041globex$80due
#1040initech$310paid
#1039umbrella$45overdue
#1038tyrell$220paid

the sidebar uses md:flex (a viewport test). shrink the browser and the layout reflows even though the container is still wide.

right · container queries

invoice$120acme · paid
invoice$80globex · due
invoice$310initech · paid
invoice$45umbrella · overdue
invoice$220tyrell · paid

@container variants: the table shows at @md, the card grid below it, the drawer docks statically at @2xl. the menu button toggles the drawer at narrow widths.

ruling
a component that must work at any width queries itself. the viewport is the browser's business, not the component's.
B markup
<div class="lay-reflow"> <!-- @container, no padding -->
<aside class="lay-refdrawer" data-drawer>
<span class="lay-refitem">dashboard</span>
</aside>
<div class="lay-refmain">
<div class="lay-refmenurow">
<Button variant="ghost" size="sm">menu</Button>
</div>
<table class="lay-reftable is-desktop">...</table>
<div class="lay-refcards">...</div>
</div>
</div>
B css
/* globals.css, mechanics 5 */
.lay-reflow { @container relative flex w-[820px] resize-x
overflow-hidden rounded-xl; } /* no p-4 */
.lay-reftable.is-desktop { hidden @md:table; } /* container query, not md: */
.lay-refdrawer { absolute inset-y-0 @2xl:static @2xl:flex; }
.lay-refcards { grid gap-px @md:hidden; }

container-query card

the same card goes row at @sm and stacks below it. resize-x the card.

wrong · viewport queries

compact cardresizes with its box

the media block's negative margins are glued to the card's padding; stack vs row flips on the viewport, not the card.

right · container queries

compact cardresizes with its box

@sm: the media squares off and docks left; below it, it bleeds to the card's top edge on its own.

ruling
a card that reflows with its own width queries itself. @container replaces both the viewport query and the negative margins that fought the padding: each state is one arrangement rule, and the media bleeds its own edge below @sm without hacking the card's box.
B markup
<div class="lay-cqcard"> <!-- @container, no padding -->
<div class="lay-cqrow">
<span class="lay-cqmedia"></span>
<span class="lay-cqtext is-center">
<b class="lay-cqname">compact card</b>
<span class="lay-cqmeta">resizes with its box</span>
</span>
</div>
</div>
B css
/* globals.css, mechanics 6 */
.lay-cqcard { @container min-w-48 w-[320px] max-w-full resize-x
overflow-hidden rounded-xl; } /* no p-4 */
.lay-cqrow { flex flex-col @sm:flex-row; }
.lay-cqmedia { aspect-video bg-gradient @sm:aspect-square @sm:w-40; } /* no -mx -mt */
.lay-cqname { px-4 pt-3; }
.lay-cqmeta { px-4 pb-3 text-ink-muted; }

z-index layers

left: magic numbers escaping the page. right: one local scale inside isolate. uncheck layers to hide them.

wrong · magic numbers

app bar · z-5
dropdown · z-999
modal · z-50
covers everything, escapes every ancestor
saved · z-40

z-999 beats z-50 by accident; the next feature needs z-9999. the scale is unbounded and global.

right · one local scale

app bar · z-10
dropdown · z-20
modal · z-30context lives in one scale
saved · z-40

isolate caps the scale: 10..40 inside the stage, nothing leaks out, nothing leaks in.

ruling
stacking is a local budget: isolate the stage and spend one scale 10..40. a z-index that exists to beat another component's number is already a bug. the next feature just needs a bigger one.
B markup
<div class="lay-zstage is-scale"> <!-- isolate: one local scale -->
<div class="lay-zhead" data-z="head">
<span class="lay-zlabel">app bar · z-10</span>
</div>
<div class="lay-zdrop" data-z="dropdown">...</div>
<div class="lay-zmodal" data-z="modal">...</div>
<div class="lay-ztoast" data-z="toast">...</div>
</div>
B css
/* globals.css, mechanics 7 */
.lay-zstage.is-scale { isolate flex flex-col; } /* caps the scale */
.lay-zhead { sticky top-0 z-10; }
.lay-zdrop { absolute left-4 top-12 z-20; }
.lay-zmodal { absolute inset-8 z-30 shadow-2xl; }
.lay-ztoast { absolute bottom-4 right-4 z-40; }

aspect ratio

a 16:9 media block: the padding hack versus one property.

wrong · padding-top hack

56.25% padding-top fakes the ratio; the image needs absolute positioning to climb back in.

right · aspect-ratio

gradient placeholder

aspect-video reserves the box; the image is a normal child.

ruling
one aspect-ratio property replaces the padding-top hack and the absolutely-positioned child it forces. the frame is the container's job; the image stays an ordinary leaf.
B markup
<div class="lay-imgcard"> <!-- container: no padding -->
<img class="lay-img" src="..." alt="">
<p class="lay-imgcap">aspect-video reserves the box...</p>
</div>
B css
/* globals.css, mechanics 8 */
.lay-imgcard { flex flex-col overflow-hidden rounded-xl bg-paper-quiet; } /* no p-4 */
.lay-img { aspect-video w-full object-cover; } /* not pt-[56.25%] + absolute */
.lay-imgcap { px-4 py-3; }

search pages

go to any page