2026-09 · Layout
Seven tiles that declare the room they want, on a grid that clamps every span against its own column count. Each tile answers for the slot it lands in rather than for the modifier it was given — and opening one re-packs the board around it.
Default — four columns. Open a tile: it takes the lead size, the board re-packs around it, and the tile re-composes itself
Edition 04 · hand-knotted
Knotted on a cotton warp over eleven weeks. The border is carried in two pitches so it reads as one band at a distance and as two up close.
Dyed in one bath, so nothing quite matches and nothing quite clashes.
Three pitches in one field: the coarse rows carry the pattern, the fine ones carry the ground.
Counted across the ground, not the border, which runs denser.
160
The stepped diamond, repeated four times along the long edge.
Cut to the loom width; the length is where the edition varies.
200
Undyed in the ground, so colour sits on grey, not white.
100
One tile's markup, three slots — identical HTML in a 1×1, a 2×1 and a 2×2. Nothing was added to it; the slot decided
Counted across the ground, not the border, which runs denser.
160
Counted across the ground, not the border, which runs denser.
160
Counted across the ground, not the border, which runs denser.
160
The same board in a 26rem column — it reflows against its own width, not the window's, and each tile answers for the slot it now has
Edition 04 · hand-knotted
Knotted on a cotton warp over eleven weeks. The border is carried in two pitches so it reads as one band at a distance and as two up close.
Dyed in one bath, so nothing quite matches and nothing quite clashes.
Three pitches in one field: the coarse rows carry the pattern, the fine ones carry the ground.
Counted across the ground, not the border, which runs denser.
160
The stepped diamond, repeated four times along the long edge.
Cut to the loom width; the length is where the edition varies.
200
Undyed in the ground, so colour sits on grey, not white.
100
The ladder, stepped — four columns to one, a rung every 600ms
Edition 04 · hand-knotted
Knotted on a cotton warp over eleven weeks. The border is carried in two pitches so it reads as one band at a distance and as two up close.
Dyed in one bath, so nothing quite matches and nothing quite clashes.
Three pitches in one field: the coarse rows carry the pattern, the fine ones carry the ground.
Counted across the ground, not the border, which runs denser.
160
The stepped diamond, repeated four times along the long edge.
Cut to the loom width; the length is where the edition varies.
200
Undyed in the ground, so colour sits on grey, not white.
100
A board of seven tiles on a grid that is told how many columns it has and nothing else. Each tile declares the room it wants — two columns, two rows, both, neither — and every span clamps itself against whatever the board currently is, so the arrangement is a rule rather than a map of where things go. Each tile is also its own container, so what it shows is decided by the slot it landed in rather than by the modifier it was given; and opening one promotes it to the lead size, which re-packs the board and re-composes the tile in the same gesture.
A tile carries --bento-grid-tile-cols and
-rows; the board resolves
grid-column: span min(var(--tile-cols), var(--cols)).
The three container queries set --bento-grid-cols to
2, 3 and 4 and touch nothing else — no per-breakpoint span
table, no grid-template-areas map to keep in step.
Without the min() a one-column board grows a phantom
track to fit a two-column tile: measured,
grid-template-columns resolves to
134px 8px and the wide tiles sit 16px proud of the
narrow ones. Rows are deliberately not clamped, so the tile that
led at four columns still leads at 320px.
One 2×2, one 2×1, one 1×2 and four 1×1. Measured at every rung: the board resolves to 4×3, 3×4, 2×6 and 1×9 and fills its rectangle exactly, with no ragged bottom row and no sideways scroll at 320px. Opening a tile breaks that on purpose — it is the reader asking for room, not the layout going wrong.
container: bento-grid-tile / size on every tile, and
two queries decide the composition. Measured as tile content
width: 15.31rem at four columns, 13.29 at three, 13.50 at two
— and 20.38rem at one, because there is only one of it. So a
unit tile is wider at one column than at four and takes
the fuller composition there, which is the answer a span-based
modifier cannot give: 1×1 is a share of a board, not a size.
The threshold is 16rem, between the widest compact case and the
narrowest wide one. Padding lives on the panel inside the tile
rather than on the tile, so the box a container query measures
stays equal to the slot the grid handed over.
<details>.The tile is an <li>; the
<details> inside owns the open state and the
<li> reads it back with :has(),
promoting the tile to 2×2. The board re-packs because the
flow is dense, and the tile re-composes because it has just become
large enough to answer both slot queries. One gesture, both
mechanisms, and no script: disclosure is what that element already
is, keyboard, screen reader and find-in-page included. A grid
cannot tween a re-pack — track counts are not interpolable
and neither is a tile's placement — so what softens it is a
dip: the promoted tile settles in over 300ms, the revealed detail
arrives rather than appears, and under --cycling
every tile dips and returns on the ladder's own clock. Genuinely
tweening the re-pack needs the View Transitions API, and
therefore script.
Nine fields, no gradient and no tint on any of them; the lift is a 5px offset block of the same ink rather than a blur, because a soft shadow would put light into a register that has none. The cream sits fifth in the colourway rather than ninth — last, it was too close to the chalk the tile is made of and the strip read as though it had been cut short.
The row height is measured rather than chosen. A tile's face gets the
row less 48px, and the tallest thing any face has to hold is the motif
tile's 117px; 11rem is the first round figure that clears it. Every
earlier value clipped something, and quietly — at 7rem six
tiles, at 9rem four, by 2 to 37 pixels each. A figure set at
line-height: 1 overflows its own line box by about
0.15em, which with an auto margin pinning it to the bottom lands
exactly on the clip. And an aside gets two lines: about sixty
characters at the narrowest width one appears at.
Three things cost a measurement each. An unnamed @container
asks the nearest ancestor container, so once the tiles became
containers a board-level rule was silently being answered by a 201px
tile and every tile in a four-column board took the wide composition;
two containers in one component is two names. And
--bento-grid-cols is registered with
@property as an <integer> —
without that it cannot be animated at all, and the ladder above jumps
straight to its last rung. And container-type: size
applies size containment, so a tile is laid out as though it had no
contents and grid-auto-rows: minmax(var(--row), auto) can
never grow a row to fit an opened tile — height-based slot
queries and rows that grow to their content are mutually exclusive.
notes.md — the full record, including what was tried and dropped