/* ==========================================================================
   WASTELAND WRECKER 3D — doll.css
   THE DRESS-UP SCREEN's paint, and nothing else's.

   WHY THIS IS A SEPARATE FILE. style.css belongs to another station
   (docs/HUMANOID.md §9: nobody writes game/style.css) and this screen arrived
   after it. A second sheet costs one <link>; editing a file two agents are
   mid-flight on costs a merge conflict and somebody's afternoon.

   THE COLLISION RULE. Not one selector below also appears in style.css — the
   check is grep-level and it is a gate (docs/HUMANOID.md §11, Builder 4 item
   4). What this screen REUSES it reuses by wearing style.css's classes on its
   own markup (.shop-box, .shop-head, .shop-purse, .purse-num, .purse-lbl,
   .kicker, .shop-msg, .shop-foot, .shop-hint, .btn, .btn.primary,
   .garage-back, .g-label, .g-sub, .g-lock, .g-input, .gcard, .si-top,
   .si-name, .si-desc, .hidden) exactly as the garage reuses the salvage bay's
   chrome. Everything here is the DELTA: the scrim, the doll, the rack, the
   readout, and the compaction.

   Every token comes from style.css's `body.wrecker3d` block — charcoal, rust,
   blood, bone, bone-dim, amber, chrome, plate-edge, display/body/mono. This
   sheet declares no colour of its own except the two alpha washes that only
   make sense against a plate, and one visor black the shell never needed a
   name for.

   PALETTE OVERRIDE, deliberately absent: dollui.js paints the SVG figure with
   an explicit `fill` attribute per shape as well as a `.d-mat-*` class. SVG's
   initial fill is BLACK, so a figure that needed this file to be legible would
   be a black blob without it. The classes below only RETHEME what already
   reads.
   ========================================================================== */

/* ================================================================== the scrim
   Same construction as #shop3d / #garage3d — absolute, inset 0, flex-centred —
   at z-index 47, one above the garage, because the kit is opened FROM the
   garage and has to sit over it. (Bay 45, garage 46, dev console 70.) */
body.wrecker3d #doll3d {
  position: absolute;
  inset: 0;
  z-index: 47;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: radial-gradient(90% 80% at 50% 45%, rgba(26, 21, 18, 0.9), rgba(15, 12, 10, 0.96));
}
body.wrecker3d #doll3d.hidden { display: none; }

/* Wider than the garage: a doll, a 142-row rack and 25 attributes do not read
   in a 64rem box, and a dress-up screen you scroll blind is one you skip. */
body.wrecker3d .doll-box { width: min(84rem, 97vw); max-height: 92svh; }

/* The panel is the scroll container on a phone and a row of independently
   scrolling columns on a desktop, which is the whole reason it is a grid and
   not three stacked blocks: the rack is 40 rows long and the readout must stay
   on screen while you walk it. `min-height: 0` on both is the grid-overflow
   incantation — without it a grid child refuses to shrink below its content
   and the columns push the panel past the viewport instead of scrolling. */
body.wrecker3d #doll3d .d-panel {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.7rem;
  /* `flex: 1 1 auto` + `min-height: 0` are what give the panel a DEFINITE height inside the
     .shop-box column. Without the definite height the columns below have nothing to scroll
     against and simply grow, and the box clips them with no way to reach the bottom. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding-right: 0.2rem;
  /* STACKED: EVERY ROW IS AS TALL AS ITS CONTENT, and the PANEL is the scroller.
     Measured at 800x480 before this line existed: the three rows came out 103.5 px EACH out of
     a 327 px panel, holding 300 / 3,732 / 544 px of content — so the rack painted straight over
     the stats column and 3,517 px of it could not be reached by any scroll. The cause is two
     correct rules meeting: `.d-col { min-height: 0 }` is required by the WIDE layout (a grid
     child refuses to shrink below its content without it, and there each column scrolls itself),
     and it also drops each row's minimum contribution to zero — so `auto` rows happily collapse
     and share the container's height equally. Naming the row sizing here is the fix; the wide
     layout names its own below, so neither has to rely on the default meaning what it wants. */
  grid-auto-rows: max-content;
}
body.wrecker3d #doll3d .d-col { min-width: 0; min-height: 0; }

@media (min-width: 58rem) {
  body.wrecker3d #doll3d .d-panel {
    /* The doll column got wider when the doll got real: a 3D figure on a lit puck needs a plate,
       not a postage stamp. The rack keeps 1fr — 142 rows still rule the middle. */
    grid-template-columns: minmax(19rem, 26rem) minmax(19rem, 1fr) minmax(15rem, 20rem);
    /* WIDE: ONE row, exactly the panel's height, so each column scrolls INSIDE it. Stated
       rather than inherited, because the stacked layout above now names max-content and a
       reader should not have to work out which one wins here. minmax(0, 1fr) is the pair to
       min-height: 0 — the 0 floor is what lets the row shrink to the panel instead of to its
       3,700 px of rack. */
    grid-auto-rows: minmax(0, 1fr);
    overflow: hidden;
  }
  body.wrecker3d #doll3d .d-col { overflow-y: auto; padding-right: 0.25rem; }
  /* Room for the turntable. >= 140px is ALSO the wire that turns the viewport on — dollui
     measures this box and only instantiates GL when a rig would actually read at this size
     (docs/DOLL_CONTRACT.md §3.2's small-figure rung). The coarse-pointer compaction below stays
     at 52–86px, so a phone keeps the SVG doll by construction. */
  body.wrecker3d #doll3d .d-figwrap { width: clamp(150px, 15vw, 230px); }
  body.wrecker3d #doll3d .d-figure { min-height: 300px; }
}

/* ================================================================== the doll */

/* Rail, figure, rail. The figure takes what the chips do not, down to a floor
   where a person still reads as a person. */
body.wrecker3d #doll3d .d-stage {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: 0.35rem;
  align-items: start;
}
body.wrecker3d #doll3d .d-figwrap {
  /* Showcase-sized by DEFAULT (2026-08-16, operator): the doll IS the screen's point,
     and the old 58-104px stamp read as a postage stamp of a box man. */
  width: clamp(150px, 22vw, 250px);
  align-self: stretch;
  display: flex;
  align-items: stretch;
  justify-content: center;
  background: radial-gradient(65% 60% at 50% 40%, rgba(58, 44, 34, 0.75), rgba(15, 12, 10, 0.9));
  border: 1px solid var(--plate-edge);
  clip-path: var(--corner-cut);
  padding: 0.3rem 0.15rem;
}
body.wrecker3d #doll3d .d-figure { width: 100%; height: auto; min-height: 300px; display: block; }

body.wrecker3d #doll3d .d-rail { display: grid; gap: 0.25rem; align-content: start; }
/* Implant, trinket and discipline have no socket and nothing drawn, so their
   chips sit under the figure rather than pointing at a part of the body that
   does not exist. Two across, because there are three of them and one lonely
   full-width chip reads as a mistake. */
body.wrecker3d #doll3d .d-rail-bottom {
  grid-template-columns: 1fr 1fr;
  margin-top: 0.35rem;
}

/* A chip is the CONTROL. The markers on the figure only echo it, which is why
   the chip and not the marker carries the 44 px touch floor. */
body.wrecker3d #doll3d .d-slot {
  font: inherit;
  cursor: pointer;
  text-align: left;
  color: var(--bone);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(0, 0, 0, 0.32));
  border: 1px solid #3a322c;
  border-radius: 6px;
  padding: 0.24rem 0.34rem;
  min-height: 44px;
  display: grid;
  grid-template-columns: 18px 1fr;
  grid-template-areas: "name name" "icon item";
  gap: 1px 0.3rem;
  align-content: center;
  width: 100%;
}
body.wrecker3d #doll3d .d-slot:hover { border-color: var(--blood); }
body.wrecker3d #doll3d .d-slot.on { border-color: var(--amber); background: rgba(245, 166, 35, 0.09); }
/* The keyboard cursor gets its own unmissable ring: the hover border alone is
   too close to it to serve as the only "you are here" signal. The bay, the
   garage and now this screen have all learned that separately. */
body.wrecker3d #doll3d .d-slot.sel { outline: 2px solid var(--amber); outline-offset: 1px; }
body.wrecker3d #doll3d .d-slot-name {
  grid-area: name;
  font-family: var(--display);
  font-size: 0.55rem;
  letter-spacing: 0.16em;
  color: var(--bone-dim);
}
body.wrecker3d #doll3d .d-slot.filled .d-slot-name { color: var(--amber); opacity: 0.8; }
body.wrecker3d #doll3d .d-slot-item {
  grid-area: item;
  font-size: 0.66rem;
  line-height: 1.15;
  color: var(--bone);
  /* break-word, NOT anywhere: `anywhere` counts every character as a break opportunity when the
     grid computes min-content, so the rail's 1fr column shrank to a sliver and every name broke
     mid-word — the screen shipped reading "SHOULDE R RAG". break-word wraps at spaces first and
     only splits a word that genuinely cannot fit a line on its own. */
  overflow-wrap: break-word;
}
body.wrecker3d #doll3d .d-slot-item.dim { color: var(--bone-dim); font-style: italic; }
body.wrecker3d #doll3d .d-slot .d-icon { grid-area: icon; }

/* The markers. Lit from either end — hovering a chip lights its marker and
   tapping a marker chooses its chip — so the two halves of the doll are one
   thing. `.d-ring` is painted `transparent` by dollui.js on purpose: this is
   the one piece of the figure that is decoration, so it is the one piece
   allowed to need this file. */
body.wrecker3d #doll3d .d-mark { cursor: pointer; }
body.wrecker3d #doll3d .d-mark .d-ring { transition: stroke 90ms linear; }
body.wrecker3d #doll3d .d-mark:hover .d-ring,
body.wrecker3d #doll3d .d-mark.hot .d-ring { stroke: var(--amber); }
body.wrecker3d #doll3d .d-mark:hover .d-dot { stroke-opacity: 1; }
/* The swap flash: the marker of a piece that just changed pulses amber for a beat, dollui adds
   and removes the class. Paired with the 3D flare light so the highlight reads on both dolls. */
@keyframes d-markflash {
  0% { stroke: var(--amber); stroke-width: 0.055; }
  100% { stroke: transparent; stroke-width: 0.022; }
}
body.wrecker3d #doll3d .d-mark.flash .d-ring { animation: d-markflash 0.75s ease-out; }

/* ============================================================== the 3D doll
   The live turntable (dollui's viewport). The CANVAS is the picture; the SVG
   stays in the DOM as the fallback AND as the marker layer — .d-has3d drops
   the figure's parts to transparent and floats the markers over the canvas,
   which keep their SVG-projected positions (the rig only sways ±25°, so a
   socket stays on the side its marker says it is on). The wrap's gradient is
   the backdrop: the renderer clears to alpha, so CSS paints the mood for
   free. */
body.wrecker3d #doll3d .d-figwrap.d-has3d {
  position: relative;
  background:
    radial-gradient(80% 55% at 50% 82%, rgba(138, 51, 36, 0.28), transparent 70%),
    linear-gradient(180deg, rgba(46, 36, 28, 0.92), rgba(15, 12, 10, 0.97));
}
body.wrecker3d #doll3d .d-view3d {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 0;
}
body.wrecker3d #doll3d .d-has3d .d-figure {
  position: absolute;
  inset: 0;
  height: 100%;
  min-height: 0;
  z-index: 1;
  /* Only the markers may catch the pointer; the figure must not eat clicks aimed at the rig. */
  pointer-events: none;
}
body.wrecker3d #doll3d .d-has3d .d-figure .d-part,
body.wrecker3d #doll3d .d-has3d .d-figure .d-ground { fill-opacity: 0; stroke-opacity: 0; }
body.wrecker3d #doll3d .d-has3d .d-figure .d-mark { pointer-events: auto; }

body.wrecker3d #doll3d .d-strip {
  margin-top: 0.5rem;
  width: 100%;
  min-height: 40px;
  padding: 0.3rem 0.6rem;
  font-size: 0.85rem;
  border-width: 2px;
}
/* Armed. accountui's two-tap language, in this screen's one destructive spot:
   the button says SURE? and turns blood until the timeout takes it back. */
body.wrecker3d #doll3d .d-strip.armed {
  background: linear-gradient(to bottom, #e04a43, var(--blood) 55%, #7d191d);
}

/* ================================================================== the icon
   Present for every item: the rendered mesh icon for the 121 visible pieces
   when the book is shipped, the typographic tile for the 21 invisible ones
   always, and the tile for everything whenever the art is deleted. */
body.wrecker3d #doll3d .d-icon {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* The rack's tile is the armoury look (docs/DOLL_CONTRACT.md §2.4): 40px on a charcoal plate
   with a 1px bone-dim edge; the row's hover lifts it and rings it amber. One light rig, one ¾
   view per icon render, so 121 items read as a single collection. */
body.wrecker3d #doll3d .d-row .d-icon {
  width: 40px;
  height: 40px;
  background: var(--charcoal-2, #241d17);
  border: 1px solid var(--bone-dim, #8f857a);
  border-radius: 4px;
  padding: 2px;
  transition: transform 100ms ease-out, box-shadow 100ms ease-out;
}
body.wrecker3d #doll3d .d-row:hover .d-icon,
body.wrecker3d #doll3d .d-row:focus .d-icon {
  transform: translateY(-2px);
  box-shadow: 0 0 0 1px var(--amber), 0 3px 8px rgba(0, 0, 0, 0.55);
}
body.wrecker3d #doll3d .d-icon-img {
  width: 100%; height: 100%; object-fit: contain; display: block;
}
/* Icon over pips: the picture column. */
body.wrecker3d #doll3d .d-lefty {
  display: grid;
  gap: 2px;
  justify-items: center;
  align-content: start;
}
body.wrecker3d #doll3d .d-lefty .d-pips { font-size: 0.5rem; letter-spacing: 0.08em; }

body.wrecker3d #doll3d .d-tile {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--d-fam, var(--chrome));
  border-radius: 3px;
  background: rgba(0, 0, 0, 0.42);
  overflow: hidden;
}
/* The bar's proportions are the item's real `bulk` (dollui.iconAspect), set
   inline: a rebar shiv is a long thin bar and a cuirass is a slab, and you can
   tell them apart at 18 px without a single pixel of art. */
body.wrecker3d #doll3d .d-tile-bar {
  background: var(--d-fam, var(--chrome));
  opacity: 0.5;
  border-radius: 1px;
}
/* An item with no bulk is not drawn on the body at all — an implant, a
   trinket, a fighting form. A ring says "under the skin"; an empty box would
   say "broken". */
body.wrecker3d #doll3d .d-tile-bar.void {
  width: 52%;
  height: 52%;
  background: none;
  border: 1px solid var(--d-fam, var(--chrome));
  border-radius: 50%;
  opacity: 0.8;
}
body.wrecker3d #doll3d .d-tile-tag {
  position: absolute;
  bottom: 1px;
  font-family: var(--mono);
  font-size: 0.42rem;
  letter-spacing: 0.04em;
  color: var(--d-fam, var(--chrome));
  text-shadow: 0 1px 2px #000;
}
/* At 18 px the tag is three letters of mush. The chip already names the item
   in words, so drop it there and keep it on the rack's 34 px tile. */
body.wrecker3d #doll3d .d-slot .d-tile-tag { display: none; }

/* ================================================================== the rack */

body.wrecker3d #doll3d .d-filters { display: grid; gap: 0.3rem; margin-bottom: 0.45rem; }
body.wrecker3d #doll3d .d-chiprow { display: flex; flex-wrap: wrap; gap: 0.25rem; align-items: center; }
body.wrecker3d #doll3d .d-chiplbl {
  font-family: var(--display);
  font-size: 0.6rem;
  letter-spacing: 0.18em;
  color: var(--bone-dim);
  margin-right: 0.15rem;
}
body.wrecker3d #doll3d .d-chip {
  font: inherit;
  cursor: pointer;
  background: rgba(0, 0, 0, 0.35);
  color: var(--bone-dim);
  border: 1px solid #3a322c;
  border-radius: 5px;
  padding: 0.24rem 0.5rem;
  /* 44 px UNCONDITIONALLY (WCAG 2.5.5 AAA; Apple HIG 44 pt; Material 48 dp). It was 30 px on a
     fine pointer and 44 only inside the (pointer: coarse) block below — but this screen's own
     render path already calls 44 "the touch floor this whole screen is held to" (dollui.js
     wallWidth) and holds every wall cell to it, so 37 filter chips were the one population on
     the screen exempt from its own rule. A touchscreen laptop reports pointer:fine. */
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  font-size: 0.66rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
body.wrecker3d #doll3d .d-chip.on {
  color: var(--amber);
  border-color: var(--amber);
  background: rgba(245, 166, 35, 0.1);
}
body.wrecker3d #doll3d .d-chip.sel { outline: 2px solid var(--amber); outline-offset: 1px; }

body.wrecker3d #doll3d .d-find { display: flex; align-items: center; gap: 0.35rem; }
body.wrecker3d #doll3d .d-input { flex: 1 1 auto; min-width: 0; }

body.wrecker3d #doll3d .d-rack { display: grid; gap: 0.35rem; }

/* ===================================================================== THE WALL (2026-08-27)
 *
 * The rack's default view. Every tile is the item's REAL PHYSICAL SIZE in cells — kitgrid.js
 * turns `bulk` in metres into a footprint at one chosen cell size and hands back x/y/w/h, and
 * dollui writes them straight onto grid-column / grid-row. Nothing about the shape of this grid
 * is decided in CSS, because the moment it were, the stylesheet would be holding a second opinion
 * about how big a sledge is.
 *
 * The two custom properties are written by dollui per render: --d-cols is how many whole cells
 * fit the measured column (4..10), --d-cellpx is the square that leaves. Explicit px rather than
 * 1fr because the rows must be the same size as the columns or a 2x2 helm is not square, and
 * grid-auto-rows has no way to say "whatever a column came out to". */
body.wrecker3d #doll3d .d-wall {
  display: grid;
  grid-template-columns: repeat(var(--d-cols, 10), var(--d-cellpx, 40px));
  grid-auto-rows: var(--d-cellpx, 40px);
  gap: 4px;
  justify-content: start;
}
/* A cell is a .gcard with everything a row needed for TEXT taken back out: no min-height (the
   footprint is the height), no padding worth the name, and the picture stretched to the tile.
   .gcard keeps the plate, the hover, the focus ring and the .sel state, so the two views cannot
   drift apart on any of them. */
body.wrecker3d #doll3d .d-cell {
  position: relative;
  min-height: 0;
  padding: 2px;
  display: block;
  overflow: hidden;
}
/* The icon fills the cell and keeps its aspect. It CAN keep its aspect, because the cell's aspect
   was derived from the same bulk vector the icon was framed against (dollui.iconAspect) — that
   agreement is the whole reason kitgrid re-uses that projection instead of picking a better one. */
body.wrecker3d #doll3d .d-cell .d-icon {
  width: 100%;
  height: 100%;
  border: 0;
  background: none;
  padding: 0;
}
/* The pips ride the bottom-left corner instead of sitting under the picture — there is no "under"
   in a tile. Hidden below two cells tall, where they would cover the item they are describing. */
body.wrecker3d #doll3d .d-cell .d-pips {
  position: absolute;
  left: 3px;
  bottom: 1px;
  font-size: 0.5rem;
  letter-spacing: 0.06em;
  text-shadow: 0 0 3px #000, 0 0 3px #000;
  pointer-events: none;
}
body.wrecker3d #doll3d .d-cell .d-tile-tag { font-size: 0.42rem; }
/* WORN, said on the tile. The list says "FITTED — TAP TO STRIP IT OFF" in words it has room for;
   a tile has room for two letters, and the blood bar below carries the rest of the signal. */
/* 0.5rem was 8.0 px. Both of these are STATE, and a state glyph nobody can resolve is a mood. */
body.wrecker3d #doll3d .d-cell-on {
  position: absolute;
  top: 2px;
  right: 3px;
  font-size: max(11px, 0.62rem);
  letter-spacing: 0.08em;
  color: var(--blood);
  text-shadow: 0 0 3px #000, 0 0 3px #000;
  pointer-events: none;
}
/* AND THE REFUSAL, said on the tile, opposite the ON. The dimming below is what a refused tile
   USED to say for itself, and dimming alone cannot distinguish "you cannot wear this" from "this
   is far away" — the ✕ names the state and the tile's title names the reason. */
body.wrecker3d #doll3d .d-cell-no {
  position: absolute;
  top: 2px;
  right: 3px;
  font-size: max(11px, 0.62rem);
  line-height: 1;
  color: rgba(255, 255, 255, 0.85);
  text-shadow: 0 0 3px #000, 0 0 3px #000;
  pointer-events: none;
}
/* ==========================================================================
   THE TILE CAPTION (2026-09-03, lane PANELS).

   Driven headless over lib_dom against the shipped screen, the visible text of a refused tile
   was, verbatim: "GUN ●○○○○ ✕". 0 of 46 tiles carried their own name and 0 of 44 refusals
   reached a sighted player with no pointer, because every word on this wall lived in `title`
   and `title` is a MOUSE affordance. On a phone a tap is a click and a click equips.

   The strip is OPAQUE rather than a gradient, and that is the load-bearing choice: a gradient's
   contrast is a function of whatever icon happens to be behind it, so its worst case is whatever
   the brightest sprite in the book happens to be. Against rgba(15,12,10,0.96) the worst case is
   a WHITE sprite and it still measures 5.07:1 for the refusal and 13.33:1 for the name; behind a
   black sprite, 5.49 and 14.44. Measured, not estimated, and recomputed from these declarations
   every run by tools/gates/gates_doll.mjs §7 — a number in a comment nobody re-derives is a
   number that stops being true the first time somebody edits the line above it.
   ========================================================================== */
body.wrecker3d #doll3d .d-cell-cap {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: block;
  padding: 2px 3px;
  /* The pips own the bottom-left 3px; the caption starts clear of them. */
  padding-left: 26px;
  background: rgba(15, 12, 10, 0.96);
  border-top: 1px solid rgba(184, 188, 192, 0.18);
  text-align: left;
  pointer-events: none;
  /* 62% clipped the refusal line off the shortest tiles in the PILE view — measured 13 of 44
     refusals reaching the screen at 1920x1080. 80% of a 46 px tile is 36 px, which is the name's
     13 px plus the refusal's 13 px plus the 4 px of padding, so both lines land. It is a
     max-height and not a height: an unrefused tile's caption is still one line tall. */
  max-height: 80%;
  overflow: hidden;
}
/* 11 px is a FLOOR, not a size: `max()` keeps it honest for a reader who has raised their root
   font. The ✕ that used to be the only refusal signal measured 8.0 px, under the 28 px-at-1080p
   the Game Accessibility Guidelines ask for and under every floor anyone has published. */
body.wrecker3d #doll3d .d-cell-nm {
  display: block;
  font-family: var(--display);
  font-size: max(11px, 0.7rem);
  line-height: 1.15;
  letter-spacing: 0.04em;
  color: var(--bone);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* THE REFUSAL, in ink of its own. --blood #c1272d measures 3.09:1 against this scrim — under
   4.5:1, so the loudest thing on a refused tile was the half a low-vision player could not read.
   --blood-lit is the palette lane's token for the same signal at a legible lightness; the
   literal is its computed value, so this rule is correct on a tree where that token has not
   landed. The lightness is not decoration: a saturated red has almost all of its luminance in a
   channel the eye weights at 0.2126, so #c1272d cannot reach 4.5:1 on any plate this game uses
   without being lightened. (The plan for this round claimed even #ff0000 tops out at 4.29:1 on
   --charcoal; re-derived here it is 4.53:1, i.e. it scrapes a pass. The claim was too strong and
   is not repeated — the argument stands on --blood's own 3.09, which is measured.) */
body.wrecker3d #doll3d .d-cell-why {
  display: block;
  font-family: var(--mono);
  font-size: max(11px, 0.66rem);
  line-height: 1.2;
  color: var(--blood-lit, #e8574a);
  overflow: hidden;
}
body.wrecker3d #doll3d .d-cell.fitted {
  border-color: rgba(245, 166, 35, 0.55);
  border-left: 3px solid var(--blood);
  padding-left: 0;
}
/* THE DIM IS ON THE PICTURE, NOT ON THE TILE, and the reason is compositing rather than taste:
   `opacity` on the tile makes a group, and a child CANNOT climb back out of its parent's alpha.
   So `opacity: 0.55` on .d-cell dimmed the caption with everything else, and the caption is the
   one thing on a refused tile that has to survive — which is the greyed-button-with-an-invisible
   -explanation failure kitpanel.js named in its own .kp-badge.locked comment, arrived at from
   the other direction. The state is still legible without the dim: a ✕ top-right, the reason
   in blood-lit, and the border. */
body.wrecker3d #doll3d .d-cell[aria-disabled="true"] .d-icon,
body.wrecker3d #doll3d .d-cell[aria-disabled="true"] .d-pips { opacity: 0.55; }
body.wrecker3d #doll3d .d-cell[data-tier="4"] { border-color: rgba(245, 166, 35, 0.6); }
body.wrecker3d #doll3d .d-cell[data-tier="5"] {
  border-color: var(--amber);
  box-shadow: 0 0 7px rgba(245, 166, 35, 0.45), inset 0 0 5px rgba(245, 166, 35, 0.18);
}

/* A rack row is a .gcard with a picture bolted to its left. .gcard already
   owns the plate, the hover, the 44 px floor and the .sel ring; this is only
   the two-column split. */
body.wrecker3d #doll3d .d-row {
  grid-template-columns: 44px 1fr;
  align-content: start;
  align-items: start;
  gap: 0.5rem;
  padding: 0.45rem 0.6rem;
}
body.wrecker3d #doll3d .d-rowbody { display: grid; gap: 0.15rem; min-width: 0; }
/* Equipped = the BLOOD LEFT-BAR (§6's language: tier is amber, worn is blood). */
body.wrecker3d #doll3d .d-row.fitted {
  border-color: rgba(245, 166, 35, 0.55);
  border-left: 3px solid var(--blood);
  padding-left: calc(0.6rem - 2px);
}
/* TIER PRESENTATION — frame and glow climb with the rank (§2.4). Tiers 1–3 stay plates; a
   tier-4 piece carries an amber edge, a tier-5 piece glows like it costs what it costs. The
   number itself is in the pips and their aria-label; this is only the shine. */
body.wrecker3d #doll3d .d-row[data-tier="4"] .d-icon { border-color: rgba(245, 166, 35, 0.6); }
body.wrecker3d #doll3d .d-row[data-tier="5"] .d-icon {
  border-color: var(--amber);
  box-shadow: 0 0 7px rgba(245, 166, 35, 0.45), inset 0 0 5px rgba(245, 166, 35, 0.18);
}
body.wrecker3d #doll3d .d-row[data-tier="5"]:hover .d-icon,
body.wrecker3d #doll3d .d-row[data-tier="5"]:focus .d-icon {
  transform: translateY(-2px);
  box-shadow: 0 0 0 1px var(--amber), 0 0 10px rgba(245, 166, 35, 0.6), 0 3px 8px rgba(0, 0, 0, 0.55);
}
/* REFUSED, not disabled — a `disabled` button cannot take focus, so nobody on
   a keyboard or a screen reader could ever reach it to be told why. It stays
   focusable and clickable; the click prints the reason humanoid.js gave. */
body.wrecker3d #doll3d .d-row[aria-disabled="true"] { opacity: 0.6; }

body.wrecker3d #doll3d .d-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem;
  font-size: 0.68rem;
}
body.wrecker3d #doll3d .d-fam {
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  color: var(--chrome);
  border: 1px solid var(--plate-edge);
  border-radius: 3px;
  padding: 0 0.22rem;
}
body.wrecker3d #doll3d .d-inslot {
  font-family: var(--display);
  font-size: 0.6rem;
  letter-spacing: 0.14em;
  color: var(--amber);
  opacity: 0.85;
}
body.wrecker3d #doll3d .d-facts {
  font-family: var(--mono);
  font-size: 0.66rem;
  color: var(--bone-dim);
}
body.wrecker3d #doll3d .d-fitted {
  font-family: var(--display);
  font-size: 0.64rem;
  letter-spacing: 0.14em;
  color: var(--amber);
}
/* Pips AND the count in text. Rank carried in shape alone is unreadable to a
   screen reader and to anyone who cannot see the fill, so the aria-label on
   the element says "tier 3 of 5" in words. */
body.wrecker3d #doll3d .d-pips {
  letter-spacing: 0.14em;
  font-size: 0.66rem;
  color: var(--amber);
  white-space: nowrap;
}

/* ================================================================== the readout */

body.wrecker3d #doll3d .d-readout { display: grid; gap: 0.4rem; align-content: start; }

body.wrecker3d #doll3d .d-carryline {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 0.4rem;
}
body.wrecker3d #doll3d .d-carry-lbl,
body.wrecker3d #doll3d .d-carry-num {
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  color: var(--bone-dim);
  white-space: nowrap;
}
body.wrecker3d #doll3d .d-carry {
  position: relative;
  height: 9px;
  border: 1px solid var(--plate-edge);
  background: rgba(0, 0, 0, 0.5);
  border-radius: 2px;
  overflow: hidden;
}
body.wrecker3d #doll3d .d-carry-fill {
  display: block;
  height: 100%;
  background: linear-gradient(90deg, var(--rust), var(--amber));
}
body.wrecker3d #doll3d .d-carry.over .d-carry-fill { background: var(--blood); }
body.wrecker3d #doll3d .d-over {
  font-family: var(--display);
  font-size: 0.64rem;
  letter-spacing: 0.1em;
  color: var(--blood);
  line-height: 1.3;
}

body.wrecker3d #doll3d .d-statgrp { display: grid; gap: 1px; }
body.wrecker3d #doll3d .d-statgrp-h {
  font-family: var(--display);
  font-size: 0.6rem;
  letter-spacing: 0.2em;
  color: var(--amber);
  opacity: 0.75;
  margin-top: 0.25rem;
  border-bottom: 1px solid var(--plate-edge);
}
body.wrecker3d #doll3d .d-stat {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 0.4rem;
  align-items: baseline;
  padding: 0.06rem 0.15rem;
  font-size: 0.7rem;
}
body.wrecker3d #doll3d .d-stat-lbl { color: var(--bone-dim); letter-spacing: 0.06em; }
body.wrecker3d #doll3d .d-stat-val {
  font-family: var(--mono);
  color: var(--bone);
  white-space: nowrap;
}
/* A stat nothing is contributing to is still printed — a block with holes in
   it makes you count — but it steps back so the ones that matter stand out. */
body.wrecker3d #doll3d .d-stat.zero { opacity: 0.42; }
body.wrecker3d #doll3d .d-stat.changed { background: rgba(245, 166, 35, 0.08); border-radius: 2px; }
body.wrecker3d #doll3d .d-stat-d {
  font-family: var(--mono);
  font-size: 0.68rem;
  white-space: nowrap;
  min-width: 3.4em;
  text-align: right;
}
/* Not colour alone: the number itself carries a + or a - (dollui.fmtStat),
   which is what a monochrome display and a colour-blind player both read. */
body.wrecker3d #doll3d .d-stat-d.up { color: var(--amber); }
body.wrecker3d #doll3d .d-stat-d.down { color: var(--blood); }

/* ============================================================== the combo chain */

body.wrecker3d #doll3d .d-chain {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0.2rem;
  margin: 0.2rem 0;
}
body.wrecker3d #doll3d .d-chain-arrow { color: var(--bone-dim); align-self: center; font-size: 0.8rem; }
body.wrecker3d #doll3d .d-link {
  display: grid;
  gap: 0;
  padding: 0.2rem 0.35rem;
  border: 1px solid #3a322c;
  border-radius: 5px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(0, 0, 0, 0.3));
  min-width: 3.6rem;
}
/* The last link is the finisher and it is the reason to hold the chain
   together, so it is the one that is lit. */
body.wrecker3d #doll3d .d-link.last { border-color: var(--amber); background: rgba(245, 166, 35, 0.1); }
body.wrecker3d #doll3d .d-link-n {
  font-family: var(--mono);
  font-size: 0.58rem;
  color: var(--bone-dim);
}
body.wrecker3d #doll3d .d-link-key {
  font-family: var(--display);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  color: var(--bone);
}
body.wrecker3d #doll3d .d-link-in {
  font-family: var(--mono);
  font-size: 0.55rem;
  letter-spacing: 0.1em;
  color: var(--amber);
  opacity: 0.8;
}
body.wrecker3d #doll3d .d-finisher {
  font-family: var(--display);
  font-size: 0.66rem;
  letter-spacing: 0.1em;
  color: var(--amber);
  line-height: 1.35;
}

/* The garage's launcher into this screen, in the garage's own footer. */
body.wrecker3d #garage3d .d-launch {
  min-height: 44px;
  padding: 0.35rem 0.9rem;
  font-size: 0.85rem;
}

/* ==========================================================================
   COMPACTION — the SAME axis as everything else in this game.

   `pointer: coarse` OR `max-height: 560px`, matching style.css's HUD, title
   and garage blocks exactly rather than inventing a third definition of
   "mobile". This game nudges phones to LANDSCAPE, and a landscape phone is
   844x390 — wide and SHORT — so width is the wrong axis and a coarse pointer
   is the device signal. Two disagreeing detectors is the bug that hid the
   tablet controls entirely once already.

   Height is the scarce axis, so everything below trades vertical size. The
   44 px floor on the slot chips is NOT traded: it is the whole reason the
   chips exist beside the markers.
   ========================================================================== */
@media (pointer: coarse), (max-height: 560px) {
  body.wrecker3d .doll-box { max-height: 95svh; padding: 0.6rem 0.7rem; gap: 0.45rem; }
  body.wrecker3d #doll3d .d-panel { gap: 0.5rem; }
  /* The 52-86px stamp is gone (2026-08-16): it also caught TOUCHSCREEN DESKTOPS via
     pointer:coarse and pinned the SVG box man on a 1548px screen. Phones render the
     whole arena in three.js; they can render one turntable. Height stays the traded
     axis — the figure floor drops, the width keeps a real rig readable. */
  body.wrecker3d #doll3d .d-figwrap { width: clamp(120px, 24vw, 200px); }
  body.wrecker3d #doll3d .d-figure { min-height: 220px; }
  body.wrecker3d #doll3d .d-slot-name { font-size: 0.5rem; letter-spacing: 0.12em; }
  body.wrecker3d #doll3d .d-slot-item { font-size: 0.62rem; }
  /* A real thumb target on every filter chip, not just the rows. The garage's
     tabs went through this exact change for the same reason. */
  body.wrecker3d #doll3d .d-chip { min-height: 44px; padding: 0 0.6rem; display: inline-flex; align-items: center; }
  body.wrecker3d #doll3d .d-stat { font-size: 0.66rem; }
  body.wrecker3d #doll3d .d-facts { font-size: 0.62rem; }
  body.wrecker3d #doll3d .d-strip { min-height: 44px; }
}

/* A phone in portrait has no room for a rail either side of the figure. The
   rails become one column each of a 2-up grid under the doll, which keeps the
   reading order (chips follow the body) and keeps the chips full width. */
@media (max-width: 30rem) {
  body.wrecker3d #doll3d .d-stage { grid-template-columns: 1fr; }
  body.wrecker3d #doll3d .d-figwrap { width: 100%; max-width: 130px; justify-self: center; }
  body.wrecker3d #doll3d .d-rail { grid-template-columns: 1fr 1fr; }
  body.wrecker3d #doll3d .d-stat { grid-template-columns: 1fr auto auto; font-size: 0.68rem; }
}

/* Respect a reduced-motion preference: the marker ring, the swap flash and the
   icon lift all go still for somebody who asked for stillness. (The 3D sway is
   JS-driven and slow enough to be ambience, not motion — and the SVG doll is
   one preference toggle away regardless.) */
@media (prefers-reduced-motion: reduce) {
  body.wrecker3d #doll3d .d-mark .d-ring { transition: none; }
  body.wrecker3d #doll3d .d-mark.flash .d-ring { animation: none; }
  body.wrecker3d #doll3d .d-row .d-icon { transition: none; }
  body.wrecker3d #doll3d .d-row:hover .d-icon,
  body.wrecker3d #doll3d .d-row:focus .d-icon { transform: none; }
}

/* ============================================================ the operator pass
   2026-08-16. Five asks, all about the same thing: the doll screen's point is the
   BODY, and the layout was spending its space on everything else.
   ========================================================================== */

/* "THE KIT" centred under its kicker. .shop-head is a 3-up flex (back | title |
   purse) and the title stack inherited left alignment, so the kicker read centred
   (its own chevrons centre it) while the h1 hung left of it. */
body.wrecker3d #doll3d .d-title { text-align: center; }

/* THE FIGURE TAKES THE COLUMN. The doll column is label + stage + bottom rail +
   STRIP, and STRIP sat against a tall empty gutter because the stage only claimed
   its content height. The column is a flex box now, the stage takes what is left
   (min-height 0 or a flex child refuses to shrink), and STRIP is pushed to the
   floor where a destructive button belongs. */
body.wrecker3d #doll3d .d-col-doll { display: flex; flex-direction: column; }
body.wrecker3d #doll3d .d-col-doll .d-stage { flex: 1 1 auto; min-height: 0; align-items: stretch; }
body.wrecker3d #doll3d .d-col-doll .d-strip { margin-top: auto; }
body.wrecker3d #doll3d .d-figwrap { align-self: stretch; }

@media (min-width: 58rem) {
  /* Wider AND taller: the turntable is the screen's subject, not its garnish. */
  body.wrecker3d #doll3d .d-figwrap { width: clamp(190px, 19vw, 300px); }
  body.wrecker3d #doll3d .d-figure { min-height: 380px; }
  body.wrecker3d #doll3d .d-panel { grid-template-columns: minmax(23rem, 30rem) minmax(19rem, 1fr) minmax(15rem, 20rem); }
}

/* ------------------------------------------------------------ the leader lines
   The tie between a chip and its dot. Faint by default — it is a hint, not a
   diagram — and lit amber for the chosen slot. pointer-events none throughout:
   the line is never a target, the chip and the dot are. Under the SVG marker
   layer in z so a dot is always clickable through it. */
body.wrecker3d #doll3d .d-stage { position: relative; }
body.wrecker3d #doll3d .d-leads {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
  z-index: 2;
}
body.wrecker3d #doll3d .d-lead {
  stroke: var(--bone);
  stroke-opacity: 0.16;
  stroke-width: 1;
  stroke-dasharray: 3 4;
  transition: stroke-opacity 120ms linear;
}
body.wrecker3d #doll3d .d-lead.on {
  stroke: var(--amber);
  stroke-opacity: 0.65;
  stroke-dasharray: none;
}

/* The dot is the whole marker now (the worn glyph was an occluder over a live
   rig). Bigger hit ring than ink, and a soft halo so a 5-px dot still reads
   against a lit body. */
body.wrecker3d #doll3d .d-dot {
  filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.85));
  transition: r 120ms linear;
}
body.wrecker3d #doll3d .d-mark.hot .d-dot,
body.wrecker3d #doll3d .d-mark.on .d-dot { stroke-opacity: 1; }

/* ==========================================================================
   THE INVENTORY PANEL — kitpanel.js's paint. (lane KIT, 2026-08-31)

   WHY IT IS IN THIS FILE. It is the kit's paint and this is the kit's sheet;
   style.css belongs to another station (docs/HUMANOID.md §9). Every selector
   below is new — `kp-*`, a namespace nothing else in the game uses — so the
   COLLISION RULE at the head of this file still holds: not one of them also
   appears in style.css. What it reuses (.gcard, .shop-tab, .btn) it reuses by
   wearing those classes on its own markup, exactly as the rack does.

   IT IS NOT SCOPED TO A SCREEN. Every rule hangs off `.kp-root`, never off
   `#char3d` or `#doll3d`, because kitpanel.js is a COMPONENT and the whole
   point of it is that one inventory can be mounted in more than one host. A
   sheet that scoped it to one host would make the second mount render as
   unstyled text in normal flow — the exact failure game/index.html records
   about this screen, committed a second time.

   THE PHONE IS THE DEFAULT. Everything below is written narrow-first and the
   only min-width query widens the rows to two columns. The screen this
   replaces put 46 rows and 46 chrome controls in front of a man who owned
   8 things, which is unusable at 1280 px and hostile at 360.
   ========================================================================== */

body.wrecker3d .kp-root {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  min-height: 0;
  font-family: var(--body);
}

/* ------------------------------------------------------------------ the head
   Sticky, because the census and the load line are the two numbers you want
   while you scroll the thing they describe — the budget-in-the-frame rule the
   character screen already learned about perk points. */
body.wrecker3d .kp-head {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding-bottom: 0.35rem;
  background: linear-gradient(to bottom, var(--charcoal-3, #0f0c0a) 78%, rgba(15, 12, 10, 0));
}
body.wrecker3d .kp-sources { display: flex; gap: 0.35rem; }
/* 44 px is the touch floor this repo holds every primary control to, and a
   source toggle is the most important control on the panel. */
body.wrecker3d .kp-src { flex: 1 1 0; min-height: 44px; }

body.wrecker3d .kp-census {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.4rem;
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
}
body.wrecker3d .kp-count { color: var(--amber); }
body.wrecker3d .kp-load { color: var(--bone-dim); }
body.wrecker3d .kp-blurb {
  margin: 0;
  font-size: 0.76rem;
  line-height: 1.35;
  color: var(--bone-dim);
}

/* --------------------------------------------------------------- the scroller */
body.wrecker3d .kp-listwrap { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
body.wrecker3d .kp-list { display: flex; flex-direction: column; gap: 0.55rem; }
body.wrecker3d .kp-group { display: flex; flex-direction: column; gap: 0.3rem; }

/* The slot name is a RULE with a word on it, not a heading in a box: it
   separates without costing a row of chrome, which is what makes fourteen
   groups affordable on a phone. */
body.wrecker3d .kp-slot {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0.2rem 0 0;
  font-family: var(--display);
  font-size: 0.72rem;
  font-weight: normal;
  letter-spacing: 0.14em;
  color: var(--bone-dim);
  text-transform: uppercase;
}
body.wrecker3d .kp-slot::after {
  content: '';
  flex: 1 1 auto;
  height: 1px;
  background: var(--plate-edge);
}

/* ------------------------------------------------------------------- a row
   .gcard carries the plate; this carries the layout and the three states. */
/* DENSITY IS A FEATURE HERE. Photographed at 1280x800 with 0.5rem padding and a
   44 px tile, four slot groups filled the whole 497 px content window and a
   13-item inventory still had to be scrolled — which is a list of thirteen
   things behaving like a list of a hundred. A row is two short lines and a
   picture; it does not need the height of a card. */
body.wrecker3d .kp-row {
  display: grid;
  grid-template-columns: 38px 1fr;
  gap: 0.5rem;
  align-items: start;
  width: 100%;
  min-height: 44px;
  padding: 0.35rem 0.5rem;
  text-align: left;
}
/* WORN reads as ON — an amber edge, the same amber the doll's chosen slot and
   the perk budget use, so "this is live" means one thing everywhere. */
body.wrecker3d .kp-row.worn { border-color: var(--amber); background: rgba(245, 166, 35, 0.07); }
/* READY is a thing you can do right now. Faint by default so a pack of spares
   does not shout, brighter on hover/focus where the decision is being made. */
body.wrecker3d .kp-row.ready:hover,
body.wrecker3d .kp-row.ready:focus-visible { border-color: var(--bone); }
/* REFUSED is dimmed, NEVER hidden and never `disabled` — the sentence on it is
   the only documentation of that rule a player will ever read, so it has to be
   reachable by Tab and legible when it gets there. Text stays at full contrast;
   only the plate recedes.
   
   THAT LAST SENTENCE WAS FALSE FOR AS LONG AS THIS RULE SAID `opacity: 0.72`, and it is the
   same compositing trap .d-cell was carrying (see its own note above). `opacity` on a row makes
   a GROUP: everything inside composites through it and nothing inside can climb back out. So
   the refusal badge, whose own comment two rules down calls it "the loudest thing on a refused
   row", was rendering at (2026-09-03, WCAG relative luminance, computed off these very
   declarations):

       --blood     over #1a1512, through the 0.72 group   2.14:1
       --blood-lit over #1a1512, through the 0.72 group   3.20:1   <- the new ink STILL fails

   which is why recolouring the badge alone would not have fixed it. The intent the comment
   already stated is now what the rule does: the PLATE recedes, the text does not. The
   :focus-visible companion went with it — it existed only to undo the group alpha. */
body.wrecker3d .kp-row.locked { background: rgba(0, 0, 0, 0.28); border-style: dashed; }

body.wrecker3d .kp-tile {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border: 1px solid var(--plate-edge);
  background: var(--charcoal-2, #241d17);
  overflow: hidden;
}
body.wrecker3d .kp-tile img { width: 100%; height: 100%; object-fit: contain; display: block; }
body.wrecker3d .kp-mono {
  font-family: var(--display);
  font-size: 1.1rem;
  color: var(--bone-dim);
}

body.wrecker3d .kp-body { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
body.wrecker3d .kp-top {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.3rem;
}
body.wrecker3d .kp-name {
  font-family: var(--display);
  font-size: 0.94rem;
  letter-spacing: 0.03em;
  color: var(--bone);
}
body.wrecker3d .kp-badge {
  font-family: var(--mono);
  font-size: 0.66rem;
  letter-spacing: 0.06em;
  padding: 0.05rem 0.3rem;
  border: 1px solid currentColor;
  white-space: nowrap;
}
body.wrecker3d .kp-badge.worn { color: var(--amber); }
/* The slot name on the row. HIDDEN wherever the group headings are shown — two names for one
   fact is clutter — and swapped in for them on a short screen. See the max-height block. */
body.wrecker3d .kp-in { display: none; }
/* THE REFUSAL IS THE LOUDEST THING ON A REFUSED ROW. Blood, not grey: the row
   is dimmed and the reason must survive the dimming, or the whole affordance is
   a greyed button with an invisible explanation — which is the failure this
   panel exists to end. */
/* --blood #c1272d measures 2.98:1 on this plate — under 4.5:1, so the loudest thing on the row
   was the one a low-vision player could not read. --blood-lit is the palette lane's token for
   the same signal at a legible lightness (4.95:1 here); the literal is its computed value, so
   this rule is right on a tree where that token has not landed. */
body.wrecker3d .kp-badge.locked {
  color: var(--blood-lit, #e8574a);
  border-color: var(--blood-lit, #e8574a);
  white-space: normal;
}

body.wrecker3d .kp-facts {
  font-family: var(--mono);
  font-size: 0.7rem;
  line-height: 1.3;
  color: var(--bone-dim);
}
/* The swap, in the same ink as a live number, because it is the one line on the
   row that is about a decision rather than about an object. */
body.wrecker3d .kp-delta {
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--ice, #6fd0ff);
}
body.wrecker3d .kp-note {
  font-size: 0.7rem;
  line-height: 1.3;
  color: var(--bone-dim);
  font-style: italic;
}
/* The price wears the game's scrap colour, so a number you can spend looks the same here as it
   does over the bay's counter. */
body.wrecker3d .kp-price {
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--amber);
  opacity: 0.85;
}
body.wrecker3d .kp-bare {
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--bone-dim);
  padding: 0.2rem 0.1rem;
}

/* --------------------------------------------------------------- the gaps line
   Six empty slot groups compressed to one wrapped row of chips. Same fact, a
   sixth of the height, and every chip is a real button into the pile. */
body.wrecker3d .kp-gaps {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem;
  padding-top: 0.4rem;
  border-top: 1px solid var(--plate-edge);
}
body.wrecker3d .kp-gapslbl {
  font-family: var(--display);
  font-size: 0.7rem;
  letter-spacing: 0.12em;
  color: var(--bone-dim);
}
body.wrecker3d .kp-chip {
  min-height: 32px;
  padding: 0.2rem 0.5rem;
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.05em;
  color: var(--bone-dim);
  background: transparent;
  border: 1px dashed var(--plate-edge);
  cursor: pointer;
}
body.wrecker3d .kp-chip:hover,
body.wrecker3d .kp-chip:focus-visible { color: var(--amber); border-color: var(--amber); }
body.wrecker3d .kp-chip.sel { color: var(--amber); border-style: solid; border-color: var(--amber); }

body.wrecker3d .kp-empty {
  margin: 0.4rem 0 0;
  font-family: var(--mono);
  font-size: 0.74rem;
  line-height: 1.4;
  color: var(--bone-dim);
}

/* ---------------------------------------------------------------- the pile bar */
body.wrecker3d .kp-pilebar { display: flex; gap: 0.4rem; align-items: stretch; }
body.wrecker3d .kp-find {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 40px;
  padding: 0.3rem 0.5rem;
  font-family: var(--mono);
  font-size: 0.8rem;
  color: var(--bone);
  background: var(--charcoal-3, #0f0c0a);
  border: 1px solid var(--plate-edge);
}
body.wrecker3d .kp-find:focus-visible { border-color: var(--amber); outline: none; }
/* A pile row is never lit as available: nothing on it is yours yet, so it does
   not get the affordance colouring a row you can act on gets. */
body.wrecker3d .kp-row.pile { opacity: 0.85; }

/* --------------------------------------------------------- the character screen
   The two nodes characterui.js wraps the panel in. `min-height: 0` is the
   flex/grid overflow incantation: without it the mount refuses to shrink below
   its content and the fourteen groups push the shelf past the viewport instead
   of scrolling inside it. */
body.wrecker3d .kp-wrap {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  flex: 1 1 auto;
  min-height: 0;
}
body.wrecker3d .kp-mount { flex: 1 1 auto; min-height: 0; display: flex; }
body.wrecker3d .kp-mount > .kp-root { flex: 1 1 auto; min-width: 0; }
body.wrecker3d .kp-door { align-self: flex-start; min-height: 40px; }

/* --------------------------------------------------------------- wider screens
   MULTI-COLUMN, NOT A TWO-COLUMN GRID, and the difference is measured. The first
   cut paired the ROWS inside each group, which put a lone HEAD item in the left
   half and dead space in the right — most slots hold exactly one thing, so a
   2-col grid of rows is 50% empty by construction. Photographed at 1280x800:
   four slot groups filled a 497 px body and the rest was scroll.

   `columns` flows the GROUPS instead, so HEAD, FACE and ARMS stack beside MAIN
   HAND's four rows with no gaps, and `break-inside: avoid` keeps a group's rule
   attached to the rows under it — a slot heading orphaned at the foot of a
   column would be worse than the space it saved. */
@media (min-width: 52rem) {
  /* `display: block` IS LOAD-BEARING, not tidying. `.kp-list` is a flex column by default and a
     FLEX CONTAINER IS NOT A MULTICOL CONTAINER — `columns` on it is silently ignored, which is
     exactly what the first cut shipped: the media query matched at 1280 px, every declaration
     below was live, and the photograph showed one full-width column. A rule that has no effect
     and no error is the worst kind, so it is written down at the line that fixes it. */
  body.wrecker3d .kp-list { display: block; columns: 2; column-gap: 1rem; }
  body.wrecker3d .kp-group,
  body.wrecker3d .kp-gaps { break-inside: avoid; margin-bottom: 0.55rem; }
  body.wrecker3d .kp-list.pile > .kp-row { break-inside: avoid; margin-bottom: 0.4rem; }
}
@media (min-width: 78rem) {
  body.wrecker3d .kp-list { columns: 3; }
}

/* A coarse pointer gets bigger targets and no hover-only affordance — every
   state above is also expressed by a border style or a badge, so nothing is
   lost when :hover never fires. */
@media (pointer: coarse) {
  body.wrecker3d .kp-row { min-height: 48px; }
  body.wrecker3d .kp-chip { min-height: 40px; }
}

/* ------------------------------------------------------- THE SHORT SCREEN, and
   this block is the whole reason the panel was photographed rather than gated.

   MEASURED at 844x390 (the phone landscape profile this game is played in): the
   character screen's content window is 148 px, and the panel's head — a door
   button, a 44 px source toggle, a census line and a two-line blurb — is about
   140 px of it. ZERO ITEM ROWS WERE VISIBLE. The gate was green, the census read
   "13 PIECES · 9 WORN · 4 SPARE", and the screen showed a man nothing he owned.
   That is the exact failure this whole screen was created to fix (a 139 px bay
   holding 470 px of content), committed again one level down by its replacement.

   So on a short viewport the head collapses to ONE ROW and everything that is
   not load-bearing goes. What survives is what a player cannot act without:
   the source toggle at its 44 px touch floor, and the two counts.

   WHAT GOES, AND WHY EACH ONE IS AFFORDABLE:
     the blurb   explanatory prose you read once. It is furniture by the second
                 visit and it was costing two lines of a two-line budget.
     the door    a SECOND route to the paperdoll. V opens it from anywhere and
                 always has, so nothing becomes unreachable — only less obvious,
                 which is the right trade against showing no inventory at all.
     the note    "NORMALLY MAIN HAND" is a disambiguation for a duplicate row,
                 and the aria-label still carries it for a screen reader. */
@media (max-height: 560px) {
  body.wrecker3d .kp-door,
  body.wrecker3d .kp-blurb,
  body.wrecker3d .kp-note { display: none; }
  body.wrecker3d .kp-head {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.3rem;
    padding-bottom: 0.2rem;
  }
  body.wrecker3d .kp-sources { flex: 1 1 16rem; }
  body.wrecker3d .kp-census { flex: 1 1 11rem; justify-content: flex-end; font-size: 0.7rem; }
  body.wrecker3d .kp-root { gap: 0.3rem; }
  body.wrecker3d .kp-list { gap: 0.35rem; }
  body.wrecker3d .kp-row { padding: 0.3rem 0.45rem; gap: 0.45rem; grid-template-columns: 36px 1fr; }
  body.wrecker3d .kp-tile { width: 36px; height: 36px; }
  body.wrecker3d .kp-name { font-size: 0.86rem; }
  body.wrecker3d .kp-gaps { padding-top: 0.25rem; }
  /* THE HEADINGS BECOME A WORD ON THE ROW. Fourteen `.kp-slot` rules cost ~16 px each, which
     on a 148 px content window is most of the window spent on separators. The slot name is
     already on every row (kitpanel.js's `.kp-in`), so nothing is lost but the whitespace —
     and the grouping still reads, because the rows are still in slot order. */
  body.wrecker3d .kp-slot { display: none; }
  body.wrecker3d .kp-in {
    display: inline;
    font-family: var(--display);
    font-size: 0.62rem;
    letter-spacing: 0.12em;
    color: var(--bone-dim);
  }
  body.wrecker3d .kp-group { gap: 0.25rem; }
}
