/* ==========================================================================
   site.css — shared chrome: the floating nav, the footer, and the layouts
   every page uses. Loaded by every page.
   ========================================================================== */

/* --------------------------------------------------------------------------
   THE FIXED NAV RAIL

   No chips. Three groups of black text sitting directly on the page ground,
   held apart by the same three-column grid and the same --page-margin inset
   as the body copy below — so the wordmark and the Connect link pin to
   exactly the line the text does, by construction rather than by a matching
   number.

   The rail carries a TRUE WHITE ground (not the old 90% grey + 30px blur):
   fixed chrome has to stay legible when it passes over a full-bleed
   photograph, and the only surface the colour law allows is #FFF. At the top
   of a page that ground is invisible against the page. A hairline appears
   under the rail only once the page has scrolled — so the line shows up
   exactly when there is something beneath it to separate from.
   -------------------------------------------------------------------------- */
.nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 999;
  height: var(--header-height);
  padding-inline: var(--page-margin);
  padding-block: var(--header-padding-block);
  background: var(--ground);
  border-bottom: 1px solid transparent;
  transition: border-color var(--anim-sm);
}
html.is-scrolled .nav { border-bottom-color: var(--border); }

.nav__grid {
  display: grid;
  grid-template-columns: 1fr 1.5fr 1fr;
  gap: var(--grid-gap);
  align-items: center;
  height: var(--header-element-height);
}

/* left — wordmark */
.nav__mark {
  justify-self: flex-start;
  display: flex;
  align-items: baseline;
  gap: var(--space-xs);
}
.nav__wordmark { font-weight: 500; letter-spacing: -0.03em; line-height: 1; }
.nav__wordmark--lg { --base-size: 24; --min-size: 20; font-size: var(--fluid); }
.nav__est { color: var(--ink-muted); transition: color var(--anim-sm); }
.nav__mark:hover .nav__est { color: var(--ink); }

/* centre — navigation. Items are 12px sentence case at zero tracking: the
   same caption step as every other small label on the site. The gap is the
   layout gutter; nothing is padded, so the row is text and air only. */
.nav__pill {
  justify-self: center;
  display: flex;
  align-items: baseline;
  gap: var(--grid-gap);
}
.nav__link {
  display: inline-flex;
  align-items: baseline;
  color: var(--ink-muted);
  white-space: nowrap;
}
/* ONE hover gesture on this site now: quiet brightens to ink. Nothing fills. */
.nav__link:hover { color: var(--ink); }

/* The active page. Not a filled chip and not a colour change alone — a
   hairline in ink under the current item. It is the same affordance the
   in-page .link uses, which is the point: "this is the thing you are on"
   and "this is a thing you can go to" are the same gesture, at rest versus
   on hover. Colour alone (ink vs #6B6B6B at 12px) was not legible enough to
   find at a glance; a black chip was the single loudest object on a page
   that no longer has any filled object at all. */
/* Two values, and the difference is real: "page" is the page you are on,
   "true" is the section you are in. An article under /ideas/ is not the Ideas
   index, so its nav mark is `true`, and the rail looks identical either way
   because the reader's question is the same one. */
.nav__link[aria-current="page"],
.nav__link[aria-current="true"] {
  color: var(--ink);
  border-bottom: 1px solid var(--ink);
  padding-bottom: 0.25em;
}

/* right — contact. Same hairline idiom as every other .link on the site. */
.nav__end {
  justify-self: flex-end;
  display: inline-flex;
  align-items: baseline;
  gap: 0.5em;
  color: var(--ink);
}
.nav__end .link__label {
  border-bottom: 1px solid var(--border);
  padding-bottom: 0.25em;
  transition: border-color var(--anim-sm);
}
.nav__end:hover .link__label,
.nav__end[aria-current="page"] .link__label { border-color: var(--ink); }
.nav__end .link__arrow { transition: translate var(--anim-sm); }
.nav__end:hover .link__arrow { translate: 3px 0; }

@media (max-width: 768px) {
  /* The wordmark's date string goes; the Connect link does NOT. Hiding it
     left a phone with no route to /connect/ anywhere in the navigation. */
  /* Three text groups on one 390px line. The GRID gap (between groups) stays
     larger than the gap INSIDE the nav group, so the four page links read as
     one set and "Connect" reads as separate from them — which the chips used
     to say with three rectangles. */
  .nav__grid { grid-template-columns: auto 1fr auto; gap: var(--space-sm); }
  .nav__est { display: none; }
  .nav__end .link__arrow { display: none; }
  .nav__pill { justify-self: flex-end; gap: var(--space-xs); }
}

/* --------------------------------------------------------------------------
   FOOTER
   -------------------------------------------------------------------------- */
/* A terminus, not a second navigation. One hairline, one line of plate data.
   The four-column version repeated every destination the three-item nav
   already carries, and gave a three-page site a site map. */
.foot {
  border-top: 1px solid var(--border);
  padding-block: var(--space-md);
}
.foot__line {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-xl);
  justify-content: space-between;
  align-items: baseline;
  margin: 0;
}
.foot__line .nav__wordmark { color: var(--ink); text-transform: none; letter-spacing: -0.03em; }
.foot__line a { color: var(--ink-muted); }
.foot__line a:hover { color: var(--ink); }
/* The portfolio disclaimer. One quiet sentence under the plate line, on every
   page, in the site's one small-label style rather than in the mono uppercase
   of the line above it (A6). Measure in em, like every measure here (B6).

   FLUSH LEFT AND ONE STEP DOWN, which reverses round seven. That round
   centred the line and argued explicitly against shrinking it: .caption is
   the smallest sans step the scale has, and a tenth step invented for one
   sentence is not a system. Rendered, the centred line reads as a caption
   belonging to the rail above it rather than as a footing under the whole
   page, and at .caption it wraps to two lines at every width the site is
   read at. The principal ruled for one line, smaller, flush left. So the
   sentence was cut until it fits one line at 1440, and the size is set here
   as a local override on this one element rather than as a new named step,
   which is the narrower change: the scale still has nine steps. */
.foot__note {
  --base-size: 11;
  --min-size: 11;
  font-size: var(--fluid);
  max-width: none;
  margin-inline: 0;
  text-align: left;
  padding-top: var(--space-sm);
  text-wrap: pretty;
}

/* --------------------------------------------------------------------------
   PAGE HEAD (the one page that still has a display word of its own)
   -------------------------------------------------------------------------- */
.pagehead__word { margin-top: var(--space-2xl); }

/* --------------------------------------------------------------------------
   THE GAP BETWEEN A SECTION'S OPENER AND ITS BODY
   One token, one rule, used by every band on the site — it was six copies of
   the same inline style, which is how a system quietly stops being one. The
   benchmark's dominant in-band gap is --space-3xl; --space-4xl is its BETWEEN
   -band value, and using the between-band number inside a band is what opened
   the dead air under every opener.
   -------------------------------------------------------------------------- */
.band__body { margin-top: var(--space-3xl); }

/* the one remaining one-off nudge, as a rule rather than an inline attribute */
.figcap { padding-top: var(--space-xs); }

/* --------------------------------------------------------------------------
   ENTRY ROWS — the shared list. /ideas uses it, the home page's ideas tease
   and its migration row use it, and option C's whole index is built from it.
   Same 1fr 2fr spine as every opener on the site.
   -------------------------------------------------------------------------- */
.entries { border-top: 1px solid var(--border); }
/* A PICTURE LEFT OF THE TITLE. The row was kicker, title, then a column of
   air, and the air was the 2fr the description used to sit in on the index.
   An entry now opens with a 1:1 crop of the thing it is about, which is the
   one cell in the row that answers "what is this" before a word is read.

   It is a column of its own rather than a float, so the rest of the row
   keeps its spine: a thumbnail inside .entry__main would push the kicker and
   the title off the seam every other opener on the site sits on.

   Rows WITHOUT a thumbnail still work, and that is deliberate rather than
   incidental -- the first column collapses to nothing and the row reads as
   it did before. */
.entry {
  display: grid;
  grid-template-columns: var(--entry-thumb) 1fr 2fr auto;
  gap: var(--grid-gap);
  align-items: start;
  padding-block: var(--space-lg);
  border-bottom: 1px solid var(--border);
}
:root { --entry-thumb: 88px; }
.entry__thumb {
  width: var(--entry-thumb);
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--ground-2);
}
.entry__thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
.entry__main { display: flex; flex-direction: column; gap: var(--space-2xs); align-items: flex-start; }
/* The row's heading is its SUBJECT, and it is the only sans on the site set
   at 500 outside the wordmark. That is deliberate and it is not a new step:
   the step is still .h6, the face is the one already preloaded for the mark,
   and the weight is doing the job a dek used to do — telling the eye where a
   row begins in a list whose other three lines are all mono and all muted.
   Rows carry no description any more; a subject and a date are the row. */
.entry__title { font-weight: 500; }
.entry__desc { max-width: 40em; }
.entry__titlerow { display: flex; align-items: baseline; gap: var(--space-xs); flex-wrap: wrap; }
/* An entry row is a quiet link, not a bounded control, so it takes the other
   half of the hover law: everything muted in it brightens to ink. It used to
   do the exact opposite — its title DIMMED on hover while the footer links
   above it brightened, which meant two link families moving in two directions
   on the same page. */
.entry__title, .entry__desc, .entry__meta, .entry__main .utility {
  transition: color var(--anim-sm);
}
a.entry:hover .entry__desc,
a.entry:hover .entry__meta,
a.entry:hover .entry__main .utility { color: var(--ink); }
/* Pinned to the third column rather than auto-placed. A row whose description
   has been cut has two children, not three, and auto-placement would put its
   meta in the description's column — so one row would break the spine the
   whole list is built on. */
.entry__meta { grid-column: 4; justify-self: flex-end; display: flex; align-items: baseline; gap: var(--space-md); }
.entry__arrow { transition: translate var(--anim-sm); }
a.entry:hover .entry__arrow { translate: 3px 0; }

@media (max-width: 1024px) {
  /* The thumbnail keeps its column here too. The row's other three cells
     collapse into one stack beside it, which is the layout the row has on a
     phone anyway; what changes is that the stack now has a picture to its
     left instead of starting at the page inset. */
  .entry { grid-template-columns: var(--entry-thumb) 1fr; gap: var(--space-xs) var(--grid-gap); }
  .entry__main { grid-column: 2; }
    /* re-placed, not deleted: it used to display:none, which silently dropped
     "Rev. Sep 2026", "Live · May 2026" and every row's destination on mobile */
  .entry__meta { grid-column: 2; justify-self: flex-start; gap: var(--space-sm); }
  .entry__desc { grid-column: 2; }
}

/* --------------------------------------------------------------------------
   THE MERCHANT APP SURFACE, AND THE RE-RANK RUN INSIDE IT

   Merchant is built on Polaris, Shopify's open-source admin design system, so
   this is Polaris ANATOMY drawn in this site's CHROME. What is faithful: a
   top bar carrying the app name and the run's metadata; cards stacked on
   hairlines, each with a heading and a trailing meta line; an index table
   with a column-heading row, hairline row dividers and a numeric column on
   the trailing edge; a tab row that selects what the table shows.

   What is NOT faithful, and why: Polaris paints a card as a surface with a
   shadow and a badge as a filled pill. Neither a fill nor a shadow exists on
   this site (A7), so a card is a hairline box and a badge is mono-uppercase
   register beside sentence-case sans. Polaris' radius is 8px; the one
   structural radius here is 3px (A1) and the app takes it. Nothing is loaded
   from anywhere: no Shopify wordmark, no Shopify asset, no Polaris stylesheet.

   THE ONE NEW GEOMETRY. This is the first bounded rectangle on the page
   ground since the chips came off. It is not a fill — its ground is the
   page's own #FFF and its edge is --border, the same hairline every rule on
   the site is drawn with — and it is bounded because the thing it is
   drawing IS bounded: an embedded admin app is a panel inside someone
   else's page, and drawing it unbounded would be drawing something else.
   -------------------------------------------------------------------------- */
.rerank { display: flex; flex-direction: column; gap: var(--space-md); }

.app { border: 1px solid var(--border); border-radius: var(--radius); }

.app__bar, .app__foot {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: var(--space-xs) var(--space-md);
  padding: var(--space-xs) var(--space-md);
}
.app__bar  { border-bottom: 1px solid var(--border); }
.app__foot { border-top: 1px solid var(--border); }
/* Wider columns than a tag row: these are four label/value pairs rather than
   four one-word tags, and the pairs have to stay legible as pairs once the
   boxes are gone. */
.app__run { gap: var(--space-2xs) var(--space-xl); }
/* The replay control does nothing without the script, so it is not on the
   page without it (F5). */
.app__foot .link { display: none; cursor: pointer; }
.rerank.is-live .app__foot .link { display: inline-flex; }

.app__card { padding: var(--space-md); }
.app__card + .app__card { border-top: 1px solid var(--border); }
.app__head {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: var(--space-2xs) var(--space-md);
  padding-bottom: var(--space-xs);
}

/* ---- the four signal stages, which are also the tabs ---------------------
   Position is shown exactly the way the nav shows the current page and the
   migration panel shows its part: the chosen one is ink with an ink hairline
   under it, the rest are quiet. Nothing fills, and there is no dot. */
.sigs {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--space-xs) var(--space-xl);
  border-block: 1px solid var(--border);
  padding-block: var(--space-xs);
}
.sig {
  display: inline-flex; align-items: baseline; gap: var(--space-2xs);
  color: var(--ink-muted);
  cursor: pointer;
  border-radius: 0;
  border-bottom: 1px solid transparent;
  padding-bottom: 0.25em;
  transition: color var(--anim-sm), border-color var(--anim-sm);
}
.sig:hover { color: var(--ink); }
/* computing, during the run; selected, afterwards. The same two properties
   either way, so the run's cursor and the tab's selection are one gesture.
   The 01 to 04 that used to lead each stage label came off in round seven:
   the card head already says "4 stages", the tabs are in order on the page,
   and a datum rule does not need an ordinal in its label half to be read in
   the order it is written. */
.sig.is-computing,
.sig[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--ink); }

/* One note is on the page at a time. It has an attribute of its own rather
   than riding data-signal, because during the run the note follows the stage
   that is computing while the table's metric column has to stay put — a
   column that appears and disappears mid-run would re-flow the rows the FLIP
   is in the middle of moving. Both attributes are set in the markup, which
   is why the finished state is correct with no script at all.

   Four notes in one grid cell, visibility rather than display: the card is
   as tall as the longest note in every state, so a stage change moves
   nothing, and the hidden notes stay out of the accessibility tree without
   any ARIA bookkeeping to drift. */

/* ---- the index table ---------------------------------------------------- */
.rerank__diff { display: grid; grid-template-columns: 1fr 1fr; gap: var(--grid-gap); }
.rerank__col { display: flex; flex-direction: column; }
/* Both columns march on the same rhythm. A before/after diff whose row 4 does
   not sit level with the other column's row 4 is not a diff, it is two lists —
   and the trailing metric makes its rows taller unless the row height is
   pinned. The floor value comes off the frozen detail scale. */
.rerank__colhead, .rerank__rows li {
  display: grid;
  grid-template-columns: 2.5em var(--rerank-thumb) 1fr auto;
  gap: var(--space-xs);
  align-items: center;
  min-height: calc(var(--space-md) + var(--space-xs));
  padding-block: var(--space-2xs);
  border-bottom: 1px solid var(--border);
}
.rerank__colhead { align-items: baseline; }
.rerank__rows { display: flex; flex-direction: column; }
/* THE WELLS COME BACK, CARRYING PHOTOGRAPHS.

   They were cut when this band was drawn, for a reason that was correct at
   the time: they stood in for a product thumbnail nobody had, which made
   them sixteen grey rectangles carrying no information, and a filled
   rectangle carrying no information is the exact thing the fill budget
   exists to stop.

   What changed is not the rule, it is the inventory. The twelve products of
   Lot 001 are photographed, on a white ground, and the studio owns them. So
   the well is no longer a placeholder: it is a 1:1 crop of a real garment,
   and a row of them turns two columns of product names into two columns a
   reader can compare at a glance, which is the entire claim this band makes
   about re-ranking.

   The photographs ship unwashed, like every other image here: no overlay, no
   filter, no opacity. The well's own ground is --ground-2 and shows only for
   the instant before the file decodes.

   The column is a token rather than a number because the row height is
   pinned to it: a thumbnail taller than the row it sits in would break the
   one thing these two columns have to do, which is march level. */
:root { --rerank-thumb: 34px; }
.rerank__thumb {
  width: var(--rerank-thumb);
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--ground-2);
}
.rerank__thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
.rerank__metric { justify-self: flex-end; text-align: right; }
/* What the selected signal saw. The lit rows are the ones that signal
   actually moved; the rest keep their value and stay quiet, which is the
   same colour-weight gesture the rest of the site uses to mean "this one". */
.rerank__metric.is-lit { color: var(--ink); }

/* THE MOVE COLUMN CARRIES HUE, AND IT IS THE ONLY INTERFACE INK ON THIS SITE
   THAT DOES.

   E1 says every neutral is literally R = G = B and that all chroma on this
   site is photographic. This is an exception to the second half of that, made
   deliberately and scoped as narrowly as it can be: two colours, on one
   column, of one table, inside one demonstration.

   The argument for it is that a delta is the one value in this table a reader
   resolves before reading it. Everything else here is a quantity to be read;
   promoted-or-demoted is a direction to be seen, and every operator who would
   ever use this app already reads that pair of colours that way in every
   other tool they open. Rendering it in the same grey as the units column
   spends a glance on something a glance should answer.

   Both are dark enough to pass against white as body-weight text rather than
   as decoration, and they are used ONLY here. No fill, no chip, no badge: the
   glyph and its number take the colour and nothing else does. Sort direction
   is still carried by the glyph, so the column does not depend on hue alone.
   "No move" stays grey, which is correct: it is not a direction. */
.move--up   { color: var(--up); }
.move--down { color: var(--down); }
.rerank[data-signal="0"] .rerank__metric:not([data-metric="0"]),
.rerank[data-signal="1"] .rerank__metric:not([data-metric="1"]),
.rerank[data-signal="2"] .rerank__metric:not([data-metric="2"]),
.rerank[data-signal="3"] .rerank__metric:not([data-metric="3"]) { display: none; }

.rerank__notes { display: flex; flex-direction: column; gap: var(--space-xs); }
.rerank__notes p { display: flex; flex-wrap: wrap; gap: var(--space-2xs) var(--space-xs); }

/* ---- THE RUN ------------------------------------------------------------
   A fenced, single-play sequence. `is-run` is the one switch: it rewinds the
   finished state above into the state the run starts from — no chips, no
   metrics, no rationale, stages unlit — and the script plays it forward once
   and then removes it. Take the class away at any moment and the finished
   run is back, which is what makes this additive (F5) rather than a state
   machine the page depends on.

   Every duration in it is a ladder value: a stage computes for --anim-md, a
   row settles over --anim-md, a rationale line arrives --anim-sm after the
   one before it. The sequence adds no new number, only a count of steps. */
.rerank.is-run .app__run .tag,
.rerank.is-run .rerank__metric,
.rerank.is-run .rerank__notes p { opacity: 0; }
.rerank.is-run .rerank__notes p { translate: 0 10px; }
.rerank.is-run .app__run .tag.is-in,
.rerank.is-run .rerank__metric.is-in,
.rerank.is-run .rerank__notes p.is-in { opacity: 1; translate: 0 0; }
@media (prefers-reduced-motion: no-preference) {
  .rerank.is-run .app__run .tag,
  .rerank.is-run .rerank__metric,
  .rerank.is-run .rerank__notes p {
    transition: opacity var(--anim-md), translate var(--anim-md);
  }
}
/* The metric column is display:none for every unselected signal, so during
   the run it has to be the selected one that is merely transparent — or the
   column has no width and the rows re-flow underneath the FLIP. */
.rerank.is-run .rerank__colhead .rerank__metric { opacity: 1; }

@media (max-width: 768px) {
  .rerank__diff { grid-template-columns: 1fr; gap: var(--space-xl); }
}
@media (max-width: 600px) {
  /* Four columns do not fit a 390px row at a legible size, so the metric
     drops to its own line under the product name rather than squeezing it.
     The thumbnail keeps its column: it is the cell doing the most work at
     this width, because a name plus a garment is legible in a glance where
     two columns of names are not. The metric starts under the name rather
     than under the rank, so the rank and the picture stay the row's spine. */
  .rerank__colhead, .rerank__rows li { grid-template-columns: 2.5em var(--rerank-thumb) 1fr; }
  .rerank__metric { grid-column: 3; justify-self: flex-start; text-align: left; }
}

/* --------------------------------------------------------------------------
   PROSE
   -------------------------------------------------------------------------- */
.prose { display: flex; flex-direction: column; gap: var(--space-sm); align-items: flex-start; }
.prose p { max-width: 40em; color: var(--ink-muted); }

/* `.listnote` is deleted, not orphaned. It was the sentence under the ideas
   index explaining that the rows did not open yet; two of them open now, and
   the arrow in a row's meta says which. Dead rules are the quiet way a system
   stops being one. */

/* ==========================================================================
   TWO COMPONENTS THAT MOVED HERE FROM home.css

   The node canvas and the finish strip were the Creative band's (a) and (b)
   on the studio page. They are the imagery-pipeline article's now, and a
   component used by two page types belongs in the sheet both page types
   load. Nothing about either rule changed in the move; only the file did,
   and the band letters came off the headings because there is no band.
   ========================================================================== */

/* --------------------------------------------------------------------------
   THE NODE CANVAS

   A real node graph: cards in a grid, and hairline edges drawn between them
   by the script from their MEASURED positions. Drawing the edges in a static
   SVG was the obvious build and it is the wrong one — an SVG cannot reflow,
   so the six-across desktop graph and the one-across phone graph would be
   two drawings of the same pipeline, kept in step by hand. Measured edges
   mean there is one drawing and the layout is free to be whatever the grid
   makes it.

   ADDITIVE, on the same law as everything else that moves or draws here (F5).
   The resting state — no script — is the six cards in flow, in reading order,
   which is a readable pipeline with no lines in it. They used to carry 01 to
   06 as well; round seven took every leading ordinal off a label sitewide, and
   reading order is what the numbers were restating.
   `is-wired` is set by the script once the edges exist, and nothing in the
   page's correctness depends on it.
   -------------------------------------------------------------------------- */
.canvas__plane {
  position: relative;
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: var(--grid-gap);
  align-items: start;
}
.canvas__edges {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
}
.canvas__edges path {
  fill: none;
  stroke: var(--border);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

/* A node card is a hairline and one radius, like the app surface in the
   Tools band. Its ground is the page's own #FFF and it is declared rather
   than inherited, so an edge passes BEHIND a card instead of through its
   label — the edges are drawn under the cards, in a layer of their own. */
.node {
  position: relative;
  z-index: 1;
  display: flex; flex-direction: column; align-items: flex-start;
  gap: var(--space-2xs);
  padding: var(--space-xs);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--ground);
}
.node__thumb {
  width: 100%;
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--ground-2);
}
.node__thumb img { width: 100%; height: 100%; object-fit: cover; }
.node__note { max-width: 18em; }

/* The fan-out. Four variants is what makes this a graph rather than a chain,
   so the four are four real nodes with four real edges in and four out — not
   a list inside one card. The group's own label sits above them, unboxed,
   because the group is not a step. */
.fan {
  position: relative;
  z-index: 1;
  display: flex; flex-direction: column; align-items: stretch;
  gap: var(--space-2xs);
}
.node--mini { padding: var(--space-2xs) var(--space-xs); }

@media (max-width: 1024px) { .canvas__plane { grid-template-columns: repeat(3, 1fr); row-gap: var(--space-2xl); } }
@media (max-width: 600px)  {
  .canvas__plane { grid-template-columns: 1fr; row-gap: var(--space-lg); }
  /* In one column a node card is the full measure, and a 1:1 thumbnail in it
     is a 330px square — which makes the two nodes that carry a thumbnail
     four times the height of the four that do not, and the pipeline stops
     reading as a sequence of steps. The thumbnail is a thumbnail at every
     width; only here does it have to be told so. */
  .node__thumb { max-width: 40%; }
}

/* --------------------------------------------------------------------------
   THE FINISH STRIP

   One composition, four finishes, four frames of the recording in the band
   above. The label under each tile is the finish name and the two yarn codes
   read off that same frame — pattern yarn, then ground yarn. There is no
   colour swatch beside them on purpose: a swatch would be a filled box
   carrying hue, and this site has neither (A7, E1). The photograph IS the
   swatch.
   -------------------------------------------------------------------------- */
.finishes__row { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--grid-gap); }
.finishes__cell { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-3xs); }
.finishes__frame {
  width: 100%;
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--ground-2);
  margin-bottom: var(--space-2xs);
}
.finishes__frame img { width: 100%; height: 100%; object-fit: cover; }
@media (max-width: 600px) { .finishes__row { grid-template-columns: 1fr 1fr; row-gap: var(--space-lg); } }
