/* ============================================================================
   Stock — tree view, single-item detail pane
   ----------------------------------------------------------------------------
   The tree now selects ONE block at a time (`data-slab-group` is a block id, not
   a product), so the right pane holds a single item rather than a grid of them.
   A 190px card had to stack a 130px image over every attribute, which forced the
   whole spec set into an 11–12px wrapped run and left most of the pane empty.

   Here the item is a two-column figure: media on the left, attributes on the
   right at readable sizes, using the shared `.spec-list` pairing (label right,
   value left, tight) so this pane matches every other detail surface.
   ========================================================================== */

/* The media column is capped BOTH in px and as a share of the pane. A bare
   `minmax(280px, 440px)` starves the facts column, because this pane is not the
   viewport — the tree nav takes 300px first, so at a 1024px viewport the pane is
   ~487px and the facts column was left with 21px. The `min()` makes the media give
   ground as the pane narrows; below 1200px the two columns stack outright. */
.itm-view {
  display: grid;
  grid-template-columns: minmax(240px, min(440px, 42%)) minmax(0, 1fr);
  gap: 24px;
  align-items: start;
}

/* ---- media: one stage plus a reel of up to 4 photos and 2 videos ---------- */
.itm-stage {
  position: relative;
  margin: 0;
  border: 1px solid var(--color-border);
  border-radius: 7px;
  overflow: hidden;
  background: var(--color-surface-alt);
}
/* `cover`, not `contain`: these are photographs of physical stock, and letterboxing
   a block against blank card leaves the frame reading as an error state. */
.itm-stage__img {
  display: block;
  width: 100%;
  height: 300px;
  object-fit: cover;
  border: 0;
  border-radius: 0;
}
/* Stock carries one photo per product type and one per material, so the later
   slots re-show those two. The modifiers make them read as further angles rather
   than as the same file pasted twice; they come off when real per-item
   photography lands and each slot has its own source. */
.itm-stage__img.is-flip,
.itm-shot img.is-flip { transform: scaleX(-1); }
.itm-stage__img.is-zoom,
.itm-shot img.is-zoom { transform: scale(1.6); }
.itm-stage__img.is-zoom { transform-origin: 62% 38%; }
/* The caption rides a scrim rather than a solid bar: over a real photograph a
   bar would crop the image, while the scrim only darkens what sits under text. */
.itm-stage__cap {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 14px 10px 7px;
  font-size: 11px; color: #fff;
  background: linear-gradient(to top, rgba(15, 23, 42, .74), rgba(15, 23, 42, 0));
}
.itm-stage__play {
  display: none;
  position: absolute; inset: 0; margin: auto;
  width: 54px; height: 54px;
  border-radius: 50%;
  background: rgba(15, 23, 42, .55);
  color: #fff;
  align-items: center; justify-content: center;
}
.itm-stage.is-video .itm-stage__play { display: inline-flex; }
/* Filled, not stroked: at these sizes an outlined triangle reads as a cursor. */
.itm-stage__play svg { width: 24px; height: 24px; fill: currentColor; }

/* The reel holds up to six items (4 photos + 2 videos) and must stay on ONE row at
   any count — they share the width instead of taking a fixed one, so a two-photo
   block doesn't leave a ragged gap and a six-item block doesn't wrap. */
.itm-reel { display: flex; gap: 6px; margin-top: 8px; }
.itm-shot {
  position: relative;
  flex: 1 1 0; min-width: 0; max-width: 76px;
  padding: 0; line-height: 0;
  border: 1px solid var(--color-border);
  border-radius: 5px;
  background: var(--color-surface);
  cursor: pointer;
  overflow: hidden;
}
.itm-shot img { display: block; width: 100%; height: 44px; object-fit: cover; }
.itm-shot:hover { border-color: var(--color-primary); }
.itm-shot.is-active { border-color: var(--color-primary); box-shadow: 0 0 0 2px var(--color-primary-50); }
/* Video thumbs read as video before the icon is parsed: the wash is what tells
   them apart at a glance, the glyph and duration confirm it. */
.itm-shot--video::after { content: ""; position: absolute; inset: 0; background: rgba(15, 23, 42, .30); }
.itm-shot > svg { position: absolute; inset: 0; margin: auto; z-index: 1; width: 16px; height: 16px; color: #fff; fill: currentColor; }
.itm-shot__len {
  position: absolute; right: 3px; bottom: 3px; z-index: 1;
  padding: 0 3px;
  font-size: 10px; line-height: 1.5; color: #fff;
  background: rgba(15, 23, 42, .78);
  border-radius: 3px;
}

/* ---- attributes ---------------------------------------------------------- */
.itm-facts__head { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
.itm-facts__head .btn { margin-left: auto; }
/* One step up from the shared .spec-list (12/13) — this is the pane's primary
   content, not a summary strip, and 14px is the top of the type ladder. */
.itm-spec { gap: 15px 12px; }
.itm-spec dd { font-size: 14px; }
.itm-spec dd.itm-spec__val { font-size: 15px; font-weight: 700; color: var(--color-primary-700); }

@media (max-width: 1200px) {
  .itm-view { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 900px) {
  .itm-stage__img { height: 220px; }
}

/* ---- detail slide-over: the product type's attribute block ----------------
   Rendered from STOCK_TYPES in stock.html, so the markup is only a mount.
   Key above value, not across from it: an attribute is a definition, and a row
   with the label left and the value pushed to the far right makes the eye track
   whitespace to pair them (memory.md rule 6). Two columns because these lists
   run to eight entries on Tiles and ten on Remnants, and one column would push
   Location and Valuation below the fold on every open. */
.so-attrs { margin-bottom: 14px; }
.so-attrs__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px 16px;
}
.so-attr { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.so-attr__k { font-size: 12px; line-height: 1.35; color: var(--color-text-muted); }
/* Body data, so no mono and no step below the 13px row line (rules 4 and 8). */
.so-attr__v { font-size: 13px; line-height: 1.4; color: var(--color-text); overflow-wrap: anywhere; }
/* The one line on the block that is not the attributes themselves, so it takes
   the step down to 12px rather than sitting level with the values above it. */
.so-attrs__note { margin-top: 10px; font-size: 12px; line-height: 1.45; color: var(--color-text-muted); }
.so-attrs__note a { color: var(--color-primary-700); font-weight: 500; }
.so-attrs__note svg { width: 12px; height: 12px; vertical-align: -2px; }

/* A sample's issue history — who is holding a piece and when it went out. */
.so-issues { width: 100%; }
.so-issues td, .so-issues th { vertical-align: middle; }

/* Slide-over furniture. The six older panels inline all of this; memory.md rule 13
   forbids the style attribute, so the two new panels use classes and the older ones
   pick them up as they are touched. */
.so-sec { margin: 0 0 6px; }
.so-sec--gap { margin: 14px 0 6px; }
.so-idt { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.so-idt__thumb { width: 56px; height: 56px; flex: none; }
.so-idt .main { font-size: 15px; }
.so-loc { display: flex; align-items: flex-start; gap: 6px; flex-wrap: nowrap; margin-bottom: 14px; }
.so-loc__pin { width: 15px; height: 15px; flex-shrink: 0; color: var(--color-text-subtle); }
.so-val {
  border: 1px solid var(--color-border);
  background: var(--color-surface-alt);
  border-radius: 5px;
  padding: 10px 12px;
  margin-bottom: 14px;
}
.so-val__row { display: flex; align-items: center; justify-content: space-between; }
.so-val__row + .so-val__row { margin-top: 4px; }
.so-val__row strong { font-weight: 600; }
.so-moves { display: flex; flex-direction: column; gap: 8px; }
.so-move { display: flex; align-items: center; gap: 8px; }
.so-move svg { width: 14px; height: 14px; flex: none; color: var(--color-text-muted); }
.so-move--in svg { color: var(--color-success); }
.so-move--out svg { color: var(--color-danger); }
.so-move--flag svg { color: var(--color-warn); }
.so-move--hold svg { color: var(--color-info); }
.so-move__when { margin-left: auto; }
.so-ledger { margin-top: 8px; }
.so-foot-gap { flex: 1; }

@media (max-width: 768px) {
  /* The action row is where this page's per-type work happens, and it just gained Reorder,
     Cut Into Bundles, Send To Client and Mark Returned. At .btn-sm it renders 30px tall —
     under the 44px touch minimum — so the row is raised on touch widths only. Desktop keeps
     the compact footer. */
  .slideover-foot .btn { min-height: 44px; }

  /* 20px glyph, 44px reach. The row is not re-laid out for it — the box is
     absolute and the cell keeps its height. */
  .share-btn::after { inset: -12px; }
}

@media (max-width: 480px) {
  /* One column: two 13px values in a 100vw slide-over leaves each about 150px,
     which breaks "Warm white · gold and grey veining" across four lines. */
  .so-attrs__grid { grid-template-columns: minmax(0, 1fr); }
}

/* ============================================================================
   The piece's own facts — bookmatch, and the sum behind a derived weight
   ----------------------------------------------------------------------------
   Two things a Block, Bundle or Slab carries that the Catalog Item cannot:

   • A BOOKMATCH is an IDENTITY, not a measurement. "This slab is the mirror of
     SLB-204-02" is what a buyer asks for by name, so the mark rides the id's own
     line rather than taking a column — a Bookmatch column would have printed Yes
     down eight rows without once saying which slab the Yes refers to. On a bundle
     the mark is bare, because the whole run opens like a book; on a slab it
     carries the pair's own id.

   • A DERIVED WEIGHT has to show its working. Area × thickness × density is a sum
     the reader did not watch happen, and a figure that cannot be traced back to
     its inputs is a figure nobody corrects — they just stop believing it.
   ========================================================================== */

/* `.cell-stack` is a flex COLUMN, so an id and its mark need a row of their own
   or the chip drops to the line below and reads as a second fact about the row. */
.cell-idrow {
  display: flex; align-items: center; gap: 6px;
  min-width: 0; flex-wrap: wrap;
}

.bm-chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 6px;
  border: 1px solid #D6E2FA;
  border-radius: 5px;
  background: var(--color-primary-50);
  color: var(--color-primary-700);
  font-size: 11px;
  font-weight: 500;
  white-space: nowrap;
}
.bm-chip svg { width: 11px; height: 11px; flex: none; }
/* The border is `.badge-primary`'s own literal rather than a token, because the chip sits
   inline with those badges and a token that resolved differently would put two blues on
   one line. That means it also needs `.badge-primary`'s evening override — app.css:2190
   does exactly this, and without the pair the chip keeps a light-theme edge on a dark one.
   The light theme only retokens `:root` and leaves `.badge-primary` alone, so it
   needs nothing here. */
:is([data-theme="evening"], [data-theme="elegant"]) .bm-chip { border-color: var(--color-border-strong); }
/* The same mark inside a .badge on a detail panel, where the badge owns the ground. */
.bm-ico { width: 12px; height: 12px; }

/* The sum sits UNDER the value it produced and steps down one size from the row
   above it (13px → 11px, memory rule 4): it qualifies the weight, it is not a
   second fact level with it. Left-aligned against the label column, because it
   explains the label's answer rather than being another answer. */
.so-derived {
  margin: -2px 0 2px;
  font-size: 11px;
  color: var(--color-text-subtle);
  font-variant-numeric: tabular-nums;
}

/* Slabs Info — the yard's own sentences about a run, sitting directly above the
   slabs they describe. A paragraph and not a field grid on purpose: what there is
   to say is the cut order, which slabs stay together and how the veining runs, and
   none of those is a value. Reads at body size; the tree below carries the numbers. */
.so-note {
  margin: 0 0 14px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--color-text-muted);
}

@media (max-width: 480px) {
  /* `.form-row` is `display:flex` with `flex:1` fields and NO global collapse — the comment in
     app.css claiming "the global .form-row collapse" refers to `.field-grid`, which is a
     different system. So on a phone a two-field row keeps two columns, and in a full-width
     slide-over that is ~165px each.

     Two of the rows added here carry a derivation under the value — "486 sqft × 2 cm ×
     2,710 kg/m³ — from the Catalog Item's density" — and a sum broken across five lines stops
     being a sum. The rows stack, and the sums read.

     Page-scoped and slide-over-scoped on purpose: this fixes the pre-existing pairs in these
     three sliders too (Volume / Weight, Price / Block Total, Variant / Grade), which is the
     right answer for all of them, but it is not a claim about `.form-row` anywhere else. */
  .slideover .form-row { flex-direction: column; gap: 14px; }
}

/* ---- Piece Facts rows -----------------------------------------------------
   A label and its value on one row. Was 28 copies of
   `style="justify-content:space-between;"` across the three detail panels, which
   memory rule 14 forbids and which meant the row's layout had 28 authors.

   Kept visually as it was — this session added fields to these blocks, it did not
   redesign them. Worth naming when they are next revisited: rule 6 says a value
   spread to the far edge of its row loses its association with the label, and
   these are exactly that shape. The class is now the one place to change it. */
.so-fact {
  display: flex; align-items: center; gap: 8px;
  justify-content: space-between;
}
.so-fact--tight { margin-top: 4px; }

/* Memory rule 5: switch on the LEFT, label beside it, label non-bold. The 10px is
   the rule's gap and belongs here rather than on each element. */
.so-switch { gap: 10px; }

/* ============================================================================
   The whole Catalog Item, on the piece
   ----------------------------------------------------------------------------
   The attribute grid was only ever a fifth of what the Catalog Item says. A person
   standing at a block asking "what IS this" wants the record the catalog page
   shows: what it is called and who makes it, what it is, which combinations it is
   sold in, what it is priced by, in which units.

   All of it sits INSIDE `.so-attrs`, above the one note that already says where it
   comes from and where to change it — so the block reads as a single borrowed
   record rather than as five sections that happen to be adjacent. Nothing here is
   editable; Stock cannot change a product from a piece.
   ========================================================================== */

/* A group heading inside the block. Carries `.table-title` and sets NO size of its own:
   every section heading in the portal is that one treatment (11px, uppercase, 700), and a
   12px override here made these sub-headings render LARGER than the block title above
   them — a child outgrowing its parent. Only the rhythm is ours. */
.so-sub { margin: 14px 0 8px; }
/* The first group follows the attribute grid, which already has the block's spacing. */
.so-attrs__grid + [data-attr-more] > .so-sub:first-child { margin-top: 16px; }

/* Prose, so it reads at body size and not at label size (rule 4: 13px is the body
   step, and a paragraph is the least abbreviated thing on the panel). */
.so-desc {
  margin: 0;
  font-size: 13px;
  line-height: 1.55;
  color: var(--color-text);
}
.so-tags {
  display: flex; flex-wrap: wrap; gap: 6px;
  margin-top: 10px;
}
/* The variant matrix and its summary are the shared read-only rendering from
   app.css (`.variant-row--ro`) — the same one the catalog detail pages use, so a
   variant reads identically whichever page you met it on. Only the top margin is
   ours, because here it follows a heading rather than a field grid. */
[data-attr-more] .variant-matrix { margin-top: 6px; }

/* ============================================================================
   Product view — the pane when a MATERIAL is selected rather than a piece
   ----------------------------------------------------------------------------
   A product row used to do one thing: expand. Everything it names lived one level
   down, borrowed onto a piece — which meant a product the yard holds NONE of could
   not be read at all. The row now selects, and the pane answers "what is this
   material" instead of "what is this block".
   ========================================================================== */
/* EXACTLY `.itm-view`'s grid, and deliberately the same numbers rather than a second
   set: this pane sits in the same column as a block's or a bundle's figure and switches
   with it, so a media column that started at a different width would visibly jump every
   time you moved between a product and one of its pieces. The media column is capped in
   px AND as a share for the reason .itm-view already documents — the pane is not the
   viewport, the nav takes 300px first. */
.prod-view {
  display: grid;
  grid-template-columns: minmax(240px, min(440px, 42%)) minmax(0, 1fr);
  gap: 24px;
  align-items: start;
  padding: 2px 0 4px;
}
.prod-view__facts { min-width: 0; }

/* No photograph in the library. The empty state keeps the stage's footprint so the two
   columns still line up, and says what is missing instead of leaving a hole. */
.prod-media__none {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px;
  height: 300px;
  border: 1px dashed var(--color-border-strong);
  border-radius: 7px;
  background: var(--color-surface-alt);
}
.prod-media__mark { width: 56px; height: 56px; font-size: 16px; }
.prod-media__msg { font-size: 12px; color: var(--color-text-subtle); }

.prod-view__head {
  display: flex; align-items: center; gap: 10px;
  margin-bottom: 8px;
}
/* 15px is this pane's established top value size — `.itm-spec__val` and `.so-idt .main`
   both sit there. Going above it would make the product name the largest text on a page
   whose own header is 11px uppercase. */
.prod-view__name { font-size: 15px; font-weight: 600; color: var(--color-text); line-height: 1.3; }
/* The description leads the column, so it gets the room a lede needs rather than the
   tight 0 margin a mid-block paragraph takes. */
.prod-view__lede { margin: 0 0 2px; }

/* Priced-by is one value and Units is three: alone in a two-column grid each left most
   of a row empty, which is what made the first pass read as a form full of gaps. */
.prod-view__pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 16px;
}
/* Inside a half-width column a two-up grid would be ~120px a cell, so these stack. */
.prod-view__pair .so-attrs__grid { grid-template-columns: minmax(0, 1fr); }

/* Same step down as .so-attrs__note: the line that says where the record comes from
   is not one of the record's own values. */
.prod-view__foot {
  margin: 16px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--color-border);
  font-size: 12px; line-height: 1.45;
  color: var(--color-text-muted);
}
.prod-view__foot a { color: var(--color-primary-700); font-weight: 500; }
/* The first heading follows the tags row, which carries its own spacing. */
.prod-view__facts > .so-sub:first-of-type { margin-top: 14px; }

/* The same two steps `.itm-view` takes, at the same widths — the panes swap in place, so
   they have to break at the same points or the layout shifts on selection. */
@media (max-width: 1200px) {
  .prod-view { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 900px) {
  .prod-media__none { height: 220px; }
}
@media (max-width: 620px) {
  /* Two half-columns of label/value below this are ~130px each — narrower than
     "Public / Display Name" needs, so the pair unstacks. */
  .prod-view__pair { grid-template-columns: minmax(0, 1fr); }
}

/* A selected PRODUCT row reads the same as a selected piece row (.slab-nav-bundle
   .active) — one selection model, so the eye does not have to learn two. */
.slab-nav .tree-row[data-product].is-selected { background: var(--color-primary-50); }
.slab-nav .tree-row[data-product].is-selected .tree-label { color: var(--color-primary-700); font-weight: 600; }
.slab-nav .tree-row[data-product] { cursor: pointer; }
.slab-nav .tree-row[data-product]:hover { background: var(--color-surface-alt); }

/* Out-of-stock rows: the material is still sold, so the record reads normally and
   only the COUNT is muted. Greying the whole row would say the product is retired,
   which is a different fact and not this one. */
.slab-nav [data-oos] .tree-meta { color: var(--color-text-subtle); font-style: italic; }
/* Same treatment on a bulk tab, where the row is a table row rather than a tree row: the
   COUNT is the only cell that is different, so it is the only cell that reads differently.
   Scoped to the count itself and not to every `.tnum` in the row, or it would take the price
   with it. The product is not changed by there being none of it, and greying the whole row
   would say retired — which is a different fact and a different decision. */
.oos-count { color: var(--color-text-subtle); font-style: italic; }

/* ---- Manage Images ----------------------------------------------------------
   Sits under the reel, inside the media column, because it manages what is above it and
   nothing else. Full width of the column, which aligns it with the STAGE rather than with the
   reel directly above it: `.itm-shot` caps at 76px, so a three-shot reel only runs ~240px
   of a 344px column. Matching the short thing would leave the button hanging in the middle.
   Media is no longer captured at intake, so this is the only way in — which is why it is a
   labelled control and not an icon.

   Full width and centred are `.btn-block`, the shared class, not two properties re-derived
   here — the hand-rolled pair had missed justify-content and left the icon and label
   sitting hard against the left edge of a 344px button. All this adds is the gap. */
.itm-media__manage { margin-top: 8px; }

/* ---- Share ----------------------------------------------------------------
   Every item on this page — a block, a bundle, a slab, a remnant, a sample, a
   SKU — carries the address of its own public page, and this is how it comes
   off the screen. It rides beside the item's NAME rather than sitting in a
   column of its own, because the last column is the status and a share control
   is not a fact about the piece; and a column would have to be paid for in
   every one of the seven tables.

   Deliberately quiet: 20px, muted, no border, no fill. It is the third thing a
   row is for, after reading it and opening it, and it should not out-shout the
   id it sits next to. A click no longer flashes green: it opens the dialog that
   asks which client the piece is for, which is answer enough. */
.share-btn {
  width: 20px; height: 20px;
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0;
  border: 0; border-radius: 5px;
  background: transparent;
  color: var(--color-text-subtle);
  cursor: pointer;
  transition: background 150ms, color 150ms;
}
.share-btn svg { width: 14px; height: 14px; }
.share-btn:hover { background: var(--color-primary-50); color: var(--color-primary-700); }

/* WHAT YOU CAN PRESS IS WIDER THAN THE MARK — the same trick `.add-row-btn` uses.
   The glyph stays 20px so it does not crowd the id it sits beside; the press
   carries out to 34px on a mouse and to the 44px minimum on a touch width, where
   a 20px target beside a link is a coin-toss between copying and opening. */
.share-btn { position: relative; }
.share-btn::after { content: ""; position: absolute; inset: -7px; }

/* Everywhere a whole item is already the subject — a detail panel's action row,
   the tree pane's head, a stock page's own action row — the control is a
   labelled `.btn` reading "Share" instead. There is room for the word out
   there, and it is one of the things you came to do rather than a per-row
   convenience, so it belongs beside Transfer and Print QR and not in a corner. */

