/* ============================================================================
   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;
}
/* The unit pairs are three short facts ("Area / sqft"), so they sit side by side and the
   band stays one row tall — stacked, Units ran three rows and left a hole beside the
   single Priced-by pair. auto-fit drops them back to a column as the pane narrows,
   instead of breaking at one fixed width. */
.prod-view__pair .so-attrs__grid { grid-template-columns: repeat(auto-fit, minmax(96px, 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 heading is a band, not a floating label: the hairline takes the leftover row —
   the same device concierge-floor's group headings use. Scoped to the product pane;
   the slide-overs keep their own rhythm. */
.prod-view__facts .so-sub { display: flex; align-items: center; gap: 10px; }
.prod-view__facts .so-sub::after { content: ""; flex: 1; min-width: 24px; height: 1px; background: var(--color-border); }

/* Label-value pairs carry the value at 500 (branding rule) — at 400 the key and its
   value read as two lines of the same grey block. */
.prod-view__facts .so-attr__v { font-weight: 500; }

/* A variant combination is a token on the axis's own key line. A combination the
   product declares but is not selling keeps its token — dashed, muted, and saying so —
   because "declared but off" is a different fact from "never declared". */
.pv-variants { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; }
.pv-variant {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 10px;
  border: 1px solid var(--color-border-strong);
  border-radius: 5px;
  background: var(--color-surface);
  font-size: 12px; font-weight: 500; line-height: 1.4;
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
}
.pv-variant.is-off { border-style: dashed; color: var(--color-text-subtle); background: var(--color-surface-alt); }
.pv-variant__flag { font-size: 11px; color: var(--color-text-subtle); }

/* 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. */

/* ================= The New Product panel =================================
   The wizard slides in over the yard instead of replacing it. 80% and not 100% on
   purpose: the strip of page left showing on the left is what says you are still
   ON Stock and this is a thing you opened, not somewhere you navigated to. A
   full-width overlay is indistinguishable from a page, and then Escape and the
   backdrop stop being obvious ways out.

   `.slideover-overlay` already starts below the top bar, so the header stays put
   and the panel is 80% of the width only — the vertical inset is the shell's.

   The frame carries the wizard itself (`stock-product-wizard.html?embed=1`), which
   is the same file the page-level route opens. One wizard, two ways in. */
.slideover-wizard { width: 80%; }
/* The head holds the only close button, so it is never the thing that gives way when the
   panel is short. `.slideover` is a flex column and a head left to default `flex-shrink: 1`
   is a head that can be compressed out from under its own X. */
.slideover-wizard .slideover-head { flex: none; }
.slideover-wizard .np-frame {
  flex: 1; min-height: 0; width: 100%; border: 0; display: block;
  background: var(--color-surface);
}
/* A phone has no room to spend on showing what is behind, so there the panel takes
   the width it needs. The 80% rule is about a desktop, where 20% is a legible margin
   and on a 390px screen it is 78 pixels of nothing. */
@media (max-width: 720px) { .slideover-wizard { width: 100%; } }

/* ================= Who is interested (so-interest) ========================
   The demand side of a stock row: who has asked for this material and who has
   only said they want it. Narrower than Locate in Yard because it is a list and
   not a map — nothing here needs the width. */
#so-interest .slideover { width: min(448px, 100%); }
.int-tabs { margin-bottom: 4px; }
/* Why the list is empty and what the other side holds — an empty panel that says
   nothing reads as a page that failed to load. */
.int-empty { margin: 18px 0 0; font-size: 12px; color: var(--color-text-subtle); }

/* Two columns: WHO, and WHEN. Where the interest came from used to close the
   row as a chip on the right; it is an icon in front of the name instead. The
   source and the place it opens are the same fact, and the name is what a
   reader points at — so the name IS the link, and the row gives the space back
   to the contact line. */
.int-list { display: flex; flex-direction: column; }
.int-row {
  position: relative;
  display: grid; grid-template-columns: 1fr auto;
  align-items: center; gap: 16px; padding: 12px 0;
}
/* A short mark and not a rule across the panel: at full width it reads as a
   table band, and this is a list of people, not a table. The general sibling
   selector is what makes it survive filtering — the first row SHOWING carries
   no mark whichever tab is open. */
.int-row:not([hidden]) ~ .int-row:not([hidden])::before {
  content: ''; position: absolute; top: 0; left: 0; width: 45%;
  border-top: 1px solid var(--color-border-strong);
}
/* No underline: the icon, the weight and the colour on hover already say it is
   a link, and a rule under a client's name reads as emphasis on the name. The
   contact line clears the icon so the two lines of text share one left edge. */
.int-name {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 13px; font-weight: 600; color: var(--color-text); text-decoration: none;
}
.int-name:hover { color: var(--color-primary-700); }
.int-name svg { width: 14px; height: 14px; flex: none; color: var(--color-text-subtle); }
.int-name:hover svg { color: var(--color-primary); }
.int-contact { margin-top: 3px; padding-left: 21px; font-size: 11px; color: var(--color-text-subtle); }
/* The date is the value and the scope qualifies it, so the scope sits UNDER —
   side by side the two read as one crowded line. */
.int-when { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; text-align: right; }
.int-date { font-size: 12px; color: var(--color-text-muted); white-space: nowrap; }
.int-scope { font-size: 11px; color: var(--color-text-subtle); white-space: nowrap; }
/* WHO NAMED THIS PIECE, told apart at a glance. The generic rows say the
   material in plain subtle text; the ones about the piece in front of you take
   the info wash and the piece's own ID, so the two kinds of interest are one
   look apart in a list that is otherwise all client names. */
.int-scope--this {
  padding: 2px 7px; border-radius: 7px;
  background: color-mix(in oklab, var(--color-info) 14%, var(--color-surface));
  color: var(--color-text); font-weight: 600;
}

/* ================= Locate in Yard: the warehouse is the heading ============
   The same material sits in more than one yard as a matter of course — a slab
   pulled for QC is in the QC Hold Area, not in the slab yard it came from — so
   every map and every walk is filed under the yard it belongs to, with the code
   beside the name. A rack number with no yard over it is not an address. */
.loc-wh { border: 1px solid var(--color-border); border-radius: 7px; overflow: hidden; margin-bottom: 14px; }
.loc-wh:last-child { margin-bottom: 0; }
.loc-wh__head {
  display: flex; align-items: center; gap: 9px;
  padding: 9px 12px; background: var(--color-control);
  border-bottom: 1px solid var(--color-border);
}
.loc-wh__head svg { width: 15px; height: 15px; color: var(--color-primary); flex: none; }
.loc-wh__name { font-size: 13px; font-weight: 600; }
.loc-wh__place { font-size: 11px; color: var(--color-text-subtle); }
.loc-wh__count { margin-left: auto; font-size: 11px; color: var(--color-text-muted); }
/* Inside a section the map is the section's own body — its border would be a
   second frame a millimetre inside the first. */
.loc-wh .loc-map { border: 0; border-radius: 0; }
.loc-wh .loc-list { margin: 0; padding: 12px; }

/* A BLOCK HAS NO RACK, so it gets facts instead of a map: the lot, the row and
   the place it stands, which is what a person walking out to it needs. */
.loc-facts { margin: 0; padding: 12px; display: grid; grid-template-columns: auto 1fr; gap: 8px 16px; }
.loc-facts > div { display: contents; }
.loc-facts dt { font-size: 12px; color: var(--color-text-subtle); }
.loc-facts dd { margin: 0; font-size: 13px; font-weight: 500; }
.loc-note { margin: 0; padding: 0 12px 12px; font-size: 11px; color: var(--color-text-subtle); }

/* ================= Damaged, Sold and Out of Stock ==========================
   They used to hold the right-hand end of the product-type strip; the status chips
   took that spot (app.css `.tabs__tools`, user 2026-10-11), and these drop into each
   panel's first row, on its right, right under the chips. A tree view already opens
   on a row (its `+`); a table view opens on its header, so it gets a row of its own. */
.stk-switches { display: inline-flex; align-items: center; gap: 16px; flex: none; }
.stk-switch-row { border-radius: 0; }

/* ================= The walk-in strip ======================================
   The header's live-strip seat, holding the client who is in her showroom right
   now. It is the Next-Up strip's ARRIVED state, reused whole — every box, the
   2px frame, the card hung under it and the two chips come from `.nextup` in
   app.css, and the markup wears those class names on purpose.

   ONE DECLARATION BLOCK IS THE WHOLE DIFFERENCE. The strip's colour is not
   written anywhere in its rules; it is read from `--heat`, which the heat
   classes set as the clock runs down. Nothing here is a clock, so the ramp is
   simply told what it is: the platform's VIP gold, the colour it already spends
   on a VIP everywhere else it names one. Red was the call's, and it was right
   for a call — a red bar standing over a stock list reads as something wrong
   WITH THE STOCK, which is the one thing it must not say. Green said the same
   in reverse, and said it about the stock rather than about the client.

   Specificity is deliberate: one class, so the `.nextup.is-hot` pair and its
   two-class siblings would still outrank it. The strip never wears them — but
   a later hand adding a countdown here should get the countdown's colours, not
   a silent draw. */
.nextup--walkin {
  --heat: var(--color-vip);
  --heat-50: var(--color-vip);
  --heat-bg: var(--color-warn-50);
}

/* Assist goes somewhere, so it is an anchor and not a button. It borrows Done's
   box entire; the only thing an anchor needs telling is that this platform does
   not underline a control. */
.nextup--walkin a.nextup__done { text-decoration: none; }
