/*
  Bento grid — a board of tiles that declare the room they want, on a grid that
  clamps every span against its own column count, with tiles that recompose
  themselves to whatever slot they land in.

  Self-contained: copy this file and component.html into an unrelated project
  and both keep working. No resets, no global selectors, no font loading, no
  imports. Every tunable is a custom property on the root class.
*/

/* The column count is animated by the `--cycling` modifier further down, and a
   plain custom property cannot be animated — the browser has no idea it holds
   an integer rather than an arbitrary token stream. Registering it says so.

   An at-rule, not a global selector: it registers one property under this
   component's own namespaced name and touches nothing it does not own. */
@property --bento-grid-cols {
  syntax: "<integer>";
  inherits: true;
  initial-value: 4;
}

.bento-grid {
  /* --- tunables ------------------------------------------------------- */

  /* Rhythm. --bento-grid-row is one grid track; every tile is a whole number
     of them. Written in `cqi` — one percent of this element's inline size — so
     tiles keep their proportion as the board narrows. The unit resolves
     against the nearest ancestor container, which is this element, so the
     value is declared here and used on the descendants below. */
  /* The row has to clear the tallest stack of things in a tile that CANNOT
     shrink. At one column every tile is full width and so takes the wide
     composition: label, a two-line aside and a figure is about 99px of content
     in a tile that used to offer 76, and the weave's three bands will not go
     below 130px however little room they are given. The floor was 7rem and
     that is where all of it came from.

     Set from the measurement rather than by eye. A tile's face gets the row
     less 48px — two for the border, 36 for the panel's padding, ten for the
     panel's gap to its own closed details — and the tallest thing any face has
     to hold is the motif tile at 117px: an 11px label, a two-line 38px aside,
     a 48px mark and two 10px gaps. 176px is the first round figure that clears
     it. Every earlier value clipped something: at 7rem, six tiles; at 9rem,
     four; and the failures were small and quiet, 2 to 37 pixels, which is the
     kind that survives a look and turns up in a screenshot later.

     It varies far less than it looks — 176px at a 320px board against 192px at
     a 1400px one. A bento carrying type does not want a track that scales with
     the page; it wants one that clears its own content and then holds still. */
  --bento-grid-row: clamp(11rem, 17cqi, 12rem);
  --bento-grid-gap: 0.75rem;
  --bento-grid-pad: 1.125rem;
  --bento-grid-radius: 0.875rem;

  /* Chalk, not paper: a warm unlit ground that the flat colour sits on
     without either of them lifting. */
  --bento-grid-surface: #efe7d8;
  --bento-grid-ink: #1d1a16;
  --bento-grid-muted: #6d6455;
  --bento-grid-line: rgba(29, 26, 22, 0.16);
  --bento-grid-line-strong: rgba(29, 26, 22, 0.42);
  --bento-grid-sheen: rgba(255, 255, 255, 0.4);
  --bento-grid-shadow: 0 1px 0 rgba(29, 26, 22, 0.05);
  --bento-grid-shadow-lift: 0 5px 0 rgba(29, 26, 22, 0.14);

  /* The nine. Flat and unlit — no gradient, no tint, no shadow on any of
     them. Adjacent pairs are meant to disagree slightly; that is the whole
     register, and softening one to make it sit better takes it out. */
  --bento-grid-c1: #c1493a;
  --bento-grid-c2: #e0a136;
  --bento-grid-c3: #7c8b39;
  --bento-grid-c4: #2f6a63;
  --bento-grid-c5: #474080;
  --bento-grid-c6: #a5487c;
  --bento-grid-c7: #dd8a70;
  --bento-grid-c8: #2b2925;
  --bento-grid-c9: #e9dcbf;

  /* The lead tile is a plate of one of them rather than a surface. */
  --bento-grid-plate: var(--bento-grid-c4);
  --bento-grid-plate-ink: #f3ecda;
  --bento-grid-plate-line: rgba(29, 26, 22, 0.38);

  /* The two values drawn in device pixels rather than layout ones, pulled out
     because something rendering this shrunk — a thumbnail scaled to 0.7 — has
     to be able to hand them back. A hairline asked for at 1px and rasterised
     at 0.7 of one stops being antialiased and starts being a smear. */
  --bento-grid-line-weight: 1px;
  --bento-grid-band-weight: 1px;

  /* Type. Three sizes and nothing between them. */
  --bento-grid-font: system-ui, sans-serif;
  --bento-grid-mono: ui-monospace, "SF Mono", Menlo, monospace;
  --bento-grid-label-size: 0.625rem;
  --bento-grid-label-tracking: 0.16em;
  --bento-grid-body-size: 0.8125rem;
  /* Both of these are `cqi`, and BOTH resolve against the tile rather than
     against the board — because the tiles are containers, and a cqi resolves
     against the nearest one. That is worth stating because it changed silently
     the moment `container` went onto the tile: the hero title had been sized
     against a 1024px board and was suddenly being sized against a 478px tile,
     which took it from 34px to 24px with nothing in the declaration altered.
     Sized against the tile is the better answer for both, so the coefficients
     are the thing that moved, not the unit. --bento-grid-row above is the one
     cqi that still measures the board, because it is used on the board, and
     the board is not a container. */
  --bento-grid-figure-size: clamp(1.5rem, 5.5cqi, 2.125rem);
  --bento-grid-title-size: clamp(1.375rem, 7cqi, 2.375rem);

  /* The specimens inside a tile. Small, but a size is a size. */
  --bento-grid-field-min: 4rem;
  --bento-grid-strip-h: 1.75rem;
  --bento-grid-motif-size: 3rem;
  --bento-grid-specimen-gap: 0.5rem;
  --bento-grid-specimen-label-gap: 0.3rem;
  --bento-grid-specimen-min: 1.25rem;
  --bento-grid-tile-gap: 0.625rem;
  --bento-grid-foot-gap: 0.5rem;
  --bento-grid-band-coarse: 9px;
  --bento-grid-band-medium: 5px;
  --bento-grid-band-fine: 3px;

  /* The two slot thresholds. See the container queries at the foot of the
     file for why they are content-box figures rather than tile widths. */
  --bento-grid-lift: -2px;
  --bento-grid-ease: 200ms cubic-bezier(0.16, 1, 0.3, 1);
  --bento-grid-cycle: 2.4s;
  --bento-grid-reveal: 300ms;
  /* How far a tile drops back while the layout underneath it changes. It is
     small on purpose: the point is to take the hard edge off a cut, not to
     announce one. */
  --bento-grid-settle: 0.986;
  /* -------------------------------------------------------------------- */

  /* The board reflows on its OWN width, not the viewport's. This is the line
     that decides whether the component survives being copied out: dropped into
     a 24rem sidebar on a 1600px page, a viewport media query would still hand
     it four columns and it would overflow. */
  /* Named, and every query below names it back. An unnamed `@container` asks
     the NEAREST ancestor container, and once the tiles became containers
     themselves that stopped being this element: a board-level rule written
     unnamed was silently being answered by a 201px tile, so every tile in a
     four-column board passed a `max-width: 22rem` test meant for the board and
     took the wide composition. Two containers in one component is two names. */
  container: bento-grid-root / inline-size;

  /* What it wants is what it is given; what it will accept is its column. */
  max-width: 100%;
}

.bento-grid__board {
  /* The one number. Every span below clamps itself against it, so the queries
     that follow change this and nothing else. */
  --bento-grid-cols: 1;

  display: grid;
  grid-template-columns: repeat(var(--bento-grid-cols), minmax(0, 1fr));

  /* Fixed, and it has to be. `minmax(var(--bento-grid-row), auto)` was the
     obvious answer to an opened tile wanting more height — and it is a no-op
     here, because the tiles carry `container-type: size`, which applies size
     containment: a tile is laid out as though it had no contents at all, so it
     can never ask its row for anything. That is the trade the height half of
     the slot queries is bought with, and it is worth knowing before trying:
     height-based container queries on a tile and rows that grow to fit that
     tile's content are mutually exclusive. The panel scrolls instead. */
  grid-auto-rows: var(--bento-grid-row);

  /* `dense` is what makes it a bento rather than a stack: a tile too big for
     the gap the cursor is sitting on does not push everything down, it waits,
     and a later unit tile backfills the hole. The cost is that visual order
     stops matching DOM order, which is a decision rather than a bug — see
     notes.md. Reading order stays the DOM's, and the tiles are written in the
     order they should be read. */
  grid-auto-flow: row dense;
  gap: var(--bento-grid-gap);
  max-width: 100%;

  /* The board is a list: the tiles are peers, and the order they are written
     in is the order they are meant to be read in. */
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The ladder, stated once. Mobile-first, so the bare declaration above is the
   single column and each query adds a rung. Nothing here names a tile: the
   spans clamp themselves.

   These three widths are the one set of numbers in this file that cannot be
   custom properties, and not for want of trying: a container or media query's
   condition is evaluated before custom properties are substituted, so
   `@container (min-width: var(--x))` never matches. They are the rule rather
   than a value the rule reads. Override --bento-grid-cols directly instead —
   an inline value beats these, which is how the preview holds a rung. */
@container bento-grid-root (min-width: 22rem) { .bento-grid__board { --bento-grid-cols: 2; } }
@container bento-grid-root (min-width: 29rem) { .bento-grid__board { --bento-grid-cols: 3; } }
@container bento-grid-root (min-width: 44rem) { .bento-grid__board { --bento-grid-cols: 4; } }

/* --- tiles ------------------------------------------------------------- */

.bento-grid__tile {
  --bento-grid-tile-cols: 1;
  --bento-grid-tile-rows: 1;

  /* The fallback, for anything that will not do arithmetic in an integer
     position: every tile is a unit tile and the board is a plain grid. */
  grid-column: span 1;
  /* And the rule. A tile asks for two columns; in a two-column board it gets
     two, and in a one-column board `min()` hands it one rather than letting
     the grid grow a phantom track to accommodate it. Measured: without the
     clamp, a one-column board resolves to `134px 8px` and the wide tiles sit
     16px proud of the narrow ones. */
  grid-column: span min(var(--bento-grid-tile-cols), var(--bento-grid-cols));
  /* Rows are not clamped, and deliberately. A tile that was the tall one at
     four columns is still the tall one at one, which is how the hierarchy
     survives the reflow instead of flattening into a list. */
  grid-row: span var(--bento-grid-tile-rows);

  /* Each tile is its own container, so what it shows is decided by the slot it
     landed in rather than by the modifier it was given. `size` rather than
     `inline-size` because both axes are asked about below, and a grid item in
     a fixed row track has a definite height for it to contain. */
  container: bento-grid-tile / size;

  position: relative;
  min-width: 0;
  min-height: 0;
  box-sizing: border-box;
  overflow: hidden;

  border: var(--bento-grid-line-weight) solid var(--bento-grid-line);
  border-radius: var(--bento-grid-radius);
  background: var(--bento-grid-surface);
  color: var(--bento-grid-ink);
  box-shadow: var(--bento-grid-shadow);

  font-family: var(--bento-grid-font);
  transition:
    transform var(--bento-grid-ease),
    border-color var(--bento-grid-ease),
    box-shadow var(--bento-grid-ease);
}

.bento-grid__tile--feature {
  --bento-grid-tile-cols: 2;
  --bento-grid-tile-rows: 2;
}

.bento-grid__tile--wide { --bento-grid-tile-cols: 2; }
.bento-grid__tile--tall { --bento-grid-tile-rows: 2; }

/*
  The interaction, and the only one: opening a tile promotes it to the lead
  size. The board re-packs around it because the flow is dense, and the tile
  re-composes itself because it is its own container and has just become large
  enough to answer both queries at the foot of this file. One gesture, both
  mechanisms — which is the argument for building them as one system rather
  than as a modifier per state.

  `:has()` is what lets the grid item read a state that belongs to the element
  inside it. The <details> owns the state, as it should: disclosure is what
  that element already is, keyboard, screen reader and find-in-page included,
  and no study should carry script for it.
*/
.bento-grid__tile:has(> .bento-grid__panel[open]) {
  --bento-grid-tile-cols: 2;
  --bento-grid-tile-rows: 2;
}

/* One behaviour, on the tile rather than the board: it lifts a couple of
   pixels and its edge comes up out of the chalk. The shadow is flat and
   unblurred — an offset block of the same ink, which is what the register
   asks for and what a drop shadow would undo. */
.bento-grid__tile:hover {
  transform: translateY(var(--bento-grid-lift));
  border-color: var(--bento-grid-line-strong);
  box-shadow: var(--bento-grid-shadow-lift);
}

/* A finger cannot hover, so the same state is reachable for the length of a
   press. Without this the lift does not exist on a phone at all. */
@media (hover: none) {
  .bento-grid__tile:active {
    transform: translateY(var(--bento-grid-lift));
    border-color: var(--bento-grid-line-strong);
    box-shadow: var(--bento-grid-shadow-lift);
  }
}

/* Keyboard: the summary inside is what takes focus, so the ring is drawn on
   the tile around it rather than on a box the reader cannot see the edges of. */
.bento-grid__tile:focus-within {
  border-color: var(--bento-grid-line-strong);
  outline: 2px solid var(--bento-grid-ink);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .bento-grid__tile { transition: none; }
}

/* --- the panel inside a tile -------------------------------------------- */

.bento-grid__panel {
  display: flex;
  flex-direction: column;
  gap: var(--bento-grid-tile-gap);
  height: 100%;
  min-height: 0;
  padding: var(--bento-grid-pad);
  box-sizing: border-box;

  /* The tile clips, so without this an opened tile in a slot too short for its
     detail loses the end of it with no way to reach it. Scrolling a disclosure
     is not elegant; losing its content silently is worse. */
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* The always-visible face. `list-style: none` kills the disclosure marker in
   Firefox and Chrome; the ::-webkit- rule is still needed for Safari. */
.bento-grid__face {
  display: flex;
  flex-direction: column;
  gap: var(--bento-grid-tile-gap);

  /* It has to be able to shrink — the drawn field is a replaced box whose
     natural height is its width over its aspect ratio, 237px in a tile with
     152px to give, and only a shrinking face lets it come down to fit.
     Refusing to shrink instead put half the tiles into a scroll bar.

     But a <summary> does not clip, so a box shrunk under its content spills
     it rather than hiding it — which is what drew the weave's bands straight
     through the paragraph below them. So: shrink, and clip. The clip is a
     guarantee that a tile can never draw outside itself, not a licence to
     overfill one; the row height below is sized so nothing reaches it. */
  flex: 1;
  min-height: 0;
  overflow: hidden;
  cursor: pointer;
  list-style: none;
}

.bento-grid__face::-webkit-details-marker { display: none; }
.bento-grid__face:focus-visible { outline: none; }

/* The affordance. A plus that becomes a minus, because a tile that grows when
   you press it has to say so before you press it. */
.bento-grid__cue {
  position: absolute;
  top: var(--bento-grid-pad);
  right: var(--bento-grid-pad);
  width: 0.75rem;
  height: 0.75rem;
  flex: none;
  color: var(--bento-grid-muted);
}

.bento-grid__cue::before,
.bento-grid__cue::after {
  content: "";
  position: absolute;
  inset: 50% 0 auto 0;
  height: var(--bento-grid-line-weight);
  background: currentColor;
  transform: translateY(-50%);
  transition: transform var(--bento-grid-ease), opacity var(--bento-grid-ease);
}

.bento-grid__cue::after { transform: translateY(-50%) rotate(90deg); }

.bento-grid__panel[open] .bento-grid__cue::after {
  transform: translateY(-50%) rotate(0deg);
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  .bento-grid__cue::before,
  .bento-grid__cue::after { transition: none; }
}

/* --- tile contents ------------------------------------------------------ */

.bento-grid__label {
  margin: 0;
  padding-right: 1.25rem;
  font-family: var(--bento-grid-mono);
  font-size: var(--bento-grid-label-size);
  letter-spacing: var(--bento-grid-label-tracking);
  text-transform: uppercase;
  color: var(--bento-grid-muted);
}

.bento-grid__title {
  margin: 0;
  font-size: var(--bento-grid-title-size);
  font-weight: 600;
  line-height: 1.02;
  letter-spacing: -0.03em;
  text-wrap: balance;
}

.bento-grid__figure {
  margin: 0;
  font-size: var(--bento-grid-figure-size);
  font-weight: 600;
  /* Not 1. A line box of exactly the font size is shorter than the glyphs it
     holds — ascent plus descent runs about 1.15em — so the ink overflowed the
     box by a constant 2 to 5px, scaling with the font and not with the tile.
     With the figure pinned to the bottom of the face by an auto margin, that
     overflow lands exactly on the clip, which is how it stayed invisible in a
     tile whose figure happens to be `160` and would not have in one with a
     descender. */
  line-height: 1.15;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}

.bento-grid__meta {
  margin: 0;
  font-family: var(--bento-grid-mono);
  font-size: var(--bento-grid-label-size);
  letter-spacing: 0.06em;
  color: var(--bento-grid-muted);
}

.bento-grid__foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--bento-grid-foot-gap);
  margin-top: auto;
}

/* What opening a tile reveals. It sits outside the face, so <details> hides it
   when closed without a line of CSS being involved. */
.bento-grid__detail {
  margin: 0;
  padding-top: var(--bento-grid-tile-gap);
  border-top: var(--bento-grid-line-weight) solid var(--bento-grid-line);
  font-size: var(--bento-grid-body-size);
  line-height: 1.5;
  color: var(--bento-grid-muted);
  text-wrap: pretty;
}

.bento-grid__detail dl {
  margin: 0.4rem 0 0;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0.2rem 0.75rem;
  font-family: var(--bento-grid-mono);
  font-size: var(--bento-grid-label-size);
  letter-spacing: 0.06em;
}

.bento-grid__detail dt { color: var(--bento-grid-muted); text-transform: uppercase; }
.bento-grid__detail dd { margin: 0; color: var(--bento-grid-ink); }

/* --- the lead plate ----------------------------------------------------- */

.bento-grid__plate {
  --bento-grid-ink: var(--bento-grid-plate-ink);
  --bento-grid-muted: rgba(243, 236, 218, 0.7);
  --bento-grid-line: var(--bento-grid-plate-line);
  --bento-grid-sheen: rgba(255, 255, 255, 0.1);
  background: var(--bento-grid-plate);
  border-color: var(--bento-grid-plate-line);
}

/* The drawn field: flat shapes on a flat ground, no gradient anywhere in it.
   It is the one tile carrying an image, and it is drawn rather than
   photographed so it re-tones with the nine above. */
.bento-grid__field {
  display: block;
  width: 100%;
  max-width: 100%;
  height: auto;
  flex: 1;
  /* NOT `min-height: 0`. As a flex child with `flex: 1` and nothing to stop it,
     the field is the one element in the tile that will shrink all the way to
     nothing to make room for the type — and it did: at a 480px frame the hero
     kept its label, title, meta and paragraph and drew no rug at all. The
     drawn field is what the lead tile is FOR, so it is the last thing that
     gives, not the first. */
  min-height: var(--bento-grid-field-min);
  margin-block: var(--bento-grid-tile-gap);
}

/* --- the colourway strip ------------------------------------------------ */

/* Nine flat fields, flush. The join is the point; a gap would turn a bolt of
   colour into nine swatches. */
.bento-grid__way {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  flex: none;
  height: var(--bento-grid-strip-h);
  border-radius: calc(var(--bento-grid-radius) / 2);
  overflow: hidden;
}

.bento-grid__swatch { background: var(--bento-grid-swatch); }

.bento-grid__swatch:nth-child(1) { --bento-grid-swatch: var(--bento-grid-c1); }
.bento-grid__swatch:nth-child(2) { --bento-grid-swatch: var(--bento-grid-c2); }
.bento-grid__swatch:nth-child(3) { --bento-grid-swatch: var(--bento-grid-c3); }
.bento-grid__swatch:nth-child(4) { --bento-grid-swatch: var(--bento-grid-c4); }
/* The cream sits fifth rather than ninth. Last, it is 1.6 OKL off the chalk
   the tile is made of and the strip read as though it had been cut short;
   inboard it reads as one of the nine, which is what it is. */
.bento-grid__swatch:nth-child(5) { --bento-grid-swatch: var(--bento-grid-c9); }
.bento-grid__swatch:nth-child(6) { --bento-grid-swatch: var(--bento-grid-c5); }
.bento-grid__swatch:nth-child(7) { --bento-grid-swatch: var(--bento-grid-c6); }
.bento-grid__swatch:nth-child(8) { --bento-grid-swatch: var(--bento-grid-c7); }
.bento-grid__swatch:nth-child(9) { --bento-grid-swatch: var(--bento-grid-c8); }

/* --- the weave bands ---------------------------------------------------- */

.bento-grid__weave {
  display: grid;
  gap: var(--bento-grid-specimen-gap);
  flex: 1;

  /* `minmax(min-content, 1fr)`, not `minmax(0, 1fr)`, and no `min-height: 0`
     on the block. Three equal shares of whatever the tile has left is right
     while there IS anything left — but opened, this tile's panel also carries
     an aside and a detail, and the weave was what paid for them: measured with
     every tile open, all three rows resolved to a height of exactly 0 while
     their bands stayed 20px, so the labels and the bands drew on top of each
     other and straight through the paragraph below.

     A 1fr row that is allowed to reach zero will, the moment anything else in
     the tile wants the room. The floor is what stops it; the panel scrolls
     instead, which is what its overflow is for. Third time this shape of bug
     has turned up in this file — the drawn field, the motif, and now these —
     and it is always the same one: a flex or grid child with no floor is the
     thing that pays for the type. */
  grid-auto-rows: minmax(min-content, 1fr);
}

.bento-grid__weave-row {
  display: grid;
  /* Same floor, for the same reason: the band is the specimen. */
  grid-template-rows: auto minmax(min-content, 1fr);
  gap: var(--bento-grid-specimen-label-gap);
}

.bento-grid__band {
  --bento-grid-band-ink: var(--bento-grid-c8);
  min-height: var(--bento-grid-specimen-min);
  border-radius: calc(var(--bento-grid-radius) / 3);
  background-color: var(--bento-grid-c9);
  background-image: repeating-linear-gradient(
    90deg,
    var(--bento-grid-band-ink) 0 var(--bento-grid-band-weight),
    transparent var(--bento-grid-band-weight) var(--bento-grid-band-pitch)
  );
}

.bento-grid__band--coarse { --bento-grid-band-pitch: var(--bento-grid-band-coarse); --bento-grid-band-ink: var(--bento-grid-c1); }
.bento-grid__band--medium { --bento-grid-band-pitch: var(--bento-grid-band-medium); --bento-grid-band-ink: var(--bento-grid-c4); }
.bento-grid__band--fine   { --bento-grid-band-pitch: var(--bento-grid-band-fine);   --bento-grid-band-ink: var(--bento-grid-c5); }

/* --- the motif ---------------------------------------------------------- */

.bento-grid__motif {
  display: block;
  width: 100%;
  max-width: var(--bento-grid-motif-size);
  height: auto;
  margin: auto;
  /* Same trap the drawn field fell into: as a flex child it shrinks below its
     own aspect-ratio height to make room for the type, and in a slot wide
     enough to show the aside it had collapsed from 64px to about 20px. The
     motif is the tile's subject, so it does not pay for the sentence. */
  flex: none;
}

/* --- what the slot decides ---------------------------------------------- */

/*
  Everything above describes a tile. These two blocks describe what a tile
  does about the slot it landed in, and they are the reason a tile can be
  moved anywhere without being re-written for its new size.

  A container query measures the container's CONTENT box, not its border box,
  which is why the padding sits on the panel inside the tile rather than on the
  tile: it keeps the box being measured equal to the slot the grid handed over,
  minus a hairline. Put the padding on the tile instead and every threshold
  here has to carry 2.25rem of correction that has nothing to do with layout.

  Measured, as tile content width in rem:

    board 1024px, 4 cols   unit 15.31   wide 31.50   -> compact
    board  668px, 3 cols   unit 13.29   wide 27.46   -> compact
    board  448px, 2 cols   unit 13.50   wide 27.88   -> compact
    board  328px, 1 col    unit 20.38   wide 20.38   -> wide
    board  288px, 1 col    unit 17.88   wide 17.88   -> wide

  So 16rem sits between the widest compact case and the narrowest wide one with
  room on both sides. Note the fourth and fifth rows: a unit tile at ONE column
  is wider than the same tile at four, because there is only one of it. The
  tile that is wider shows more, and a span-based modifier cannot express that
  — 1x1 is a share of a board, not a size.

  Past a board of about 1050px a unit tile clears 16rem on its own and takes
  the wide composition too. That is the rule working rather than failing: a
  270px tile has room for the sentence. demo.html caps its board at 64rem so
  the compact case is what that page shows.
*/
/*
  The supporting line. Two lines is its budget, and that is a constraint on the
  copy rather than on the CSS: a 1x1 tile's face is 128px, which holds a label,
  two 19px lines and a figure and nothing more. At the narrowest width an aside
  appears at — a 256px tile — two lines is about sixty characters. One of these
  ran to seventy, took a third line, and clipped by 6px at exactly one board
  width. Keep them to a breath.
*/
.bento-grid__aside {
  display: none;
  margin: 0;
  font-size: var(--bento-grid-body-size);
  line-height: 1.45;
  color: var(--bento-grid-muted);
  text-wrap: pretty;
}

/* The last five fields of the colourway, which a narrow slot cannot show
   without each one becoming a stripe. Four reads as a colourway; nine at 14px
   each reads as noise. */
.bento-grid__swatch:nth-child(n + 5) { display: none; }

@container bento-grid-tile (min-width: 16rem) {
  .bento-grid__aside { display: block; }
  .bento-grid__swatch:nth-child(n + 5) { display: block; }
}

/* A floor under the above, and it is the board's question rather than the
   tile's: once the board is down to one column, every tile is already as wide
   as anything here is ever going to get, so the compact composition has
   nothing left to save room for. It only fires below a board of about 258px —
   a 320px phone with generous page padding — where the tile's own 16rem test
   would start failing and take a sentence with it that has nowhere else to go,
   since promoting a tile at one column buys rows and not columns. */
@container bento-grid-root (max-width: 22rem) {
  .bento-grid__aside { display: block; }
}

/* Wide AND tall — the lead tile, and any tile that has been opened into the
   lead size. */
@container bento-grid-tile (min-width: 16rem) and (min-height: 11rem) {
  /* 1.35 first, which was not a composition change so much as a rounding
     error — side by side at 34px against 46px the two tiles read as the same
     tile, one of them bigger. A stat tile given four times the area should
     become a stat tile, so the figure nearly doubles. */
  .bento-grid__figure { font-size: calc(var(--bento-grid-figure-size) * 1.9); }
  .bento-grid__way { height: calc(var(--bento-grid-strip-h) * 1.6); }
}

/* --- the ladder, stepped ------------------------------------------------ */

/*
  What the board is about is what it does when it loses a column, and that is
  the one thing a still picture of it cannot show. Under `--cycling` it steps
  its own ladder: four columns to one and back, a quarter of --bento-grid-cycle
  on each rung, with `steps(1, end)` so each count holds for its whole beat
  rather than being interpolated through.

  It is a modifier rather than a hover, so whatever drives it — a demo page, a
  thumbnail being read — decides when. At rest the board is simply a board.
  Because the tiles are their own containers, the ladder also shows them
  re-composing on the way down, which was not designed and is the better half
  of it.
*/
@keyframes bento-grid-ladder {
  0%   { --bento-grid-cols: 4; }
  25%  { --bento-grid-cols: 3; }
  50%  { --bento-grid-cols: 2; }
  75%  { --bento-grid-cols: 1; }
}

.bento-grid--cycling .bento-grid__board {
  animation: bento-grid-ladder var(--bento-grid-cycle) steps(1, end) infinite;
}

/*
  Softening the cut. A grid cannot tween a re-pack — track counts are not
  interpolable and neither is a tile's placement — so nothing here moves a tile
  from where it was to where it is going. What it does is take the hard edge
  off the frame where the change happens: every tile dips a little and comes
  back, on the same clock as the ladder, so a rung reads as a settle rather
  than as a jump cut. It is the cheapest honest thing available in CSS; the
  only way to genuinely tween the re-pack is the View Transitions API, which
  needs script, and this study does not carry any.

  `scale` rather than `transform: scale` so it does not fight the hover lift,
  which is a `transform: translateY` on the same element.
*/
@keyframes bento-grid-settle {
  0%,  25%, 50%, 75% { opacity: 0.55; scale: var(--bento-grid-settle); }
  10%, 35%, 60%, 85% { opacity: 1;    scale: 1; }
}

.bento-grid--cycling .bento-grid__tile {
  animation: bento-grid-settle var(--bento-grid-cycle) ease-out infinite;
}

/* The promoted tile settles in as well. A selector starting to match is enough
   to run an animation, so `:has()` going true on open is what fires this — and
   it is the tile that was clicked, which is where the eye already is. The
   tiles that merely got shoved along by the re-pack are not covered and cannot
   be: CSS has no way to notice that an element's placement changed. */
@keyframes bento-grid-promote {
  from { opacity: 0.6; scale: 0.975; }
}

.bento-grid__tile:has(> .bento-grid__panel[open]) {
  animation: bento-grid-promote var(--bento-grid-reveal) cubic-bezier(0.16, 1, 0.3, 1);
}

/* What opening reveals arrives rather than appearing. */
@keyframes bento-grid-reveal {
  from { opacity: 0; translate: 0 -0.4rem; }
}

.bento-grid__panel[open] .bento-grid__detail {
  animation: bento-grid-reveal var(--bento-grid-reveal) cubic-bezier(0.16, 1, 0.3, 1);
}

/* Content that moves without being asked is the whole of the preference. */
@media (prefers-reduced-motion: reduce) {
  .bento-grid--cycling .bento-grid__board,
  .bento-grid--cycling .bento-grid__tile,
  .bento-grid__tile:has(> .bento-grid__panel[open]),
  .bento-grid__panel[open] .bento-grid__detail { animation: none; }
}
