/* SKYSUNDER — the shop screen's layout.   Josh, 2026-07-31: "make all the elements of the shop screen FIT"
 *
 * WHY THIS IS ITS OWN FILE, LOADED LAST.
 * The prep screen's layout lived in assets/town-preparation-pilot.css — 2541 lines of Steve's pilot, which
 * loads after skysunder.css and therefore wins every specificity contest with it. Putting Josh's layout in
 * skysunder.css would have meant escalating selectors against a file that is still being handed off; putting
 * it INSIDE the pilot would have meant editing a file Steve overwrites. So it goes in a third place that
 * loads after both, and the pilot keeps ownership of everything it is actually for (card decoration, stat
 * rows, button skins). One owner per concern.
 *
 * WHAT WAS WRONG. The pilot's grid was written for a resizable viewport:
 *     grid-template-rows: minmax(210px, 1fr) minmax(400px, min(518.4px, 430px));
 * Two rows with independent minima that sum past the height available on a fixed 1080 stage. The overflow is
 * the dead space in Josh's screenshot.
 * The first fix here made the top band `auto` and gave the Bazaar `1fr` — better, but still wrong, because
 * the town's ten buildings then claimed their height first and the Bazaar inherited less than a card needs.
 * It is now the other way round: the Bazaar row is `auto` and the top band takes the remainder. See the
 * long note on grid-template-rows below.
 *
 * THE COLUMNS, and why the Bazaar spans three of them. Josh: "Don't make the cards any bigger, just shrink
 * the bazaar panel and allow room for it to expand to the additional relic slot if purchased." rollShop()
 * deals `hasArt('monopoly') ? 5 : 4`, so the fifth slot is a relic unlock, not a resize. The card width is
 * the pilot's --ss-pilot-card (218px) and is left alone; the panel is sized to hold five of them, so the
 * fifth arrives into space that already exists rather than reflowing the row.
 */

/* ── THE GRID ────────────────────────────────────────────────────────────────────────────────────────
   town | camp | spells | banners  across the top, Bazaar along the bottom under the first three.
   Widths are content-led except the two rails, which are fixed so the Bazaar's width never depends on
   how many banners happen to be recruited. */
body:has(#prep:not(.hidden)) #prepBody {
  --sky-rail-banners: 304px;      /* matches the pilot's --ss-pilot-banner, so the two agree */
  --sky-rail-spells: 216px;   /* the middle strip: INCOMING on the top row, the 210px monogram below it */
  --sky-card: var(--ss-pilot-card, 218px);
  /* One banner slot = the pilot's `#armySum > .abat { min-height:116px }` plus #armySum's 8px flex gap.
     Named so the five-slot floor below reads as five slots rather than as 628. */
  --sky-banner-slot: 124px;
  --sky-banner-head: 44px;        /* the panel's own YOUR BANNERS heading */
  /* YOUR TOWN is a third of the width it was (Josh 2026-07-31). Its column is unchanged — the Bazaar
     underneath still spans the full width — so this shrinks the panel, not the layout. */
  /* 34% → 37%: the ~9% widening Josh asked for on 2026-08-01, "so that all the buttons UI are not cutoff".
     Still comfortably the "about a third" he specified the first time. */
  --sky-town-width: 37%;
  display: grid;
  /* Column 2 does double duty: SPELLS on the top row, then the monogram strip beside the banners.
     Column 3 likewise: INCOMING on the top row, then the banner list. That is what lets the rail read as
     two boxes over a logo-plus-list without a nested grid. (Josh 2026-07-31) */
  grid-template-columns:
    minmax(0, 1fr)                /* town above, Bazaar below */
    var(--sky-rail-spells)
    var(--sky-rail-banners);
  /* THE BAZAAR ROW IS SIZED BY ITS CARDS, and the top band takes what is left — not the other way round.
   * Josh, 2026-07-31: "Right now there is a scroll wheel for the shop, which under no circumstance is
   * acceptagble."
   * It was `auto minmax(0,1fr)`: the town/camp/spells band claimed its content height first and the Bazaar
   * inherited the remainder. YOUR TOWN is ten buildings over five rows, so the remainder was smaller than a
   * card, and a card whose own min-height is 300px does not shrink — it overflows, and the panel grows a
   * scrollbar.
   * Inverted, the row the cards live in is `auto`, so it asks for exactly what a card needs and always gets
   * it. Everything above it flexes instead. That is the right way round because the card height is fixed by
   * the design and the town's is not: the town can lose a few pixels, or be collapsed, and nothing is cut
   * off — whereas a clipped card hides the stats you are buying on. */
  /* Three rows now. The spells/incoming band is `auto` (it asks for what it needs), the Bazaar row is
     `auto` (sized by a card, as before), and the middle row takes everything left — which is what feeds
     the town and the banner list. Josh chose the Bazaar as the thing that gives way when they collide,
     so the card's own floor is gone entirely; see the note on min-height below. */
  /* SPELLS AND INCOMING ARE SWAPPED (Josh 2026-07-31: "Swap the spells menu location with the altitude
     x/x - incoming menu"). SPELLS now sits in the right-hand column, directly above BANNERS, and INCOMING
     takes the middle. That pairing is the point: the banner list and the spellbook are both "what I am
     bringing", and they now read as one rail. */
  /* ── 📥 INCOMING GETS ITS OWN ROW (Josh 2026-08-01) ───────────────────────────────────────────────
   * "Why does BANNERS shrink when INCOMING expands?" — and he answered it himself: "give INCOMING its
   * own row to the left of banners/spells."
   *
   * He was exactly right, and the coupling is a row track rather than anything horizontal. INCOMING and
   * SPELLS shared row 1, which is `auto`; BANNERS spans rows 2 and 3, and row 2 is the `minmax(0,1fr)`
   * that absorbs whatever is left. So INCOMING's height set row 1, row 1 ate row 2, and BANNERS paid —
   * two panels that never touch, one of them shrinking the other. Measured in the running game, an
   * Ascent round 1 at the 1920x1080 stage:
   *
   *     INCOMING   rows (1 / 2 / 3)      BANNERS
   *       176px    283 / 186 / 496        548px      as shipped
   *       298px    376 /  93 / 496        474px
   *       394px    498 /   0 / 496        401px      row 2 gone, BANNERS down 147px
   *
   * INCOMING now spans rows 1-2 — the whole middle column beside SPELLS and BANNERS. Nothing is
   * displaced: the `sslogo` area it grows into was already dead, since the fifth pass took the SS
   * watermark off this screen entirely (`display: none`, below).
   *
   * WHY SPANNING IS THE FIX AND NOT JUST A MOVE. An item spanning a set of tracks that includes a
   * FLEXIBLE track contributes nothing to the intrinsic sizing of the `auto` tracks it also spans
   * (CSS Grid §11.5). So once INCOMING spans row 1 and the 1fr row, its height stops feeding row 1
   * altogether — row 1 is sized by SPELLS alone and row 2 keeps its remainder. Re-measured with the
   * same probe:
   *
   *       176px    283 / 186 / 496        548px
   *       298px    283 / 186 / 496        548px
   *       456px    283 / 186 / 496        548px
   *       614px    283 / 186 / 496        548px      BANNERS does not move at all
   *
   * Its span is 283 + 10 + 186 = 479px of room against a live height of 176px, and it keeps
   * `align-self: start` so it hugs the top rather than stretching. Past 479px it would spill toward the
   * Bazaar row and be clipped by this element's own `overflow: hidden` — which is the right failure:
   * clipping the panel that grew, not shortening the one beside it. */
  /* ── 👁 SPELLS TAKES THE MIDDLE, BANNERS TAKES THE WHOLE RIGHT COLUMN (Josh 2026-08-01) ───────────
   * "I think we need to rethink the squadron banner ui. Can you move spells to be where altitude x/x is
   * now and push altitude x/x to be a pop up menu."
   *
   * The camp preview has left the grid entirely — it is #incomingOv now, opened by the eye in the header —
   * so the middle column was free and SPELLS moved into it. That in turn freed the top-right cell, and the
   * banner rail took it: BANNERS now spans the full height of the right column instead of starting a row
   * down. That is the "rethink" half of the ask — the rail is the thing that was short of room.
   *
   * THREE ROWS BECAME TWO. The third row only ever existed so INCOMING and SPELLS could stack in the middle
   * column, and with one of them gone it had nothing to separate. Worse, it would have collapsed anyway:
   * every remaining area spans into the flexible row, and an item spanning a flexible track contributes
   * nothing to the intrinsic sizing of the `auto` tracks it also spans (CSS Grid §11.5) — so the old row 1
   * would have measured zero and the layout would have been two rows pretending to be three. Saying two is
   * honest, and it keeps the decoupling that fix bought: nothing in the top band can shorten the rail.
   *
   * `sslogo` keeps its cell under SPELLS. The mark itself is `display: none` on this screen since the fifth
   * pass, so the area is dead either way, and naming it costs nothing while leaving the template readable
   * against the older entries in this file. */
  /* ⚠️ THE ROW TEMPLATE LIVES HERE, BESIDE THE AREAS, AND IS DECLARED EXACTLY ONCE.
   * It used to be down in the `--sky-bazaar-row` block instead, and the two-row rewrite above edited THIS
   * copy — the losing one, at equal specificity and earlier in the file. The result was a three-track row
   * template under a two-row area template: `bazaar` resolved to the 1fr row, collapsed to 0px, and the
   * Bazaar rendered as a 29px sliver with its cards clipped. Caught on screen, not by any assertion.
   * The row count and the area template have to agree, so they are kept in the same block where a change
   * to one cannot miss the other, and preplayouttest.js now asserts both the single declaration and the
   * agreement. `--sky-bazaar-row` is still defined further down with the reasoning for its value; custom
   * properties resolve per element rather than in source order, so using it before that block is fine. */
  grid-template-rows: minmax(0, 1fr) var(--sky-bazaar-row);
  grid-template-areas:
    "top     spells    banners"
    "bazaar  sslogo    banners";
  gap: 10px;
  align-items: start;
  min-height: 0;                  /* a grid child of a flex column will not shrink without this */
  overflow: hidden;               /* nothing may spill into the letterbox */
}

/* The town and the camp preview sit side by side in the top band. A nested grid rather than more columns
   on the parent, so collapsing either one gives its width to the other instead of to the Bazaar. */
body:has(#prep:not(.hidden)) #prepBody > #townPanel { grid-area: top; }
body:has(#prep:not(.hidden)) #prepBody {
  grid-auto-flow: row;
}
body:has(#prep:not(.hidden)) #townPanel {
  grid-area: top;
  align-self: start;
  width: auto;
  min-width: 0;
  margin: 0;
  max-height: none;
}
/* 👁 THE CAMP PREVIEW IS NO LONGER A GRID CELL (Josh 2026-08-01). It was `position:absolute; top/right:12px`
   until 2026-07-31, then a cell in this grid, and it is now the body of the #incomingOv popup. The grid
   placement is gone with the cell it named — a `grid-area: incoming` pointing at an area this template no
   longer declares would place the panel in an implicit track, which is a silent way to end up with a
   mystery row. What it still needs is the un-floating that the 07-31 move introduced, because the panel's
   older rules elsewhere still assume it hangs off the corner of the screen. */
/* The un-floating that the 2026-07-31 move introduced is folded into the popup's own block below rather
   than kept in a separate rule with a two-id branch. The panel lives in exactly one place now, so one
   selector describes it — and every override of it has to clear the same specificity bar (see the note
   there), which a `body:has(…) #ascentPanel` branch does not. */

/* ── 👁 THE CAMP PREVIEW POPUP ────────────────────────────────────────────────────────────────────────
 * `.ovCard` is built for a paragraph and two buttons — 30px/42px of padding and a centred column. The camp
 * preview is a LIST, and a list reads left-aligned, so the card is re-tuned rather than the panel being
 * bent to fit a card meant for prose. Width is capped against the viewport so a long camp cannot run off
 * the stage, and the list scrolls inside the card instead of the card growing past it.
 *
 * ⚠️ THREE IDS, AND HANDOFF_2026-08-01 §2.1 IS WHY. The first version of the block below used
 * `#incomingOv #ascentPanel` — two ids — and every declaration in it silently lost. The pilot styles this
 * panel through:
 *
 *     body:has(#prep:not(.hidden)) :is(#standings, #ascentPanel) { padding: 11px; overflow-y: auto; … }
 *
 * and `:is()` takes the specificity of its MOST SPECIFIC ARGUMENT, so that `:is()` scores as an id: the
 * selector is (2,1,1) against my (2,0,0). Ids tie, and it wins on the class. The panel kept its 11px
 * padding, its border and its own max-height inside the card, and the rule I had written did nothing —
 * measured, not assumed: computed border-top-width came back 1px against a declared `border: 0`.
 * The remedy the handoff prescribes is to add another id to the chain, and the card was a class, so it
 * gained `id="incomingCard"` purely to be that third id. (2,1,1) loses to (3,0,0) on the first number. */
#incomingOv #incomingCard {
  padding: 24px 26px 20px;
  text-align: left;
  /* Josh 2026-08-01, on the first version: "the preview is great, but can you use the full real estate of
     the popup ui?" It was a 460px card holding a 224px column — see the width note below — so the answer is
     both halves: let the content fill the card, and give the card a size worth filling. 760px on a 1920
     stage is wide enough that a unit's name, its tier/type/race line and its stack count sit on one
     comfortable row instead of an ellipsis; the vw cap keeps it sane if the stage ever narrows. */
  width: min(760px, 62vw);
  max-width: none;
}
/* A bordered panel inside a bordered card reads as a box in a box. The panel contributes the heading and
   the rows; the card contributes the surface. The pilot's `top: 232px !important` is answered by the
   `position: static` further up — an absolute offset on an element that is no longer positioned. */
/* ── 👁 THE PANEL IS A MODAL IN HERE, NOT A DOCKED SIDEBAR ───────────────────────────────────────────
 * `#ascentPanel` spent most of its life as a 224px widget pinned to the top-right corner, and the pilot
 * still dresses it as one through
 *
 *     body:has(#prep:not(.hidden)) :is(#chatBox, #standings, #ascentPanel) { width: 224px !important; … }
 *
 * — the same skin it gives the chat box. That is why the card was 460px wide with a 224px column of content
 * in it: not a layout mistake in the popup, an identity the panel brought with it. `!important` beats even
 * an inline style, so `width: 100%` set on the element by hand did nothing; measured, that is how this was
 * finally pinned down.
 *
 * So the popup undoes the sidebar identity deliberately and in one place: the width, the small type, the
 * cramped padding and the panel's own frame. Everything below is scoped to the popup, so the panel keeps
 * every bit of that skin anywhere else it is ever shown. */
#incomingOv #incomingCard #ascentPanel {
  /* !important, because the declaration it answers is. Three ids beat the pilot's (2,1,1) among the
     important declarations — see the specificity note above. */
  width: auto !important;
  right: auto !important;
  font-size: 15px;
  /* Un-floated. The pilot still aims `top: 232px !important` at this panel from the days when it hung off
     the top-right corner of the screen; `position: static` makes that offset inert rather than fighting it
     with a competing coordinate. */
  position: static;
  justify-self: stretch;
  align-self: start;
  width: auto;
  max-width: none;
  margin: 0;
  z-index: auto;
  border: 0;
  background: none;
  box-shadow: none;
  padding: 0;
  /* ── THE LIST SCROLLS. THE HEADING AND THE VERDICT DO NOT. ─────────────────────────────────────────
   * This was `max-height` + `overflow-y: auto` on the PANEL, which scrolls everything together — and with
   * six stacks in the camp that pushed #ascMeta, the "their strength 420 · yours 0 / you are outmatched"
   * line, off the bottom of the card. That line is the single most useful thing in the popup and it was
   * the first thing to go. Caught on screen with a six-stack camp; a two-stack round-1 camp never reaches
   * the cap and would have hidden it indefinitely.
   * So the panel is a flex column that clips, and only the list inside it scrolls: title, camp name and
   * verdict stay put while the roster runs under them. `min-height: 0` on both the column and the list is
   * what actually lets a flex child shrink below its content — without it the list refuses to scroll and
   * the panel grows past its own max-height instead. */
  display: flex;
  flex-direction: column;
  min-height: 0;
  max-height: min(76vh, 800px);
  overflow: hidden;
}
#incomingOv #incomingCard #ascentPanel .pcolBody {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1 1 auto;
}
#incomingOv #incomingCard #ascList {
  min-height: 0;
  flex: 1 1 auto;
  overflow-y: auto;
  /* Room for the scrollbar so the last row's count is not sitting under it. */
  padding-right: 6px;
}
#incomingOv #incomingCard #ascMeta {
  flex: 0 0 auto;
}
#incomingOv #incomingCard #incomingClose {
  display: block;
  width: 100%;
  margin-top: 14px;
}
/* The heading is not a collapse toggle in here (12-boot.js stopped wiring one), so it must not keep
   advertising itself as one. */
#incomingOv #incomingCard #ascentPanel > h2 {
  cursor: default;
  margin-bottom: 14px;
  padding-bottom: 12px;
  font-size: 22px;
}

/* ── THE CAMP LIST, AT MODAL SCALE ───────────────────────────────────────────────────────────────────
 * Every number here is the sidebar's, doubled or thereabouts, and for the same reason: a 28px thumbnail
 * and 11px type are right for a 224px widget in the corner of the screen and wrong for a card you opened
 * on purpose to study what you are about to fight. The art is the part worth the space — these are the
 * units that are about to kill you — so it gets the largest jump.
 * `!important` only where the pilot uses it; the rest wins on the three ids alone. */
#incomingOv #incomingCard #ascFlavor {
  font-size: 15px;
  font-style: normal;
  letter-spacing: .04em;
  color: var(--ss-gold-light, var(--gold));
  margin-bottom: 12px;
}
#incomingOv #incomingCard .ascrow {
  min-height: 68px;
  gap: 14px;
  margin-bottom: 8px;
  padding: 8px 14px;
  border-radius: 10px;
}
#incomingOv #incomingCard .ascic {
  width: 58px;
  flex: 0 0 58px;
  font-size: 30px;
}
#incomingOv #incomingCard .ascic .artic {
  max-height: 54px;
  max-width: 58px;
}
#incomingOv #incomingCard .ascnm {
  font-size: 16px;
  line-height: 1.35;
  /* The sidebar clipped long names to one ellipsised line because it had 224px. There is room now. */
  overflow: visible;
  text-overflow: clip;
  white-space: normal;
}
#incomingOv #incomingCard .asctier {
  font-size: 12px;
}
#incomingOv #incomingCard .ascct {
  font-size: 20px;
}
#incomingOv #incomingCard #ascMeta {
  margin-top: 14px;
  padding-top: 12px;
  font-size: 14px;
  line-height: 1.6;
}
/* ── 🏘 YOUR TOWN IS A THIRD OF THE WIDTH ────────────────────────────────────────────────────────────
 * Josh 2026-07-31: "Make the YOUR TOWN menu about a third of the width it is now but keep the height the
 * same" — and, asked what should fill the space: "nothing inhabits the space it frees up besides the
 * background."
 *
 * So the COLUMN is deliberately left alone. Shrinking the track would hand the width to the two rails or
 * to the Bazaar, and the Bazaar sits in this same column one row down — it keeps its full width and is
 * untouched. Only the panel narrows, anchored left, leaving open backdrop to its right.
 *
 * `justify-self: start` is what makes `width` mean anything here: a grid item defaults to `stretch`, which
 * overrides an explicit width. This rule previously said `stretch` outright, so setting a width without
 * changing it would have done nothing at all — the same trap the deploy rails were in.
 *
 * HEIGHT IS HELD, which is the part that needs watching. At a third of the width the ten buildings have
 * far less room per row and will reflow into fewer columns, which normally means a taller panel. The cap
 * keeps it inside its row so it can never push the Bazaar down, and the list scrolls if it must — but if
 * the buildings end up stacked one-per-row and scrolling, the honest fix is fewer/smaller building cards
 * rather than a taller town, and that is a decision for Josh rather than a number for me. */
/* 2026-08-01, second pass: "expand the YOUR TOWN menu ui vertically so that it touches the top of the SKY
   BAZAAR menu ui and no longer has a scroll wheel. expand it horizontally about 5%-10% so that all the
   buttons UI are not cutoff."

   THE SCROLLBAR WAS MINE. When I narrowed the panel I added `max-height: 100%; overflow-y: auto` as a
   safety valve so a reflowing town could never push the Bazaar down — and predicted in the comment that if
   it ended up scrolling, the answer would be more room rather than a taller cap. It did, and it is.

   `align-self: stretch` is what makes it reach the Bazaar. The panel spans grid rows 1–2 and the Bazaar is
   row 3, so filling its own area puts its bottom edge exactly on the Bazaar's top edge — no gap to tune
   and nothing to keep in step if either row changes. `align-self: start` (what stood here, inherited from
   the rule above) let it stop at its content height and leave the difference as a hole.
   With the full height available the cap comes off, and `overflow: visible` with it: there is now room for
   the content, so a scrollbar would only ever hide something. */
/* THE SCROLLBAR WAS NEVER ON THE PANEL. I put `overflow: visible` on #townPanel and Josh still had a
   scroll wheel, because the scroller is `.townMain` — `skysunder.css:853` gives the town's BODY
   `flex:1; min-height:0; overflow-y:auto`, and that inner box is what was clipping. Capping or uncapping
   the panel around it could never have made any difference. With the panel now stretching to the Bazaar
   there is room for the ten buildings, so the body stops scrolling and grows into it. */
body:has(#prep:not(.hidden)) #townPanel .townMain {
  overflow-y: visible;
  flex: 0 1 auto;
}
body:has(#prep:not(.hidden)) #prepBody > #townPanel {
  justify-self: start;
  align-self: stretch;
  display: flex;
  flex-direction: column;
  width: var(--sky-town-width);
  max-width: var(--sky-town-width);
  min-width: 0;
  max-height: none;
  overflow: visible;
}

body:has(#prep:not(.hidden)) #spellPanel {
  grid-area: spells;
  align-self: start;
  width: auto;
  min-width: 0;
  margin: 0;
}
/* ── 🚩 BANNERS: FIVE SLOTS TALL, WITH A SIXTH'S WORTH OF AIR ABOVE ──────────────────────────────────
 * Josh 2026-07-31: "expand the Banners menu up to meet the spells menu" → "just expand banners enough to
 * comfortably fit 5 slots, and then leave enough expansion space between spells and banners for the 6th
 * slot to pop out."
 *
 * So the gap under SPELLS is not dead space — it is the sixth slot's seat, kept warm. The panel is
 * BOTTOM-ANCHORED (`align-self: end`) and sized by its contents, which puts the slack above it rather
 * than below: five banners leave exactly one slot of air under SPELLS, and recruiting a sixth grows the
 * panel upward into it. Anchoring it to the top instead would put the same gap at the bottom, where it
 * reads as the rail having run out rather than as room to grow.
 *
 * The numbers are the pilot's, not invented here: a banner row is `#armySum > .abat { min-height: 116px }`
 * with an 8px flex gap, so a slot costs 124px and the floor below is five of them plus the panel's own
 * heading. --sky-banner-slot is declared so the arithmetic reads, and so it moves if the pilot's row does.
 */
body:has(#prep:not(.hidden)) #armyPanel {
  grid-area: banners;
  /* ── STRETCH AGAIN, AS OF 2026-08-01 ─────────────────────────────────────────────────────────────
   * This said `align-self: end`, and the note above it explains why: SPELLS used to sit directly on top of
   * the rail in the same column, so the slack between them WAS the growing room and anchoring to the bottom
   * made a sixth banner grow upward into it rather than leaving a gap that reads as "the rail ran out".
   * That reasoning died with the layout it described. SPELLS is in the middle column now and BANNERS owns
   * the right one top to bottom, so `end` just parks the rail against the floor with 165px of dead air
   * above it — measured on screen, and the opposite of the "rethink the squadron banner ui" this change was
   * asked for. Nothing sits above it any more, so there is nothing to grow toward: it fills its column.
   * The five-slot floor and the scroll cap below are unchanged and still do the work they always did. */
  align-self: stretch;
  width: auto;
  min-width: 0;
  margin: 0;
  height: auto;
  /* Five slots is the comfortable floor Josh asked for; the cap keeps the sixth from pushing into SPELLS
     itself, at which point the list scrolls instead. Both are expressed in slots so neither is a number
     someone has to re-derive when a banner row changes height. */
  min-height: min(100%, calc(5 * var(--sky-banner-slot) + var(--sky-banner-head)));
  max-height: 100%;
  overflow-y: auto;
}
body:has(#prep:not(.hidden)) #shopPanel {
  grid-area: bazaar;
  align-self: stretch;
  width: auto;
  min-width: 0;
  margin: 0;
  /* NO max-height here. It was `100%`, which is what let the panel become a scroll container the moment
     its cards asked for more than the row gave them — see the no-scroll block below. */
  /* The width now lives in the second layout pass at the foot of this file: four cards until a fifth
     card actually exists, then five. Reserving the fifth up front — which is what stood here — put an
     empty slot on screen for everyone who has never bought Monopoly. (Josh 2026-07-31) */
  display: flex;
  flex-direction: column;
}
/* ── NO SCROLL IN THE BAZAAR, EVER ───────────────────────────────────────────────────────────────────
 * Josh: "Right now there is a scroll wheel for the shop, which under no circumstance is acceptagble."
 *
 * Three separate rules were conspiring, and only the first is obvious:
 *
 *  1. The pilot sets `#shopCards { height: calc(100% - 85px) }`. The 85px was the roll row's height back
 *     when the roll button sat above the cards; Josh has since moved it up into the panel heading, so the
 *     subtraction now takes 85px that nothing occupies. A percentage height minus a stale constant is a
 *     number that was only ever right once.
 *  2. Each card carries `min-height: 300px`. A flex item at its min-height does not shrink, so when the
 *     container came out shorter than 300px the cards overflowed it rather than fitting.
 *  3. `overflow-y: hidden` on the row then CLIPPED them — which is why the stat rows were cut off mid-line
 *     — while the panel around it grew the scrollbar.
 *
 * So the height is handed back to the content. The row is as tall as a card, the panel is as tall as the
 * row, and the grid gives that row what it asks for. Nothing scrolls because nothing is too big.
 */
body:has(#prep:not(.hidden)) #shopCards {
  height: auto;                   /* was calc(100% - 85px) — see 1 above */
  flex: 0 0 auto;
  min-height: 0;
  align-items: stretch;
  overflow-y: visible;            /* see 3: clipping is what cut the stat rows in half */
  overflow-x: auto;               /* horizontal stays: six-plus cards on a narrow stage still need it */
}
body:has(#prep:not(.hidden)) #shopPanel {
  max-height: none;
  overflow: visible;
}

/* ── 🃏 ITEM CARDS ARE THE SAME LENGTH AS TROOP CARDS ────────────────────────────────────────────────
 * Josh 2026-07-31: "make the item cards the same length as the unit cards."
 *
 * `#shopCards` is `align-items: stretch`, so this looked like it should already be true — and that is why
 * it took resolving the cascade rather than reading it. town-preparation-pilot.css:2398 says:
 *
 *     #shopCards > :is(.card.ss-unit-card, .card.sold.ss-purchased-card, .ss-bazaar-slot-upgrade) {
 *       height: auto; min-height: calc(100% - 2px); align-self: flex-start; }
 *
 * and `align-self` on the ITEM beats `align-items` on the container, every time. Both kinds of card carry
 * `.ss-unit-card` (the pilot's JS adds it on both branches), so every card opted out of stretching and
 * fell back to its own content height. A troop card carries a four-cell stat grid; an item card carries
 * two lines of description. Hence one tall card and one short one, side by side.
 *
 * The `min-height: calc(100% - 2px)` that was supposed to hold the floor cannot: a PERCENTAGE min-height
 * resolves against the container's height, `#shopCards` is `height: auto` here (see the no-scroll block
 * above), and a percentage of `auto` is no constraint at all. It was doing nothing.
 *
 * Stretch is restored on the row's own children. The row is still as tall as its tallest card — the troop
 * card, which is sized by its stat grid — and every other card now matches it. */
/* ⚠️ SPECIFICITY, AND WHY THIS DID NOT WORK THE FIRST TIME.
   I wrote `#shopCards > .card { align-self: stretch }`, checked that prep-layout.css loads last, and
   called it done. It never applied once. The pilot's rule is:

       #shopCards > :is(.card.ss-unit-card, .card.sold.ss-purchased-card, .ss-bazaar-slot-upgrade)

   and `:is()` TAKES THE SPECIFICITY OF ITS MOST SPECIFIC ARGUMENT — here `.card.sold.ss-purchased-card`,
   three classes. So the pilot's selector scores (2,4,1) against my (2,2,1), and load order never enters
   into it: specificity is compared first and only ties fall through to source order. Loading last is not
   the same as winning, and I have been treating them as the same thing all week.
   Adding `#prepBody` — a real ancestor of the shop — makes it (3,2,1). Ids are compared before classes,
   so this wins outright rather than by a tie that the next edit could break. */
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card {
  align-self: stretch;
  min-height: 0;              /* the percentage floor above resolved to nothing; say so rather than imply it */
}
/* With the cards stretched, a short item card has slack under its text — so the Buy row is pinned to the
   bottom and every card's button lands on the same line. The pilot already did this for item cards only
   (`.ss-pilot-item-card .ss-unit-buy`); troop cards were exempt because they had no slack to distribute.
   Applied to both now, since "which card has slack" is no longer a fixed fact. */
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .ss-unit-buy,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card > .buy {
  margin-top: auto;
}

/* ── 🎨 THE ITEM ART SITS CENTRED AT THE TOP ─────────────────────────────────────────────────────────
 * Josh: "make the item cards art centered in the top of the card."
 * `.cic` is already `align-self: center` in a column flex `.top`, so the BOX was centred — but nothing
 * constrained the art inside it. Troop portraits are capped (`.cic.framed:not(.roundframe) .uart` gets
 * max 58px); the item circle is `.roundframe`, which that selector explicitly excludes, so item art was
 * free to overflow its 106px circle and sit off-centre inside it.
 * Both halves are stated here: the box centres in the card, and the art centres in the box. */
body:has(#prep:not(.hidden)) #prepBody #shopCards .ss-pilot-item-card .top {
  /* Foot rule owns the stack (art band → name/rarity). Keep art centred in its row. */
  align-items: stretch;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .ss-pilot-item-card .cic {
  align-self: center;
  margin-inline: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  /* itemart10: soft round box only — hard art clip is .iart-face (do not clip-path
     .cic; that shaved ::after metal and still leaked past the ring). */
  overflow: hidden;
  border-radius: 50%;
  clip-path: none;
  isolation: isolate;
  position: relative;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .ss-pilot-item-card .cic.ss-ench-cic {
  overflow: visible;
  border-radius: 0;
  clip-path: none;
  isolation: auto;
}
/* Sizing lives in the 2026-08-01 rule at the foot of this file — the 72% cap that stood here was the bug
   Josh reported as "warbanner is too small", and leaving it declared twice is how a stale value gets
   believed. See the note there. */
/* The panel's own scroll container, if any sheet gives it one, must not reappear either. */
body:has(#prep:not(.hidden)) #shopPanel,
body:has(#prep:not(.hidden)) #shopPanel > * { overflow-y: visible; }

/* ── THE ROLL BUTTON GOES UP ONTO THE HEADING LINE ───────────────────────────────────────────────────
 * Josh: "moving the MASSIVE roll button up to make more room for the bazaar."
 *
 * It was massive for a reason: the pilot gives #rollBtn `flex: 1`, and #rollRow is a full-width block
 * under the <h2>. When the Bazaar spanned a third of the screen that produced a sensible button; now that
 * it spans two thirds, `flex:1` stretched it to ~1330px and it ate a 54px band above the cards.
 *
 * So the panel becomes a two-row grid and the roll row moves onto the title's line — no markup change, so
 * nothing in renderShop() or the lock-button wiring has to know. The cards get the whole row below.
 */
body:has(#prep:not(.hidden)) #shopPanel {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto auto;
  grid-template-areas:
    "title roll"
    "cards cards";
  align-items: center;
  column-gap: 14px;
}
body:has(#prep:not(.hidden)) #shopPanel > h2 { grid-area: title; margin-bottom: 0; }
body:has(#prep:not(.hidden)) #rollRow {
  grid-area: roll;
  justify-self: end;          /* sits at the right end of the heading line, not stretched across it */
  display: flex;
  margin: 0;
  min-height: 0;
}
body:has(#prep:not(.hidden)) #shopCards { grid-area: cards; }
body:has(#prep:not(.hidden)) #rollBtn {
  flex: 0 0 auto;             /* was flex:1 — the whole reason it was "MASSIVE" */
  min-width: 150px;
  min-height: 34px;
  padding-inline: 18px;
}
body:has(#prep:not(.hidden)) #lockBtn { min-height: 34px; }

/* ── TO BATTLE HUGS THE RIGHT EDGE ───────────────────────────────────────────────────────────────────
 * Josh, twice: "move the to battle button to the top right hugging the edge", then "the to battle button
 * moves to the right edge of the screen".
 *
 * It already had `margin-left: auto`, so it was as far right as it could go — and then skysunder.css and
 * ui-theme-baseline.css each pushed it back in with `margin-right: 246px` / `236px`. That number is
 * --ss-pilot-rail: it reserved a lane for #ascentPanel, which used to be position:absolute at top/right
 * 12px and would otherwise have covered the button. The camp preview is a grid cell now, so the lane it
 * was avoiding does not exist, and the reservation was simply holding the button 236px off the edge.
 */
body:has(#prep:not(.hidden)) #readyBtn {
  margin-left: auto;
  margin-right: 0;
}
body:has(#prep:not(.hidden)) #prepHead { padding-right: 12px; }

/* ── THE BANNER RAIL AND THE BRAND MARK TRADE PLACES ─────────────────────────────────────────────────
 * Josh: "the right side with the troops and skysunder icon trade spots".
 *
 * The pilot puts .ss-prep-brand-mark at `right:13px; bottom:18px` — "the open lower-right island space",
 * which it genuinely was when the banners lived in the LEFT column. Now that the rail runs the full height
 * of the right column, that space is the bottom of the rail, and the monogram sits under the last banner
 * slots. So the two swap: monogram to the top of the right column, slots beginning beneath it.
 *
 * Done with offsets rather than by moving it into the grid, because the element is injected by the pilot's
 * script and is not a child of #prepBody — making it a grid cell would mean owning its markup too.
 */
/* Josh 2026-07-31: "move the SS logo to the direct left of the banners".
   It stays absolutely positioned — the pilot's script appends it to #prep, not to #prepBody, so it cannot
   be a grid item without owning its markup — but the grid reserves the strip it lands in (the `sslogo`
   cells) so nothing else claims that space. Horizontally it is one rail-width plus the grid gap in from
   the right edge; vertically it keeps its original bottom anchor.
   210px of monogram in a 216px strip, which is why --sky-rail-spells went up by 8. */
body:has(#prep:not(.hidden)) #prep .ss-prep-brand-mark {
  right: calc(var(--sky-rail-banners) + 10px);
  bottom: 18px;
  top: auto;
}
/* The 262px reservation is gone with it. The monogram used to sit ABOVE the rail and the banners were
   pushed down to clear it; beside them, it costs the list nothing — the rail gets all 262px back, which
   is most of two extra banner slots. */

/* The old drag handles have no meaning in a fixed grid, and an invisible 6px column that does nothing is
   still a thing to mis-click. */
body:has(#prep:not(.hidden)) #gut1,
body:has(#prep:not(.hidden)) #gut2 { display: none; }

/* ── COLLAPSIBLE PANELS ──────────────────────────────────────────────────────────────────────────────
   Josh: "Make the town, upcoming, and spells panel collapsable."
   Hiding every child except the heading, rather than wrapping each body in a new element: #townPanel's
   body is .townMain plus #oddsBox plus a stock-market box that is itself conditionally hidden, and
   wrapping that lot would have meant editing markup three panels deep for a presentation change. */
.prepPanel.collapsible > h2 {
  cursor: pointer;
  user-select: none;
  display: flex;
  align-items: center;
  gap: 8px;
}
.prepPanel.collapsible > h2::before {
  content: "\2013";               /* en dash open, plus sign closed — the state, not a decoration */
  font-family: inherit;
  opacity: .75;
  width: 9px;
  text-align: center;
}
.prepPanel.collapsible.pcol > h2::before { content: "+"; }
.prepPanel.collapsible.pcol > *:not(h2):not(#goldPill) { display: none; }
.prepPanel.collapsible.pcol {
  /* A collapsed panel is its title bar and nothing else, so it must not keep a body's min-height. */
  min-height: 0;
  height: auto;
  padding-bottom: 8px;
}
.prepPanel.collapsible.pcol > h2 { margin-bottom: 0; padding-bottom: 0; border-bottom: 0; }

/* ── EQUAL BANNER SLOTS ──────────────────────────────────────────────────────────────────────────────
 * Josh: "Make the empty banner slots the exact size as a full banner slot, so they don't visually expand
 * when a unit is purchased."
 *
 * renderArmy() emits far less markup for an empty slot — a title and one line — against a filled slot's
 * name, two stat rows, a count, a gear counter and two buttons. So the rail reflowed on every purchase and
 * every sale, and the row you were aiming at moved as you clicked.
 *
 * Fixed height on BOTH, not min-height on the empty one: a min-height that only the empty slot carries
 * still lets a filled slot grow past it — a long unit name wrapping to two lines would reintroduce exactly
 * the jump this removes. 118px is the measured height of a filled slot at this rail width.
 */
#armySum .abat,
#armySum .abat.empty {
  height: 118px;
  min-height: 118px;
  max-height: 118px;
  box-sizing: border-box;
  overflow: hidden;
}
#armySum .abat.empty { opacity: .55; }

/* ── CHROME JOSH ASKED TO BE RID OF ──────────────────────────────────────────────────────────────────
 * These three were removed from the game's own markup on 2026-07-31 and came straight back, because
 * ui-theme-baseline.css re-adds them as generated content. Removing text from a template does nothing
 * about a stylesheet that draws its own.
 *   Josh: "remove the '10 buildings' text from the YOUR TOWN panel, simlarly remove the 'up to 5' from
 *   the sky bazaar and 'max 6' from the banners and spells"
 */
#armyPanel h2::after,
#townPanel h2::after,
#shopPanel h2::after,
#spellPanel h2::after { content: none !important; }

/* ═══════════════════════════════════════════════════════════════════════════════════════════════════
   2026-07-31, second layout pass. Josh, four things at once:
     "the banner menu should hug the right side. shrink the bazaar to not display the 5th expandable
      slot - just expand the menu when the relic is actually purchased. also, the bazaar menu needs to
      be shrunk vertically to fit under the town menu can you shrink the stats squares in the unit cards
      to account for the extra space needed. there is a lot of extra real estate around the numbers."
   ═══════════════════════════════════════════════════════════════════════════════════════════════════ */

/* ── 1. THE BANNER RAIL HUGS THE RIGHT EDGE ──────────────────────────────────────────────────────────
 * #prepBody carries `padding: 0 16px 14px`, so the rail stopped 16px short of the stage. The left
 * padding stays — the town panel wants the margin — and the head keeps its own 12px so the TO BATTLE
 * button clears the corner. Only the right edge gives way, which is the one the rail is against. */
body:has(#prep:not(.hidden)) #prepBody { padding-right: 0; }

/* ── 2. THE BAZAAR IS FOUR CARDS WIDE UNTIL THE FIFTH EXISTS ─────────────────────────────────────────
 * This reverses the earlier reading of "allow room for it to expand to the additional relic slot if
 * purchased" — that was taken as "reserve the space up front", and reserving it is what put an empty
 * fifth slot on screen for every player who has never bought Monopoly. Josh has now said which he
 * meant: the panel grows when the card arrives.
 *
 * `width` + `justify-self:start` rather than `min-width`, because the panel sits in a `minmax(0,1fr)`
 * column and a min-width cannot make it narrower than the column — it would have stretched to two
 * thirds of the stage no matter what the number said.
 *
 * `max-content`, NOT A CALCULATION. My first attempt computed the width as
 * `4 * card + 3 * 10px + 28px` and came out 6px short, so the fourth card overflowed and the row grew
 * the horizontal scrollbar Josh then reported — the fifth slot was not gone, just parked off-screen.
 * The arithmetic was guessed rather than read, and there was nothing to read confidently anyway:
 * #shopCards has THREE competing gap declarations — 6px in skysunder.css, 10px at pilot:713, 12px at
 * pilot:1809 — plus its own padding and the panel's. `max-content` asks the browser to add up whatever
 * those actually resolve to.
 *
 * It also removes the need to detect the relic at all. The panel is exactly as wide as the cards it
 * holds, so four cards make a four-card panel and the fifth widens it the moment rollShop() deals it
 * (`hasArt('monopoly') ? 5 : 4`). No :has() rule, no magic numbers, and nothing to keep in sync when
 * the card width changes at the two narrower breakpoints. */
body:has(#prep:not(.hidden)) #shopPanel {
  width: max-content;
  max-width: 100%;
  justify-self: start;
}

/* ── 3 & 4. TIGHTER STAT SQUARES, AND THE HEIGHT THAT BUYS ───────────────────────────────────────────
 * "there is a lot of extra real estate around the numbers" — measured, there is. Each square was
 * min-height 55px with 7px/8px/6px padding around a 19px number, in a 2x2 grid with a 7px gap:
 *     2 rows x 55px + 7px gap = 117px of a 300px card, over a third of it, for four numbers.
 *
 * Tightened to 42px cells with 4px/7px/4px padding and a 5px gap:
 *     2 rows x 42px + 5px gap = 89px.   28px saved.
 *
 * The 28px is then TAKEN OFF THE CARD, which is the only part Josh actually asked for — a shorter
 * square on a card of unchanged height just moves the whitespace somewhere else. The card's min-height
 * is what sizes the Bazaar row (the grid gives that row exactly what a card asks for, see the long note
 * above), so 300px -> 272px is 28px off the panel's height directly.
 *
 * The icon comes down 25px -> 22px and the value 19px -> 17px so the glyph and the number still fill
 * their cell rather than floating in a smaller box; the label stays legible at 8.5px. */
body:has(#prep:not(.hidden)) #shopCards .card .ss-pilot-stat-grid { gap: 5px; margin: auto 12px 8px; }
body:has(#prep:not(.hidden)) #shopCards .card .ss-pilot-stat {
  min-height: 42px;
  padding: 4px 7px 4px;
  column-gap: 6px;
  grid-template-columns: 22px minmax(0, 1fr);
}
body:has(#prep:not(.hidden)) #shopCards .card .ss-pilot-stat__icon { width: 22px; font-size: 16px; }
body:has(#prep:not(.hidden)) #shopCards .card .ss-pilot-stat__value { font-size: 17px; }
body:has(#prep:not(.hidden)) #shopCards .card .ss-pilot-stat__label { font-size: 8.5px; }
/* Josh chose the Bazaar as the thing that yields when it and the town cannot both fit. Rather than pick
   a smaller floor — 300 became 272 and would have become another guess — the floor is removed. The row
   above sizes to a card, so with no minimum the card asks for exactly what its content needs and the
   Bazaar is as short as it can be without clipping anything. `align-items: stretch` on #shopCards (the
   pilot sets it, and the no-scroll block above keeps it) still makes every card match the tallest, so
   they stay uniform. This is the same lesson as the width: let the browser add it up. */
body:has(#prep:not(.hidden)) #shopCards .card { min-height: 0; }

/* ── BUILDING UPGRADE PIPS ───────────────────────────────────────────────────────────────────────────
 * Josh: "the building upgrade pips are almost invisible with the deep purple, either change it to a
 * bright purple or yellow."
 * The unlit pip was #2e2757 on the panel's own #1a1533-ish ground — about 1.3:1 contrast, which is not a
 * dim state, it is an absent one. You could not count how many upgrades a building had left. Lit pips are
 * already --gold, so the unlit ones go to a bright purple: it reads clearly against the panel AND stays
 * unmistakably different from gold, which is what carries the "bought / not bought" distinction.
 */
/* The general pip track, for pips outside the town list. Muted gold rather than the violet that used to
   be here — Josh asked for gold pips and a purple track anywhere would be the same complaint again. */
.pips i { background: rgba(232, 182, 76, .26); }
.pips i.lit { background: var(--gold); }

/* ── THE ROLL ROW, 10px HIGHER ───────────────────────────────────────────────────────────────────── */
/* ⚠️ THE TRANSFORM IS GONE, because a transform was never the right tool here. Josh, twice: first "move
   the roll button down about 5 px", then "now the roll button is overlapped, bring it up a bit."
   `translateY` moves a box AFTER layout — it paints somewhere else while still occupying its original
   slot, so it cannot resolve an overlap, only relocate one. Chasing it by ±5px was going to alternate
   forever. The row sits where the grid puts it now, and the heading row is given the height it actually
   needs so the button has somewhere to be. (Josh 2026-08-01) */
body:has(#prep:not(.hidden)) #rollRow { transform: none; }
body:has(#prep:not(.hidden)) #prepBody #shopPanel > h2 { align-self: center; }

/* ── ITEM CARDS MATCH UNIT CARDS ─────────────────────────────────────────────────────────────────────
 * Josh: "make the item cards the same size as unit cards."
 * NOT by pinning a height here. I wrote `height: 330px` first and it broke the assertion above — a later
 * pass had deliberately taken the card's floor to `min-height: 0` so the row sizes to content and
 * `align-items: stretch` makes every card match the tallest. Adding a fixed height back would have undone
 * a decision that was made on purpose, and picked yet another guessed number to be wrong later.
 * The real reason the two looked different was not height. It was that "Company of …" and "Host of …"
 * cards never received the pilot's unit decoration at all — no framed portrait, no stat cells — because
 * it identified units by parsing the card title. Fixed at the source, in renderShop and unitForCard.
 */

/* ── THE BOTTOM-LEFT CONTROL STACK ───────────────────────────────────────────────────────────────────
 * Josh: "move the settings and compendium button down 10px and to the right 5 px."
 * Their position comes from inline `bottom:84px` / `bottom:38px` on the rows themselves and no `.navrow`
 * rule exists in any stylesheet, so there is no declared origin to add to. A transform is the honest way
 * to say "10 down, 5 right" without first having to discover where they start.
 */
#compRow, #setRow { transform: translate(5px, 10px); }

/* ══ 2026-08-01, fourth pass ═══════════════════════════════════════════════════════════════════════ */

/* ── A BOUGHT-OUT BAZAAR KEEPS ITS SIZE — superseded, see the fifth pass at the foot of this file ──
 * The first attempt put a `min-height: 300px` FLOOR on the row, taken from the pilot's per-card
 * `min-height`. It was wrong twice over: the bought-out row already stood at ~315px so the floor could
 * never fire, and a floor only prevents shrinking while Josh reported the panel GROWING as well. The row
 * is a fixed grid track now. Nothing is left here — two declarations of --sky-bazaar-row would have meant
 * whichever a reader (or a regex) found first was the one they believed.
 */

/* ── 2. THE "ALREADY IN YOUR ARMY" ARROWS MOVE INTO THE RECRUIT BUTTON ────────────────────────────
 * Josh: "The arrows that designate a unit card in the shop that you already have in your army are broken.
 * have them instead hover inside the recruit button now, pointing at the text RECRUIT."
 *
 * WHY THEY BROKE. They were `::before`/`::after` on the card, translated fully outside it — which needs
 * `overflow: visible` on the card to be seen at all. `skysunder.css` sets exactly that (`.card.inArmy
 * { overflow: visible }`), and the pilot then sets `overflow: hidden` on every `#shopCards .card` for its
 * rounded frame and art bleed. The pilot wins, so both arrows were being clipped away entirely.
 *
 * Inside the button there is nothing to clip them: they sit within the element they point at, which is
 * also a better place to say what they mean — the marker now decorates the ACTION rather than the card. */
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.inArmy::before,
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.inArmy::after { content: none; }

/* ⚠️ SECOND ATTEMPT. The first put the markers at `left: 7px` / `right: 7px` — the BUTTON's edges, which
   on a 220px button is nowhere near the word. Josh, with a screenshot: "have them on either side of the
   RECRUIT text".

   ABSOLUTE POSITIONING WAS THE WRONG TOOL. "Beside the text" is not a coordinate — the label is
   "RECRUIT 40G" on a troop card and "ACQUIRE 50G" on an item (cost merged into the button), and the
   button's width is set by the card, so any offset I pick is right for one label and wrong for the next.
   Making the button a flex row and letting the two pseudo-elements be FLEX ITEMS puts them either side
   of the label by construction, at a fixed gap from the glyphs rather than from the border, whatever
   the text says or the button measures.
   The pilot already sets `display: inline-flex` on this button for its own centring, so this is adding a
   gap and two items to a row that exists rather than changing how the button lays out. */
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.inArmy :is(.buy, .ss-unit-buy .buy) {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  overflow: visible;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.inArmy :is(.buy, .ss-unit-buy .buy)::before,
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.inArmy :is(.buy, .ss-unit-buy .buy)::after {
  content: "";
  /* static, not absolute: these are items in the row now, so the row spaces them against the label */
  position: static;
  flex: 0 0 auto;
  width: 0; height: 0;
  border-style: solid;
  pointer-events: none;
  filter: drop-shadow(0 0 4px rgba(255, 240, 200, .75));
}
/* Both point INWARD, at the word between them. */
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.inArmy :is(.buy, .ss-unit-buy .buy)::before {
  border-width: 6px 0 6px 8px;
  border-color: transparent transparent transparent #ffeec2;
  animation: inArmyBtnL 1.05s ease-in-out infinite;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.inArmy :is(.buy, .ss-unit-buy .buy)::after {
  border-width: 6px 8px 6px 0;
  border-color: transparent #ffeec2 transparent transparent;
  animation: inArmyBtnR 1.05s ease-in-out infinite;
}
/* A nudge toward the word and back — the same 1.05s beat the card-edge arrows used, so it reads as the
   same signal relocated rather than a new one. `translate` only: as flex items they hold their own space,
   so nothing reflows as they move. */
@keyframes inArmyBtnL {
  0%, 100% { transform: translateX(0);   opacity: .45; }
  50%      { transform: translateX(3px); opacity: 1; }
}
@keyframes inArmyBtnR {
  0%, 100% { transform: translateX(0);    opacity: .45; }
  50%      { transform: translateX(-3px); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  body:has(#prep:not(.hidden)) #prepBody #shopCards .card.inArmy :is(.buy, .ss-unit-buy .buy)::before,
  body:has(#prep:not(.hidden)) #prepBody #shopCards .card.inArmy :is(.buy, .ss-unit-buy .buy)::after {
    animation: none; opacity: .9;
  }
}

/* ── 4 & 5. THE PREP HEADER ───────────────────────────────────────────────────────────────────────
 * "Shrink the main menu button vertically to be the same size as GOLD and NEXT INCOME."
 * MAIN MENU is a `.ubtn`, which the theme gives the shared 40px control height; the pills beside it are
 * sized by their own text. Rather than pick a number, it is told to match the pill — one declaration, and
 * if the pills are ever restyled the button follows instead of drifting. `align-self` matters as much as
 * the height: in a flex header a stretched item ignores its own height. */
/* Toolbar controls share one size; TO BATTLE stays the large primary. */
body:has(#prep:not(.hidden)) #prepHead #menuBtn2 {
  min-height: 40px;
  height: 40px;
  align-self: center;
  padding-block: 0;
  padding-inline: 14px;
  line-height: 1.15;
  font-size: 12px;
  box-sizing: border-box;
}
/* Town purse only. Do not touch panel/card/slot chrome elsewhere — those
   live in the Downloads sheets. Strip under Your Town: amount + Gold, then
   a 2×5 of 100G ingots. */
body:has(#prep:not(.hidden)) #townPanel #goldPill {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: flex-start;
  flex: 0 0 auto;
  gap: 6px;
  box-sizing: border-box;
  width: 100%;
  min-height: 0;
  height: auto;
  margin: 0 0 8px;
  padding: 7px 12px;
  position: static;
  cursor: help;
  font-family: var(--ss-font-display, Cinzel, Georgia, serif);
  font-size: 14px;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: .14em;
  color: var(--ss-gold-light, var(--gold));
  border: 1px solid rgba(142, 120, 88, .68);
  border-radius: 8px;
  background: none;
  box-shadow: inset 0 0 0 1px rgba(241, 230, 200, .025);
  overflow: visible;
}
body:has(#prep:not(.hidden)) #townPanel #goldPill .ss-gold-amt {
  display: flex;
  align-items: baseline;
  justify-content: flex-start;
  gap: 0.35em;
  /* Digits + Gold stay tight. Reserve "10,000 Gold" so the bar rack width
     does not reflow when the purse changes mid-game. */
  box-sizing: border-box;
  flex: 0 0 auto;
  width: calc(6.5ch + 0.35em + 5ch);
  min-width: calc(6.5ch + 0.35em + 5ch);
  max-width: none;
  margin: 0;
}
body:has(#prep:not(.hidden)) #townPanel #goldPill .ss-gold-stack {
  /* Fill leftover pill width (stable once amt footprint is reserved). Stretch
     bars in X only — height stays 8px; fill count is JS, not size. */
  position: relative;
  box-sizing: border-box;
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
  max-width: none;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  grid-template-rows: repeat(2, 8px);
  gap: 2px 3px;
  pointer-events: none;
}
body:has(#prep:not(.hidden)) #townPanel #goldPill .ss-gold-slot {
  box-sizing: border-box;
  display: block;
  width: 100%;
  height: 8px;
  border: 0;
  background: none;
}
body:has(#prep:not(.hidden)) #townPanel #goldPill .ss-gold-stack img {
  display: block;
  width: 100%;
  height: 8px;
  object-fit: fill;
  opacity: .22;
}
body:has(#prep:not(.hidden)) #townPanel #goldPill .ss-gold-slot.filled img {
  opacity: 1;
}
body:has(#prep:not(.hidden)) #townPanel #goldPill .goldLab {
  font-family: var(--ss-font-display, Cinzel, Georgia, serif);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: .14em;
  color: var(--ss-gold-light, var(--gold));
}
body:has(#prep:not(.hidden)) #townPanel #goldPill .goldtxt,
body:has(#prep:not(.hidden)) #townPanel #goldPill #goldTxt {
  font-family: var(--ss-font-display, Cinzel, Georgia, serif);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: .14em;
  color: var(--ss-gold-light, var(--gold));
  font-variant-numeric: tabular-nums;
}
body:has(#prep:not(.hidden)) #townPanel #goldPill .goldtxt {
  display: inline-block;
  min-width: 0;
}
body:has(#prep:not(.hidden)) #townPanel #goldPill #incomeTxt.incAmt,
body:has(#prep:not(.hidden)) #townPanel #goldPill .incAmt {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  font-weight: 700;
  color: var(--ss-gold-light, var(--gold));
}
/* Prep clock — same chrome box as Gold; centre the digits in the pill (Josh 2026-08-07).
   Cinzel + the shared pill’s 7px block padding sat the glyphs high/left; flex + line-height:1
   keeps "3:00" / "⏸" optically centred like the gold amount. */
body:has(#prep:not(.hidden)) #prepHead #prepTimer {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 40px;
  min-width: 64px;
  box-sizing: border-box;
  padding: 0 14px;
  font-family: var(--ss-font-ui, "Source Sans 3", "Segoe UI", system-ui, sans-serif);
  font-size: 15px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: .04em;
  font-variant-numeric: tabular-nums;
  text-align: center;
  color: #d9c2ff;
}


/* ══ 2026-08-01, fifth pass ════════════════════════════════════════════════════════════════════════ */

/* ── 1. THE SS WATERMARK IS OFF THE SHOP SCREEN ──────────────────────────────────────────────────
 * Josh: "remove the SS logo watermark from the shop menu entirely."
 * `display: none` rather than deleting the pilot's rules: the mark is Steve's, it is styled in
 * town-preparation-pilot.css which he still owns, and it may well belong on another screen later. This
 * file's job is to say it does not belong on THIS one. */
body:has(#prep:not(.hidden)) #prep .ss-prep-brand-mark { display: none; }

/* ── 4. THE BAZAAR IS ONE FIXED HEIGHT, ALWAYS ───────────────────────────────────────────────────
 * Josh, with two screenshots: "when I buy a unit, the shop EXPANDS vertically now. When I purchase
 * everything but items, the shop menu shrinks vertically."
 *
 * BOTH DIRECTIONS ARE THE SAME BUG, AND MY LAST FIX WAS AIMED TOO LOW. The Bazaar's grid row is `auto`,
 * so it is sized by its TALLEST CARD — and the cards are not all one height:
 *
 *     troop card   portrait + tier bar + name + pills + a four-cell stat grid + buy   ≈ 475px
 *     item card    portrait + tier bar + name + one line of text + buy                ≈ 315px
 *     empty slot   nothing at all
 *
 * So the panel grew when a troop card was dealt and collapsed when the last one was bought. I put a
 * `min-height: 300px` floor on it last round, taken from the pilot's `.card { min-height }` — and 300 is
 * BELOW the ~315px the bought-out row already stood at, so it could never have fired. The number was
 * defensible and the measurement was wrong.
 *
 * A floor cannot fix this anyway, because the request is not "never shrink" — it is "keep it exactly the
 * same size", and a floor still lets it GROW. So the row is a fixed track: the Bazaar is always one troop
 * card tall, whatever it happens to be holding, and the top band gets a stable remainder instead of one
 * that changes every time a card is bought. That also fixes the town being clipped in the first
 * screenshot, which was the same fluctuation seen from the other end.
 *
 * --sky-bazaar-row is a real design constant — the height of a full troop card — and it is the one number
 * on this screen that cannot be derived, because the case it exists for is the one where no troop card is
 * present to measure. Cards stretch to it, so an empty slot is a full-height frame rather than a stub. */
body:has(#prep:not(.hidden)) #prepBody {
  /* 476 → 496 on 2026-08-01: a rare/epic card carries an imprint pill the common ones do not, and its
     RECRUIT row was clipping. Raised alongside reclaiming ~45px of reserved-but-unused space in the
     card itself (see the note on .cn / .pillrow at the foot of this file) — the extra headroom is for
     a name that genuinely WRAPS, not a substitute for the padding that was doing nothing.
     DECLARED ONCE. I appended a second `--sky-bazaar-row` here and caught it immediately: one round
     earlier a duplicate of this exact variable had the test reading the stale value and handing the
     wrong number back to me as a pass. */
  --sky-bazaar-row: 496px;        /* one troop card: portrait + tier + name + pills + stat grid + buy */
  /* The `grid-template-rows` that used to sit here has MOVED UP to the block that declares
     grid-template-areas (2026-08-01). Two copies at equal specificity meant the row count and the area
     template could disagree — and did, the moment the template went from three rows to two: the edit
     landed on the earlier copy, this one went on winning with three tracks, and the Bazaar collapsed to a
     29px sliver. The variable stays here with its reasoning; the track list belongs next to the areas it
     has to agree with. */
}
body:has(#prep:not(.hidden)) #prepBody #shopPanel { align-self: stretch; min-height: 0; }
body:has(#prep:not(.hidden)) #prepBody #shopCards {
  min-height: 0;                  /* the floor is the grid track now, not this box */
}
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.sold {
  align-self: stretch;
  height: auto;
  min-height: 0;
}


/* ── 🛒 THE BOUGHT-OUT SHOP, SECOND ATTEMPT ───────────────────────────────────────────────────────
 * Josh, with a screenshot of an empty Bazaar: "when you buyout the shop the UI is all moved."
 *
 * FIXING THE TRACK WAS ONLY HALF OF IT. The grid row is 476px now, so the PANEL holds its size — but
 * #shopPanel is itself a grid (`"title roll" / "cards cards"`) with both rows `auto`. Give that grid more
 * height than its content needs and the extra goes into the row GAPS, not into the cards: the heading
 * drifts up into the panel border and the four empty frames sink to the bottom, which is exactly the
 * screenshot. I had set `flex: 1 1 auto` on #shopCards to absorb it — meaningless, because its parent is
 * a grid, not a flex container. That was the second time this week I aimed a flex property at a grid.
 *
 * So the CARDS row takes the slack: `auto minmax(0, 1fr)`. The heading keeps exactly what it needs, the
 * card row gets everything else, and the frames fill it instead of collecting at the bottom. */
body:has(#prep:not(.hidden)) #prepBody #shopPanel {
  grid-template-rows: auto minmax(0, 1fr);
  align-content: stretch;
  row-gap: 8px;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards {
  height: 100%;
  min-height: 0;
  align-items: stretch;
}

/* ── 🃏 THE DEAD SPACE ABOVE THE PILLS ────────────────────────────────────────────────────────────
 * Josh 2026-08-01, with a screenshot of four cards whose RECRUIT row is cut off: "now the recruit and
 * cost is cutoff. it looks like there is deadspace between the unit card name and the imprints to remove
 * that could fix this problem."
 *
 * He is right, and it is the better fix than raising the track again. The pilot reserves fixed minimum
 * heights for two blocks that only need them when their text WRAPS:
 *
 *     .cn      { min-height: 46px }   a two-line name — "Dwarf Cannoneer" is one line at ~22px
 *     .pillrow { min-height: 47px }   two rows of pills — most cards carry one row plus an imprint
 *
 * On a card that wraps neither, that is ~45px of nothing between the title and the pills, and the card
 * overflows the track by roughly the amount it is wasting. Reserving space for the worst case on every
 * card is what makes the common case not fit.
 *
 * Both become content-sized with a floor that fits ONE line, so a wrapping name still gets its second row
 * — the blocks grow when they need to instead of always standing at their maximum. The tier bar and the
 * stat cells are untouched: those are uniform by design and are what let two cards be compared. */
body:has(#prep:not(.hidden)) #prepBody #shopCards .card .cn {
  min-height: 0;
  padding-block: 0;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .card .pillrow {
  min-height: 0;
  margin: 0;
}
/* The stack's own rhythm comes down with them — 6px between five blocks is 30px on a card that is short
   of about 40. */
body:has(#prep:not(.hidden)) #prepBody #shopCards .card .cmeta {
  gap: 3px;
  padding-bottom: 0;
}

/* ── 📝 BAZAAR DESC IS HOVER-ONLY ──────────────────────────────────────────────────────────────────
 * Josh 2026-08-02: tip/desc on shop cards duplicates the hover tooltip. `renderShop` no longer emits
 * `.cd.cdmeta`. Hide it under `#shopCards` anyway so a shared emitter cannot reopen the band, and drop
 * the pilot's reserved `.cd` min-height (39–46px) so a blocked-reason line or a sold slot does not leave
 * an empty description hole. Stats, pills, names, tier bars, and Recruit stay untouched. */
body:has(#prep:not(.hidden)) #prepBody #shopCards .card .cd.cdmeta {
  display: none;
  min-height: 0;
  margin: 0;
  padding: 0;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .card .cd:not(.cdmeta) {
  min-height: 0;
  margin: 4px 12px 6px;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.sold .cd {
  display: none;
  min-height: 0;
  margin: 0;
}

/* ── 🟡 THE BUILDING-LEVEL PIPS ARE GOLD ──────────────────────────────────────────────────────────
 * Josh 2026-08-01: "make the pips showing building level yellow."
 *
 * skysunder.css has said `.pips i.on { background: var(--gold) }` all along, and so does the rule further
 * up THIS file — both lose. `town-preparation-pilot.css:647` scopes its own to
 * `#bldList .bld .pips i.on` and paints them `var(--ss-fissure)`, the violet, with a matching glow. Two
 * gold declarations in two files, and the purple one won on specificity, which is why the pips looked
 * violet no matter what the general rule said.
 * Scoped past it via #prepBody — ids are compared first — rather than reaching for !important. The glow
 * goes gold with the fill: a violet halo around a gold pip just looks like a mistake.
 * The OFF pips stay dark on purpose; the level you have is the thing being shown, and if every pip glows
 * there is nothing to read. */
body:has(#prep:not(.hidden)) #prepBody #bldList .bld .pips i.lit {
  background: var(--ss-gold-light, var(--gold));
  box-shadow: 0 0 5px rgba(232, 182, 76, .5);
}

/* ── 🖼 ITEM ART FILLS ITS PORTRAIT ───────────────────────────────────────────────────────────────
 * Josh 2026-08-01: "make the item art fitted inside its portraits in the shop card ui. warbanner is too
 * small."
 *
 * I capped item art at 72% of its circle when I centred it, which was the wrong lever. The cap existed to
 * stop art OVERFLOWING the frame — but `object-fit: contain` already guarantees that, by scaling the
 * image down until both axes fit and never past it. The percentage was doing nothing except shrinking
 * everything by a further quarter, and it hurt most on the art whose own aspect ratio is furthest from
 * square: the war banner is tall and narrow, so 72% of the circle's WIDTH left it a sliver.
 * At 100% each item is as large as its own proportions allow inside the frame, which is what "fitted"
 * means — and no two items need the same number for it to be true. */
body:has(#prep:not(.hidden)) #prepBody #shopCards .ss-pilot-item-card .cic > img,
body:has(#prep:not(.hidden)) #prepBody #shopCards .ss-pilot-item-card .cic .uart,
body:has(#prep:not(.hidden)) #prepBody #shopCards .ss-pilot-item-card .cic .iart {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  margin: auto;
  object-fit: contain;
}

/* ── 🟡 THE UNLIT PIPS WERE THE PURPLE ONES ───────────────────────────────────────────────────────
 * Josh 2026-08-01, after the previous fix: "the pips are still dark purple, not gold."
 *
 * I recoloured `.pips i.on` and stopped, having reasoned that the unlit ones should stay dark so the level
 * stays readable. That was right about the principle and wrong about the picture: a building at LEVEL 0 —
 * which is every building at the start of a run, and what Josh was looking at — has NO lit pips at all.
 * Every pip on screen was the `#423b4f` unlit colour, so "the pips" were, correctly, still purple. My
 * change was invisible on exactly the screen he was reporting.
 *
 * Both states are gold now, separated by BRIGHTNESS rather than by hue: lit is `--ss-gold-light` with a
 * glow, unlit is a heavily muted gold. The level is still readable at a glance — which was the real
 * requirement behind keeping them different — and the widget reads as one gold meter instead of a purple
 * track that happens to fill with gold. */
body:has(#prep:not(.hidden)) #prepBody #bldList .bld .pips i {
  background: rgba(232, 182, 76, .26);
}

/* ── 🟡 THIRD TIME, AND THE REASON IT KEPT COMING BACK ────────────────────────────────────────────
 * Josh, a third time, with a screenshot of the Light and Medium Armory rows: "they are still purple."
 *
 * The two fixes above are both real and both apply — I checked the cascade and the DOM, and
 * `#prepBody #bldList .bld .pips i` genuinely wins over the pilot's violet. The colour was still wrong
 * because of ALPHA, not selectors. `rgba(232, 182, 76, .26)` is 26% gold over a `#1c1836`-ish panel,
 * which composites to about #51413c — a muddy brown-violet. I changed the hue and left the panel
 * showing through three quarters of it, so the pips carried on looking exactly as purple as before.
 * "Make it gold" was answered with a gold-flavoured wash over purple.
 *
 * An UNLIT pip is a slot you have not bought yet. Was opaque dim gold (#6f5a28) so it could not
 * wash purple through alpha — but that read as a row of filled yellow blocks at level 0.
 * 2026-08-05: open squares (transparent fill + antique-gold border) so unpurchased levels read as
 * empty slots; lit pips stay filled bright gold. box-sizing keeps the 9×6 footprint with the border. */
body:has(#prep:not(.hidden)) #prepBody #bldList .bld .pips i,
body:has(#prep:not(.hidden)) #prepBody #bldList .bld .bn .pips i {
  box-sizing: border-box;
  background: transparent;
  border: 1px solid #c8a35a;
  box-shadow: none;
}

/* ── 🟣 THE LIT PIPS WERE PURPLE BECAUSE `.on` IS A THEME SKIN (Josh 2026-08-01, THIRD report) ─────
 * "The pips for the YOUR TOWN menu are still purple instead of gold."
 *
 * He was right all three times, and the two passes above were both looking at the wrong pip. This is the
 * LIT state, and the reason it never changed is not specificity and not alpha — it is `!important` from a
 * file nobody thought to look in. `assets/ui-theme-baseline.css` carries the theme's SELECTED-CONTROL
 * skin, and its selector list is:
 *
 *     .sel, .on, [aria-pressed="true"], .diffBtn.sel, .ascBtn.sel, .sortChip.on, .setTab.sel, … {
 *       background: radial-gradient(… rgba(199,144,255,.32) …),
 *                   linear-gradient(145deg, rgba(98,36,148,.92), rgba(43,18,74,.97) …) !important;
 *       box-shadow: … 0 0 16px rgba(138,61,255,.25) !important;
 *     }
 *
 * A bare `.on` — meant for a selected tab or a difficulty button — and `pips()` in 05-town.js emits a lit
 * pip as exactly `<i class="on">`. So every lit pip was being painted with the purple "this tab is
 * selected" gradient, with a violet glow, `!important`. Two ids or three made no difference: an
 * `!important` declaration is only beaten by another `!important` one, so `background: #f0c85a` here lost
 * to a `(0,1,0)` selector and went on losing however the colour was written.
 *
 * HOW IT HID FROM THE LAST TWO PASSES. `getComputedStyle(pip).backgroundColor` reads
 * `rgba(0, 0, 0, 0)` — transparent — because the winning `background` is a GRADIENT: the paint lives in
 * background-image and the colour channel is empty. Checking the colour therefore says "nothing is set
 * here", which reads like the fix simply not applying rather than like something else winning. The pip
 * looked purple, measured transparent, and every regex over this stylesheet said gold.
 *
 * THE FIRST FIX WAS `!important` AT THREE IDS, AND IT IS GONE AGAIN. Josh, asked whether to leave it or
 * disarm the thing properly: "yeah disarm the landmine." So the state is renamed instead of out-shouted —
 * `pips()` emits `<i class="lit">`, the theme's `.on` skin no longer matches a pip at all, and this rule
 * wins on ordinary specificity with no `!important` anywhere in it. Out-shouting the theme would have left
 * the trap armed for the next element that happened to be called `on`, and would have needed a second
 * `!important` on the glow forever.
 *
 * The pilot's `#bldList .bld .pips i.on` violet rule is deliberately left in place and simply never fires
 * now: it is Steve's file, and a rule that cannot match is not worth reaching into his sheet for.
 * preplayouttest.js reads the baseline's skinned-selector list and asserts the pip's lit class is not in
 * it, so a future rename cannot walk back into the same trap by accident. */
body:has(#prep:not(.hidden)) #prepBody #bldList .bld .pips i.lit,
body:has(#prep:not(.hidden)) #prepBody #bldList .bld .bn .pips i.lit {
  background: #f0c85a;
  box-shadow: 0 0 5px rgba(232, 182, 76, .55);
}

/* ── BONUS-MILESTONE PIP OUTLINE (Josh 2026-08-03; muted violet 2026-08-03) ─────────────────────────
 * Armory 3rd/5th, Mana Channeler 5th, Troop Channeler 5th, Summoning Circle 5th. Outline/ring ONLY —
 * do not touch background. Lit fill stays on `.lit` (gold-vs-violet fix). Class is `ss-pip-bonus`,
 * not a theme utility. Unlit bonus pips still get the ring so the milestone is readable before you buy it.
 * Ring is muted dusk violet (`#5c3a9e`), not neon fissure `#8a3dff` and not gold — still reads “special”. */
body:has(#prep:not(.hidden)) #prepBody #bldList .bld .pips i.ss-pip-bonus,
body:has(#prep:not(.hidden)) #prepBody #bldList .bld .bn .pips i.ss-pip-bonus {
  outline: 1px solid #5c3a9e;
  outline-offset: 1px;
  box-shadow: 0 0 0 1px rgba(92, 58, 158, .55), 0 0 6px rgba(92, 58, 158, .28);
}
body:has(#prep:not(.hidden)) #prepBody #bldList .bld .pips i.lit.ss-pip-bonus,
body:has(#prep:not(.hidden)) #prepBody #bldList .bld .bn .pips i.lit.ss-pip-bonus {
  /* Keep gold fill glow AND the muted violet bonus ring when both classes are present. */
  box-shadow: 0 0 5px rgba(232, 182, 76, .55), 0 0 0 1px rgba(92, 58, 158, .65), 0 0 7px rgba(92, 58, 158, .35);
}

/* 🔮 Magic Golem face to the RIGHT of Summoning Circle pips (Josh 2026-08-03; gap fix golemface1).
 * Nested inside `.pips` with position:absolute so name / pip row / MAX never reflow.
 * Pilot sets `.pips { display:flex }` which STRETCHES the span to full `.bn` width — so
 * `left:100%` used to land on the MAX column. Shrink-wrap with inline-flex + max-content so
 * `left:100%` is past the 5th pip, in the gap before the button. Vertically centered on pips. */
body:has(#prep:not(.hidden)) #prepBody #bldList .bld .bn .pips {
  position: relative;
  display: inline-flex;
  width: max-content;
  max-width: 100%;
  overflow: visible;
}
body:has(#prep:not(.hidden)) #prepBody #bldList .bld .bn .ss-golem-face {
  position: absolute;
  left: calc(100% + 4px);
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  margin: 0;
  vertical-align: middle;
  border-radius: 3px;
  overflow: hidden;
  cursor: help;
  z-index: 2;
  pointer-events: auto;
  box-shadow: 0 0 0 1px rgba(240, 200, 90, .7), 0 0 6px rgba(176, 127, 232, .35);
}
body:has(#prep:not(.hidden)) #prepBody #bldList .bld .bn .ss-golem-face .artic,
body:has(#prep:not(.hidden)) #prepBody #bldList .bld .bn .ss-golem-face img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;
  display: block;
}

/* ── 🏷 BAZAAR PILL ROWS: CENTRED, EQUAL GAP, EQUAL PAIR WIDTHS ──────────────────────────────────
 * Dave 2026-08-02: Troll Whelps (MEDIUM/TROLL then DEFENDER/FEROCIOUS) — row gaps and side margins
 * did not match. Two leftover causes after the earlier flex override of the pilot's 3-column grid:
 *
 *  1. `town-preparation-pilot.js` stamps `.ss-imprint-pill` at decorate time. An older rule then set
 *     `margin-inline: auto` on that class (meant to centre a lone imprint on a grid row). On a flex
 *     wrap line next to DEFENDER, those auto margins ate free space and opened a wider gap than the
 *     MEDIUM↔TROLL pair above, which only had the flex `gap`.
 *  2. Content-sized pills of different string lengths made each wrap line a different total width, so
 *     the two centred groups did not share the same side margins.
 *
 * Nested `catPills()` `.pillrow` is still dissolved with `display: contents` so weight/race/class/
 * imprint are siblings. Outer row stays flex + wrap + centre. Each visible pill takes half the row
 * (tier stays `display: none`), so pairs share width and the gap is only the flex gap. Scoped to
 * `#prepBody #shopCards` unit cards — army `.as` pills and item cards are untouched. */
body:has(#prep:not(.hidden)) #prepBody #shopCards .card .pillrow .pillrow {
  display: contents;
}

body:has(#prep:not(.hidden)) #prepBody #shopCards .card .pillrow {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-content: flex-start;
  align-items: stretch;
  gap: 5px 6px;
}

body:has(#prep:not(.hidden)) #prepBody #shopCards .card .pillrow .mpill:not(.ss-tier-pill) {
  flex: 1 1 calc(50% - 3px);
  max-width: calc(50% - 3px);
  box-sizing: border-box;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  margin: 0;
  text-align: center;
}

body:has(#prep:not(.hidden)) #prepBody #shopCards .card .ss-imprint-pill {
  min-width: 0;
  width: auto;
  max-width: calc(50% - 3px);
  margin-inline: 0;
  text-align: center;
}


/* ══ 2026-08-01, sixth pass ════════════════════════════════════════════════════════════════════════ */

/* ── THE RELIC RAIL ──────────────────────────────────────────────────────────────────────────────
 * Collected relics as icons only, in the strip between Compendium and the battle-preview eye.
 * Hover shows the bonus. Markup is `#relicRail` in skysunder.html; contents come from
 * renderRelicRail() / layoutRelicRail() in 05-town.js.
 *
 * MULTI-ROW PACKING (Josh 2026-08-04, relicrail8). Default is one row of 40px cells — matching
 * MAIN MENU / Compendium height. JS prefers fewer rows and grows `--sky-relic-cell` within each
 * band (1× ≥28≤40, 2× ≥16≤28, 3× ≤16), publishing `--sky-relic-rows` / `--sky-relic-cols` plus
 * width = packed pitch (not the full leftover strip). `justify-content: start` stacks icons left
 * with the fixed `--sky-relic-gap` — no space-between spread across scout / TO BATTLE. Layout is
 * CSS GRID (`grid-auto-flow: row` + fixed row/col tracks) so wrap is required. Few relics never
 * pre-allocate a second track (`--sky-relic-rows` stays 1 while a readable 1-row still fits).
 *
 * WHY THE RAIL IS FLUSH AFTER COMPENDIUM (relicrail4).
 * Earlier `margin-left: auto` on the rail right-packed it against the eye. That looked fine while
 * the rack filled the leftover width (1–2 row), but the 2→3 step narrows the grid — auto margin
 * opened a hole after Compendium and the taller 3×20 rack grew #prepHead so TO BATTLE dropped.
 * Rail now has no auto margin (left edge stays after Compendium); `#incomingBtn` owns `margin-left:
 * auto` whether the rail is present or empty. 3-row cell max is 16px so pack height (56) stays ≤
 * the 2-row band at 28 (60). `#prepHead` is nowrap so eye / TO BATTLE cannot wrap under the rack.
 *
 * WHY `min-width: 0` AND `overflow: hidden`.
 * Safety net if measuring lags a frame — clips rather than shoving TO BATTLE onto a second line.
 * Packing sets width to the packed pitch so icons stay left-aligned. */
body:has(#prep:not(.hidden)) #prepHead {
  flex-wrap: nowrap;
  align-items: center;
}
body:has(#prep:not(.hidden)) #prepHead #relicRail {
  --sky-relic-cell: 40px;
  --sky-relic-gap: 4px;
  --sky-relic-rows: 1;
  --sky-relic-cols: 1;
  display: grid;
  grid-template-rows: repeat(var(--sky-relic-rows), var(--sky-relic-cell));
  grid-template-columns: repeat(var(--sky-relic-cols), var(--sky-relic-cell));
  grid-auto-flow: row;
  align-items: center;
  justify-items: center;
  justify-content: start;
  gap: var(--sky-relic-gap);
  align-self: center;
  margin-left: 0;
  flex: 0 0 auto;
  min-width: 0;
  max-height: calc(
    var(--sky-relic-rows) * var(--sky-relic-cell)
    + (var(--sky-relic-rows) - 1) * var(--sky-relic-gap)
  );
  box-sizing: content-box;
  overflow: hidden;
}
/* data-rows forces the track count even if a stale inline --sky-relic-rows lags one frame. */
body:has(#prep:not(.hidden)) #prepHead #relicRail[data-rows="2"] {
  --sky-relic-rows: 2;
  grid-template-rows: repeat(2, var(--sky-relic-cell));
}
body:has(#prep:not(.hidden)) #prepHead #relicRail[data-rows="3"] {
  --sky-relic-rows: 3;
  grid-template-rows: repeat(3, var(--sky-relic-cell));
}

/* Round 1 holds no relics, and an empty flex box still claims its share of the header's auto margins —
 * which would shove TO BATTLE left of where it sits today for no reason. */
body:has(#prep:not(.hidden)) #prepHead #relicRail:empty {
  display: none;
}

/* ── 👁 THE EYE, LEFT OF TO BATTLE ────────────────────────────────────────────────────────────────
 * Square icon, same 40px control height as MAIN MENU / Compendium. Markup order is
 * #relicRail → #incomingBtn → #readyBtn.
 * margin-left:auto always — pulls eye+TO BATTLE to the right edge; rail stays flush after Compendium. */
body:has(#prep:not(.hidden)) #prepHead #incomingBtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  align-self: center;
  margin-left: auto;
  min-height: 40px;
  height: 40px;
  width: 40px;
  min-width: 40px;
  padding: 0;
  color: var(--ss-gold-light, var(--gold));
}
body:has(#prep:not(.hidden)) #prepHead #incomingBtn:hover {
  color: #ffe6a8;
  border-color: var(--ss-border-gold, #c8a35a);
}
body:has(#prep:not(.hidden)) #prepHead #incomingBtn svg {
  display: block;
}

/* Right cluster: eye owns the auto-margin; Compendium stays beside MAIN MENU; TO BATTLE stays flush. */
body:has(#prep:not(.hidden)) #prepHead #readyBtn {
  margin-left: 0;
}
/* Skirmish has no altitude, so the button is not there at all — see syncIncomingBtn() in 02-data.js for
   why hidden rather than disabled. Multiplayer reuses the same eye for the commanders / scout panel
   (standingsBehindEye). `.hidden` is `display:none !important` globally, so this needs no help. */

/* ── COMMANDERS (#standings) LEFT OF SPELLS (Josh 2026-08-07) ───────────────────────────────────────
 * Was the right utility rail (pilot: right:14px / top:232px), overlapping ITEMS + SQUADRON BANNERS.
 * Park it in the open strip immediately left of SPELLS (same 224px width). Cap height above Sky Bazaar
 * so ROLL stays clear. Leave #ascentPanel alone — that one is the eye popup, not this rail.
 *
 * TOP ALIGNMENT: #standings is a STAGE sibling of #prep, while SPELLS' `top: 1.0%` is of #prepBody
 * under the 72px header (Dave sketch). Using bare `top: 1%` put COMMANDERS through the relic rack —
 * offset by that header so the two tops share a line (Josh 2026-08-07). */
body:has(#prep:not(.hidden)) #standings {
  left: calc(71.7% - 1cqh - 224px) !important;
  right: auto !important;
  top: calc(72px + 1%) !important;
  width: 224px !important;
  max-height: calc((100% - 72px) * 0.544) !important;
  z-index: 40;
  pointer-events: auto;
  overflow-y: auto;
}
body:has(#prep:not(.hidden)):has(#chatMini:not(.hidden)) #standings {
  top: calc(72px + 1%) !important;
  max-height: calc((100% - 72px) * 0.544) !important;
}

/* ── THE SQUADRON BANNER CARD ────────────────────────────────────────────────────────────────────
 * Josh: "drop HP/ATK/DEF/RNG, move the unit-cap x/x to the left of the DISBAND button alongside the
 * imprint, centre the art in the portrait cut-out."
 *
 * The statline is gone from renderArmy(), so the card is two rows instead of three. That was not only a
 * preference: measured on screen, the pilot's three rows needed ~145px of content inside a 116px
 * min-height, so the count and both buttons were centred inside collapsed rows and painted ON TOP of the
 * stats — "6/20" over "DEF", "HP 62 ATK 15" over "5/12". Removing the row Josh does not want is what
 * makes the remaining content fit its own card.
 *
 * WHY THIS RE-PLACES EVERY CHILD AND DOES NOT JUST MOVE ONE.
 * The pilot addresses these children POSITIONALLY, by child index — the two counters at indexes three and
 * four, the two buttons at five and six. (Written out in words on purpose: preplayouttest.js greps this
 * whole file, comments included, for a positional selector it requires to be absent from the Bazaar rules,
 * and a comment quoting one reads to it exactly like a live declaration. HANDOFF_2026-08-01 §3 warns about
 * this and I still tripped it once.)
 * That indexing was already wrong before today: a `nogear` unit renders no gear counter, so DISBAND became
 * the fourth child and inherited the counter's cell. Changing the markup at all means those four rules
 * land on the wrong elements, so the fix is to stop counting children — every child is placed here by its
 * ROLE (`.cntcap`, `.cntgear`, `[data-d]`, `[data-x]`), which is stable under any future reordering or
 * omission.
 *
 * SPECIFICITY, per the trap in HANDOFF_2026-08-01 §2.1. The pilot's selector is
 * `body:has(#prep:not(.hidden)) #armySum > .abat > .cnt:nth-child(3)` — and `:has()` takes the
 * specificity of its most specific argument, so that is TWO ids. Loading later is not enough; ids compare
 * first. `#prepBody` is added to the chain to make it three. Verified by reading the computed grid-area
 * off the live element, not by assuming.
 *
 * LAYOUT (Dave 2026-08-02 polish): portrait | growing mid | action/cap column.
 *  1) Portrait vertically centred in the banner (overrides pilot `align-self: start`).
 *  2) Disband/Exile (`.bact`): same height as Bazaar Recruit; width ~56% of Recruit each
 *     (Recruit ≈207px on 1920 sketch 4-up → act buttons 34×116; was 155 under banact1, then −25%).
 *     Dark chrome, not purple.
 *  3) Stack size (`.cntcap`) framed badge at the banner's TOP-RIGHT.
 *  4) Items + X/3 (`.bgearline`) single line at the portrait's BOTTOM-RIGHT (row 2, mid track).
 * Race plate (`.abg`) stays full-bleed under the grid; children keep role placement. */
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat {
  /* Portrait + flex mid + right column (framed cap / Sell+Exile).
     No overflow:hidden — imprint badge hangs past the portrait rim (see .ic below). */
  grid-template-columns: 72px minmax(0, 1fr) auto;
  grid-template-rows: auto auto;
  align-items: center;
}
/* Full-bleed race art under every filled banner's content. */
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .abg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 30%;
  opacity: .38;
  z-index: 0;
  pointer-events: none;
  border-radius: inherit;
}
/* Scrim ABOVE the plate (z=1) so name / pills / buttons stay readable. */
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat.has-abg::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  border-radius: inherit;
  background:
    linear-gradient(105deg, rgba(14, 14, 16, .78) 0%, rgba(14, 14, 16, .52) 48%, rgba(14, 14, 16, .8) 100%),
    linear-gradient(180deg, rgba(14, 14, 16, .2), rgba(14, 14, 16, .55));
}
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .ic,
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .amid,
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .cnt,
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .bact,
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .bgearline {
  z-index: 2;
}
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .ic {
  grid-column: 1;
  grid-row: 1 / 3;
  width: 72px;
  min-width: 72px;
  height: 84px;
  /* Vertically centre the portrait cut-out in the banner (pilot sets start). */
  align-self: center;
}
/* Name + pills: mid track on row 1. Padding clears the framed stack badge in col 3. */
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .amid {
  grid-column: 2 / 3;
  grid-row: 1;
  align-self: start;
  padding-right: 6px;
  min-width: 0;
}
body:has(#prep:not(.hidden)) #prepBody #armySum .abat .an {
  font-size: 14px;
  font-weight: 700;
  line-height: 1.2;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .9);
}
/* Meta pills (T1 / Light / race / class) — left-to-right row (wrap only if mid track is tight). */
body:has(#prep:not(.hidden)) #prepBody #armySum .abat .as {
  font-size: 12px;
}
body:has(#prep:not(.hidden)) #prepBody #armySum .abat .pillrow {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
  margin: 0 0 3px;
}
body:has(#prep:not(.hidden)) #prepBody #armySum .abat .mpill {
  flex: 0 0 auto;
  min-height: 20px;
  padding: 2px 6px;
  font-size: 10px;
  line-height: 14px;
  background: rgba(14, 14, 16, .72);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .75);
}
/* Stack size: framed badge, top-right of the banner. */
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .cntcap {
  grid-column: 3;
  grid-row: 1;
  justify-self: end;
  align-self: start;
  margin: 0;
  padding: 3px 9px;
  color: var(--ss-gold-light, var(--gold));
  font-size: 13px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  line-height: 1.2;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .85);
  border: 1px solid rgba(200, 163, 90, .58);
  border-radius: 6px;
  background: rgba(14, 14, 16, .82);
  box-shadow:
    inset 0 0 0 1px rgba(241, 230, 200, .06),
    0 1px 4px rgba(0, 0, 0, .45);
}
/* Items + X/3: one horizontal line, bottom-right of the portrait (mid track, row 2). */
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .bgearline {
  grid-column: 2;
  grid-row: 2;
  justify-self: start;
  align-self: center;
  display: inline-flex;
  flex-direction: row;
  flex-wrap: nowrap;
  align-items: center;
  gap: 5px;
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
}
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .bgearline > .bgear {
  position: static;
  top: auto;
  right: auto;
  display: inline-flex;
  flex-direction: row;
  flex-wrap: nowrap;
  align-items: center;
  flex: 0 0 auto;
}
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .bgearline > .cntgear {
  flex: 0 0 auto;
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .85);
}
/* Right-edge Disband. Exile moved to Bazaar troop cards (shopexile1).
   Banner acts stay 34px, same as Bazaar Acquire/Recruit/Exile (buyh34). */
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .bact {
  --ss-banner-act-h: 34px;
  --ss-banner-act-w: 116px;  /* ≈ 0.56 × Recruit (~207px); 155×0.75 after banact1 */
  grid-column: 3;
  grid-row: 2;
  justify-self: end;
  align-self: center;
  display: flex;
  flex-direction: row;
  align-items: stretch;
  gap: 6px;
  width: var(--ss-banner-act-w);
  min-width: 0;
}
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .bact > .disb {
  flex: 0 0 var(--ss-banner-act-w);
  width: var(--ss-banner-act-w);
  min-width: var(--ss-banner-act-w);
  max-width: var(--ss-banner-act-w);
  min-height: var(--ss-banner-act-h);
  height: var(--ss-banner-act-h);
  padding: 0 6px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .02em;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* EXILE is the irreversible banner action, so it uses the shared danger material.
   Keep DISBAND on the neutral stone treatment beside it for a clear risk contrast. */
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .bact > .ss-banner-exile {
  color: #e8b1a7 !important;
  border-color: rgba(160, 69, 61, .82) !important;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, .03), transparent 40%),
    linear-gradient(180deg, rgba(77, 35, 37, .96), rgba(38, 20, 24, .99)) !important;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .045),
    0 4px 12px rgba(0, 0, 0, .3) !important;
  text-shadow: none;
}
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .bact > .ss-banner-exile:hover:not(:disabled) {
  color: #ffd4cc !important;
  border-color: var(--ss-danger, #d66a61) !important;
  filter: brightness(1.1);
}
/* "Centre the art in the portrait cut-out." Sizing the image to the cut-out and letting `object-fit:
 * contain` do the fitting centres it by construction rather than by luck: `max-width`/`max-height` alone
 * let each plate settle wherever its own aspect ratio put it, so different units sat differently.
 *
 * Imprint badge (Dave 2026-08-02): take `.impbadge` out of flow — absolute bottom-right on the ellipse,
 * matching `#placeMine` / `#placeFoe` in ui-theme-baseline. Previously it was a second grid row under
 * the art (`70px 30px`), so the icon hung below the cut-out and the art was centred in row 1 only. */
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .ic {
  position: relative;
  place-items: center;
  /* Keep the badge visible as it sits on the ellipse rim (pilot already used overflow:visible). */
  overflow: visible;
}
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .ic .artic {
  width: 66px;
  height: 80px;
  max-width: 66px;
  max-height: 80px;
  object-fit: contain;
  object-position: center;
  /* spearart2: never inherit battle artW CSS scale — circle frame + overflow:visible for imprint */
  transform: none !important;
}
/* Militia: long spear/shield pad left-weights contain-fit and leaves him small in the
   circle. Match Bannerman: cover the ellipse, crop the spear tip, weight on head near
   the top-middle of the plate (milban4 object-position 44%/15%; milban5 pixel nudge
   left 5 / up 10 on top of that crop — milban3 56%/22%; milban2 56%/40%; milban1
   contain+68%/46%). */
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .ic[data-u="militia"] .artic {
  width: 70px;
  height: 84px;
  max-width: 70px;
  max-height: 84px;
  object-fit: cover;
  object-position: 44% 15%;
  transform: translate(-5px, -10px) !important;
}
/* Bannerman: tall flagpole makes contain shrink the human body — cover + body-weighted crop
   (clip some finial) so the figure reads human-sized next to other troops. Battle artW untouched. */
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .ic[data-u="banner"] .artic {
  width: 70px;
  height: 84px;
  max-width: 70px;
  max-height: 84px;
  object-fit: cover;
  object-position: center 64%;
  transform: none !important;
}
/* Peasants + Shield-Bearers fill their plates denser than most units — a tad smaller in the rail. */
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .ic[data-u="peasant"] .artic,
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .ic[data-u="defender"] .artic {
  width: 56px;
  height: 68px;
  max-width: 56px;
  max-height: 68px;
}
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .ic .impbadge {
  position: absolute;
  right: -3px;
  bottom: -3px;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 0;
  pointer-events: none;
}
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat > .ic .impbadge .impart {
  display: block;
  height: 30px;
  width: 30px;
  max-width: 34px;
  object-fit: contain;
}

/* The cell is the hover target, so it is the full track and not just the glyph.
 * Grid item — do not use flex-shrink (that was the microscopic single-row failure mode).
 * font-size tracks the cell so emoji fallbacks (and any non-.artic glyph) shrink with the rack;
 * overflow clips plates that would otherwise paint past a shrunk cell (Josh: manaacc).
 * 1px padding + border-box: edge-flush tall art (Hold the Line anchor flukes) needs a hair of
 * inset so overflow:hidden + subpixel rounding cannot chop the silhouette while the rack steps
 * 40→28→16. Name stays tooltip-only — never paint labels into the cell. */
body:has(#prep:not(.hidden)) #prepHead #relicRail .rrelic {
  display: grid;
  place-items: center;
  box-sizing: border-box;
  width: var(--sky-relic-cell);
  height: var(--sky-relic-cell);
  min-width: var(--sky-relic-cell);
  min-height: var(--sky-relic-cell);
  max-width: var(--sky-relic-cell);
  max-height: var(--sky-relic-cell);
  padding: 1px;
  flex: none;
  overflow: hidden;
  font-size: calc(var(--sky-relic-cell) * 0.72);
  line-height: 1;
  cursor: help;
}

/* `.artic` is sized for running text (`height: 1.35em`, `vertical-align: -.28em`) because that is where
 * it usually appears. In a fixed cell both of those are wrong: the em size makes the icon track the
 * header's font and the baseline shift pushes it out of its own cell. object-fit keeps the plates that
 * are not square from stretching. max-* + !important beat the global `.artic{height:1.35em}` so
 * manaacc / holdline / any leftover plate cannot stay fat or clip while the rack steps down.
 * `> img` covers a missing .artic class; filter:none stops global drop-shadow painting past the
 * clip (anchor flukes looked chopped). */
body:has(#prep:not(.hidden)) #prepHead #relicRail .rrelic .artic,
body:has(#prep:not(.hidden)) #prepHead #relicRail .rrelic > img {
  display: block !important;
  box-sizing: border-box;
  width: 100% !important;
  height: 100% !important;
  max-width: 100% !important;
  max-height: 100% !important;
  min-width: 0 !important;
  min-height: 0 !important;
  object-fit: contain !important;
  object-position: center !important;
  vertical-align: baseline;
  filter: none;
}

/* 🏺 Dev + add-relic — last cell on the rail (relicrail8). Same track as .rrelic; packed via dataset.n.
   Solid gold frame so the glyph reads on the dark header strip (relicrail7 dashed border was too faint). */
body:has(#prep:not(.hidden)) #prepHead #relicRail .rrelic-add {
  cursor: pointer;
  box-sizing: border-box;
  width: var(--sky-relic-cell);
  height: var(--sky-relic-cell);
  min-width: var(--sky-relic-cell);
  min-height: var(--sky-relic-cell);
  border: 2px solid var(--ss-border-gold, #c8a35a);
  border-radius: 6px;
  background: rgba(28, 22, 58, 0.94);
  color: var(--ss-gold-light, var(--gold));
  font-family: 'Cinzel', serif;
  font-size: calc(var(--sky-relic-cell) * 0.62);
  font-weight: 700;
  line-height: 1;
  padding: 0;
  margin: 0;
  appearance: none;
  -webkit-appearance: none;
  box-shadow: 0 0 8px rgba(200, 163, 90, 0.35);
  z-index: 1;
}
body:has(#prep:not(.hidden)) #prepHead #relicRail .rrelic-add:hover {
  border-color: #ffe6a8;
  color: #ffe6a8;
  background: rgba(42, 36, 82, 0.98);
  box-shadow: 0 0 12px rgba(232, 182, 76, 0.45);
}

/* ── SHOP CARD COLOUR = RARITY ONLY (Josh 2026-08-01) ─────────────────────────────────────────────
   Race artwork is restored as a faint, non-colour identity layer. The three gradients above it and
   every accent variable remain mapped to rarity, so race never changes the card's tier language. */
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.ss-unit-card {
  background-image:
    linear-gradient(180deg, rgba(14, 14, 16, .3) 0%, rgba(14, 14, 16, .48) 29%, rgba(14, 14, 16, .92) 61%),
    radial-gradient(circle at 50% 18%, rgba(var(--ss-race-rgb), .45), transparent 50%),
    linear-gradient(135deg, rgba(var(--ss-race-rgb), .28), rgba(var(--ss-race-rgb), .08) 54%, rgba(14, 14, 16, .34)),
    var(--ss-race-image, none),
    linear-gradient(180deg, #29283a, #111116) !important;
  background-size: cover, cover, cover, cover, cover !important;
  background-position: center, center, center, center, center !important;
  background-repeat: no-repeat !important;
  border-color: var(--ss-race-accent);
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.t0 {
  --ss-race-accent: var(--r0);
  --ss-race-rgb: 154, 143, 192;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.t1 {
  --ss-race-accent: var(--r1);
  --ss-race-rgb: 88, 214, 139;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.t2 {
  --ss-race-accent: var(--r2);
  --ss-race-rgb: 176, 127, 232;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.t3 {
  --ss-race-accent: var(--r3);
  --ss-race-rgb: 232, 182, 76;
}
/* Beat the pilot's per-race selectors (same chain + attribute would tie; #prepBody wins). */
body:has(#prep:not(.hidden)) #prepBody #shopCards .card[data-race="Human"],
body:has(#prep:not(.hidden)) #prepBody #shopCards .card[data-race="Elf"],
body:has(#prep:not(.hidden)) #prepBody #shopCards .card[data-race="Goblin"],
body:has(#prep:not(.hidden)) #prepBody #shopCards .card[data-race="Dwarf"],
body:has(#prep:not(.hidden)) #prepBody #shopCards .card[data-race="Troll"],
body:has(#prep:not(.hidden)) #prepBody #shopCards .card[data-race="Ogre"],
body:has(#prep:not(.hidden)) #prepBody #shopCards .card[data-race="Mystic"],
body:has(#prep:not(.hidden)) #prepBody #shopCards .card[data-race="Animal"] {
  --ss-race-accent: inherit;
  --ss-race-rgb: inherit;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.t0[data-race] {
  --ss-race-accent: var(--r0);
  --ss-race-rgb: 154, 143, 192;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.t1[data-race] {
  --ss-race-accent: var(--r1);
  --ss-race-rgb: 88, 214, 139;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.t2[data-race] {
  --ss-race-accent: var(--r2);
  --ss-race-rgb: 176, 127, 232;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.t3[data-race] {
  --ss-race-accent: var(--r3);
  --ss-race-rgb: 232, 182, 76;
}
/* Troop tip hit-plate (Josh 2026-08-03): matches the red-box portrait band — art + corner diamond
   pips — not name / pills / stats / Recruit. `data-tth` lives on this node from renderShop.
   Height matches the pilot `.top` art row (110px). Above cardArtBg (pointer-events:none) so hover
   reaches the plate; stops before `.cmeta` name/tags. */
body:has(#prep:not(.hidden)) #prepBody #shopCards .ss-pilot-unit-card > .ss-unit-art {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 110px;
  z-index: 3;
  pointer-events: auto;
}
/* Item tip hit-plate (Josh 2026-08-03): same `.ss-unit-art` treatment, top half only (~50%) so
   Acquire / lower chrome do not `closest` to the tip. `data-tt` on the plate from renderShop. */
body:has(#prep:not(.hidden)) #prepBody #shopCards .ss-pilot-item-card > .ss-unit-art {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 50%;
  z-index: 3;
  pointer-events: auto;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .ss-pilot-unit-card .cic {
  pointer-events: none; /* invisible remnant must not steal or fake tip hits */
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .ss-pilot-item-card .cic {
  pointer-events: none; /* tip lives on the top-half plate, not the (often hidden) circle */
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .card .cardArtBg {
  filter:
    /* 25% quieter rarity halo (was 12px / .22). */
    drop-shadow(0 0 9px rgba(var(--ss-race-rgb, 154, 143, 192), .165))
    drop-shadow(0 10px 11px rgba(0, 0, 0, .72));
}
/* spearart2: pilot earlier sets `transform:none` on cardArtBg; inline scale from cardArtBgHtml
   beats that. Keep origin planted so moderated artW enlarges the body inside the 108px band. */
body:has(#prep:not(.hidden)) #prepBody #shopCards .card .cardArtBg[style*="transform:scale"] {
  transform-origin: center 80%;
}
/* Bannerman bazaar face (bannermanart3): body-weighted PNG already fills like peers — drop
   banzoom1's scale(1.52) which overgrew the card after the re-crop. Prefer CSS over artW. */
body:has(#prep:not(.hidden)) #prepBody #shopCards .card[data-unit="banner"] .cardArtBg,
body:has(#prep:not(.hidden)) #prepBody #shopCards .card .cardArtBg[data-u="banner"] {
  transform: none;
  object-position: center bottom;
}

/* SKY BAZAAR card glow — 25% less intense, pulse period ×2 (was auraPulse 1.5–1.7s).
   Scoped to #shopCards so army banners / other .aura-* surfaces keep the global beat. */
@keyframes ssBazaarAuraPulse {
  0%, 100% {
    box-shadow:
      0 0 4.5px 0.75px var(--auraA),
      0 0 9px 2.25px var(--auraB);
  }
  50% {
    box-shadow:
      0 0 11.25px 3px var(--auraA),
      0 0 22.5px 7.5px var(--auraB);
  }
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.aura-elite {
  animation: ssBazaarAuraPulse 3.4s ease-in-out infinite;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.aura-heroic {
  animation: ssBazaarAuraPulse 3s ease-in-out infinite;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.aura-emperor {
  animation: ssBazaarAuraPulse 3.2s ease-in-out infinite;
}
/* Hover race bloom — 25% quieter (was 18px / .2). Keeps pilot inset chrome. */
body:has(#prep:not(.hidden)) #prepBody #shopCards .card.ss-pilot-unit-card:hover {
  box-shadow:
    inset 0 0 0 2px rgba(14, 14, 16, .72),
    inset 0 0 0 3px rgba(var(--ss-race-rgb), .22),
    0 0 13.5px rgba(var(--ss-race-rgb), .15),
    0 8px 22px rgba(0, 0, 0, .42);
}
@media (prefers-reduced-motion: reduce) {
  body:has(#prep:not(.hidden)) #prepBody #shopCards .card.aura-elite,
  body:has(#prep:not(.hidden)) #prepBody #shopCards .card.aura-heroic,
  body:has(#prep:not(.hidden)) #prepBody #shopCards .card.aura-emperor {
    animation: none;
    box-shadow: 0 0 4.5px 0.75px var(--auraA), 0 0 9px 2.25px var(--auraB);
  }
}

/* Race pills: neutral meta ink only. Do NOT use rarity-remapped --ss-race-accent /
   --ss-race-rgb, and do not reintroduce per-race pill tints. */
body:has(#prep:not(.hidden)) #prepBody #shopCards .card .ss-race-pill {
  color: #bcb4c3;
  border-color: currentColor;
  background: rgba(14, 14, 16, .52);
  box-shadow: none;
}

/* Tier III / epic rarity bar = purple (--r2), not the old blue. */
body:has(#prep:not(.hidden)) #prepBody #shopCards .card[data-tier="3"] {
  --ss-tier-accent: var(--ss-rarity-rare, #b07fe8);
}
body:has(#prep:not(.hidden)) #prepBody #shopCards .card[data-tier="4"] {
  --ss-tier-accent: var(--ss-rarity-legendary, #e8b64c);
}

/* ── DAVE SKETCH LAYOUT (2026-08-01) ─────────────────────────────────────────────────────────────
   Absolute placement from the prep-layout-sketch numbers (1920×1080, body under the 72px header).
   Percentages are of #prepBody. ITEMS is a new panel; recruits stay in the Bazaar, non-recruit
   shop cards render into #itemCards. */
body:has(#prep:not(.hidden)) #prepBody {
  position: relative !important;
  display: block !important;
  grid-template-columns: none !important;
  grid-template-rows: none !important;
  grid-template-areas: none !important;
  gap: 0 !important;
  overflow: hidden !important;
  /* Size container so SPELLS↔ITEMS can use 1cqh (= 1% of body height) and match the Y gaps in px. */
  container-type: size;
}
body:has(#prep:not(.hidden)) #prepBody > .pgut { display: none !important; }

body:has(#prep:not(.hidden)) #prepBody > #townPanel,
body:has(#prep:not(.hidden)) #prepBody > #shopPanel,
body:has(#prep:not(.hidden)) #prepBody > #spellPanel,
body:has(#prep:not(.hidden)) #prepBody > #itemsPanel,
body:has(#prep:not(.hidden)) #prepBody > #armyPanel {
  position: absolute !important;
  grid-area: unset !important;
  margin: 0 !important;
  max-width: none !important;
  max-height: none !important;
  min-width: 0 !important;
  min-height: 0 !important;
  width: auto;
  height: auto;
  box-sizing: border-box;
  overflow: hidden;
}

/* YOUR TOWN — left rail. Top matches SPELLS/ITEMS (1.0%); bottom matches
   SKY BAZAAR + SQUADRON BANNERS (both end at 99.4%: 55.4+44 / 32+67.4). */
body:has(#prep:not(.hidden)) #prepBody > #townPanel {
  left: 0.8%;
  top: 1.0%;
  width: 16.1%;
  height: 98.4%;
  justify-self: unset !important;
  align-self: unset !important;
  display: flex !important;
  flex-direction: column !important;
  overflow: hidden !important;
}
body:has(#prep:not(.hidden)) #prepBody > #townPanel > h2 {
  flex: 0 0 auto;
  margin-bottom: 6px;
}
body:has(#prep:not(.hidden)) #prepBody > #townPanel .townMain {
  flex: 1 1 auto !important;
  min-height: 0 !important;
  display: flex !important;
  flex-direction: column !important;
  overflow: hidden !important;
}
body:has(#prep:not(.hidden)) #prepBody > #townPanel #bldList {
  flex: 1 1 auto;
  min-height: 0;
  height: 100%;
  /* 1×10 column — pilot still says repeat(2); sketch wants a single stack. */
  display: grid !important;
  grid-template-columns: minmax(0, 1fr) !important;
  grid-auto-rows: minmax(0, 1fr);
  align-content: stretch;
  gap: 5px;
}
/* Card internals rescaled for the 1×10 column (Dave 2026-08-01): the pilot styled these for a 2×5
   grid of 66px cards, so a ~90px row left dead air above and below. Sizes track the neighbours —
   48px icon circle sits between the 40px item icons and the 58px banner portraits, the 13px name
   matches the pilot's own building name size, and the button matches the banner rail's 34px actions. */
body:has(#prep:not(.hidden)) #prepBody > #townPanel #bldList > .bld,
body:has(#prep:not(.hidden)) #prepBody > #townPanel #bldList > .bld.ss-building-card {
  min-height: 0 !important;
  height: 100%;
  align-content: center;
  align-items: center;
  grid-template-columns: 52px minmax(0, 1fr) 86px;
  gap: 8px;
  padding: 6px 10px;
  box-sizing: border-box;
}
body:has(#prep:not(.hidden)) #prepBody > #townPanel #bldList .bld .ic,
body:has(#prep:not(.hidden)) #prepBody > #townPanel #bldList .ss-building-icon {
  width: 48px;
  height: 48px;
}
body:has(#prep:not(.hidden)) #prepBody > #townPanel #bldList .bld .ic .artic,
body:has(#prep:not(.hidden)) #prepBody > #townPanel #bldList .ss-building-icon .artic {
  max-width: 44px;
  max-height: 44px;
}
body:has(#prep:not(.hidden)) #prepBody > #townPanel #bldList .bld .bn {
  font-size: 13px;
  line-height: 1.2;
}
body:has(#prep:not(.hidden)) #prepBody > #townPanel #bldList .bld .pips {
  gap: 3px;
  margin-top: 6px;
}
body:has(#prep:not(.hidden)) #prepBody > #townPanel #bldList .bld .pips i,
body:has(#prep:not(.hidden)) #prepBody > #townPanel #bldList .bld .bn .pips i {
  width: 9px;
  height: 6px;
  border-radius: 1px;
}
body:has(#prep:not(.hidden)) #prepBody > #townPanel #bldList .bld .ubtn {
  width: 100%;
  min-width: 80px;
  max-width: 86px;
  min-height: 36px;
  padding: 5px 8px;
  font-size: 11px;
  line-height: 1.15;
}
body:has(#prep:not(.hidden)) #prepBody > #townPanel #bldList .bld .ubtn:disabled {
  font-size: 9px;
}
/* Prison occupancy plate — same footprint as town purchase `.ubtn` (Dave 2026-08-02).
   Quiet status chrome (not magic-purple buy); count/label sized to the 86×36 action slot. */
body:has(#prep:not(.hidden)) #prepBody > #townPanel #bldList .ss-prison-info {
  box-sizing: border-box;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  justify-self: end;
  width: 100%;
  min-width: 80px;
  max-width: 86px;
  min-height: 36px;
  padding: 5px 8px;
  border: 1px solid rgba(142, 120, 88, .68);
  border-radius: 4px;
  color: var(--ss-text-on-dark, #f1e6c8);
  background:
    linear-gradient(180deg, rgba(255, 255, 255, .045), transparent 42%),
    linear-gradient(180deg, #29282c, #17171b);
  box-shadow: inset 0 0 0 1px rgba(241, 230, 200, .025);
  font-family: var(--ss-font-ui, "Source Sans 3", "Segoe UI", system-ui, sans-serif);
  text-transform: uppercase;
  letter-spacing: .045em;
  line-height: 1.1;
  text-align: center;
  white-space: nowrap;
}
body:has(#prep:not(.hidden)) #prepBody > #townPanel #bldList .ss-prison-info__n {
  font-size: 11px;
  font-weight: 700;
  line-height: 1.15;
  color: var(--ss-text-on-dark, #f1e6c8);
}
body:has(#prep:not(.hidden)) #prepBody > #townPanel #bldList .ss-prison-info__lbl {
  font-size: 8px;
  font-weight: 600;
  letter-spacing: .08em;
  color: var(--ink-dim, #9a8f7a);
}
/* Pocket Stock Market desk — vault art from art-cleanup/pcketstckmketbg.png → assets/ui/mktbg.png.
 * Scrim keeps STOCK MARKET / invested / buttons readable over the gold pile. */
body:has(#prep:not(.hidden)) #prepBody > #townPanel #marketBldg {
  flex: 0 0 auto;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background-color: #121018;
  background-image:
    linear-gradient(180deg, rgba(8, 8, 12, .78) 0%, rgba(10, 10, 16, .82) 55%, rgba(8, 8, 12, .88) 100%),
    url('assets/ui/mktbg.png?v=mktbg1');
  background-size: cover, cover;
  background-position: center 42%, center 42%;
  background-repeat: no-repeat, no-repeat;
  color: var(--ss-text-on-dark, #f1e6c8);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .8);
}
body:has(#prep:not(.hidden)) #prepBody > #townPanel #marketBldg #mktTxt {
  font-weight: 600;
  color: var(--ss-gold-light, var(--gold, #e8b64c));
}
/* Odds moved into Sky Bazaar top-left. */
/* SPELLS — left-aligned with SQUADRON BANNERS; top matches YOUR TOWN (1.0% under toolbar).
   Height leaves the same 1.0% body-height gap below to SQUADRON BANNERS.
   Width shares the banners column with ITEMS; X gap is 1cqh so it matches the Y gaps in pixels
   (plain 1% width ≠ 1% height on a wide stage). */
body:has(#prep:not(.hidden)) #prepBody > #spellPanel {
  left: 71.7%;
  top: 1.0%;
  /* Pair with ITEMS: equal 50/50 split of the spells+items band (pre-itemfan6). */
  width: calc((27.7% - 1cqh) / 2);
  height: 30.0%;
  display: flex;
  flex-direction: column;
  padding: 8px 8px 6px;
  overflow: hidden;
}
body:has(#prep:not(.hidden)) #prepBody > #spellPanel > h2 {
  flex: 0 0 auto;
  margin: 0 0 6px;
  width: 100%;
  box-sizing: border-box;
}
/* Spell power opposite the SPELLS title (Josh 2026-08-03) — gold/void chrome, not a pill. */
body:has(#prep:not(.hidden)) #prepBody > #spellPanel > h2 .spellPowMeta {
  margin-left: auto;
  flex: 0 0 auto;
  padding-right: 2px;
  color: var(--ss-gold-light, var(--gold, #e8b64c));
  font-family: var(--ss-font-ui, "Source Sans 3", "Segoe UI", system-ui, sans-serif);
  /* Josh 2026-08-03: +25% vs prior 11px (spellsz1). */
  font-size: 13.75px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: none;
  white-space: nowrap;
  opacity: .95;
}
body:has(#prep:not(.hidden)) #prepBody > #spellPanel #spellSum {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
body:has(#prep:not(.hidden)) #prepBody > #spellPanel .spellGrid {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  height: 100%;
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  /* Slightly tighter so +25% art/type still fits the 30% panel. */
  gap: 4px;
}
body:has(#prep:not(.hidden)) #prepBody > #spellPanel .spellSlot {
  position: relative;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 3px 2px 2px;
  box-sizing: border-box;
  border-radius: 8px;
  border: 1px solid rgba(200, 163, 90, .35);
  background:
    linear-gradient(180deg, rgba(255, 255, 255, .05), transparent 45%),
    linear-gradient(180deg, #24222c, #141418);
  overflow: hidden;
}
body:has(#prep:not(.hidden)) #prepBody > #spellPanel .spellSlot.empty {
  border-style: dashed;
  border-color: rgba(200, 163, 90, .22);
  background: rgba(20, 18, 24, .45);
  opacity: .7;
}
body:has(#prep:not(.hidden)) #prepBody > #spellPanel .spellSlotArt {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  display: grid;
  place-items: center;
  overflow: hidden;
  /* Air from Q / name / slot border — Y is the tight edge.
     Extra bottom pad lifts the plate so it reads centered above the label (spelliso4). */
  padding: 3px 10px 10px;
  box-sizing: border-box;
}
body:has(#prep:not(.hidden)) #prepBody > #spellPanel .spellSlotArt .artic,
body:has(#prep:not(.hidden)) #prepBody > #spellPanel .spellSlotArt .iart,
body:has(#prep:not(.hidden)) #prepBody > #spellPanel .spellSlotArt img {
  /* Height-constrained: Y is the limit; width follows aspect (side gaps OK). */
  width: auto;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  object-position: center;
  display: block;
}
body:has(#prep:not(.hidden)) #prepBody > #spellPanel .spellSlotName {
  flex: 0 0 auto;
  width: 100%;
  text-align: center;
  /* Josh 2026-08-03: name +25% (9→11.25px). */
  font-size: 11.25px;
  line-height: 1.15;
  letter-spacing: .02em;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  padding: 0 1px;
}
body:has(#prep:not(.hidden)) #prepBody > #spellPanel .spellSlot.empty .spellSlotName {
  color: var(--ink-dim);
}
body:has(#prep:not(.hidden)) #prepBody > #spellPanel .spellSlotKey {
  position: absolute;
  top: 3px;
  left: 4px;
  /* Josh 2026-08-03: hotkey +25% (9→11.25px). */
  font-size: 11.25px;
  font-weight: 700;
  color: var(--arcane);
  line-height: 1;
  opacity: .9;
}
/* 🔀 Prep spell / banner drag-reorder (slotdrag1 / slotdrag2) */
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat.slotDragAble,
body:has(#prep:not(.hidden)) #prepBody > #spellPanel .spellSlot.slotDragAble {
  cursor: grab;
}
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat.slotDragOver,
body:has(#prep:not(.hidden)) #prepBody > #spellPanel .spellSlot.slotDragOver {
  outline: 2px solid var(--arcane);
  outline-offset: 1px;
  box-shadow: 0 0 12px rgba(126, 224, 255, .35);
}
body:has(#prep:not(.hidden)) #prepBody #armySum > .abat.slotDragSrc,
body:has(#prep:not(.hidden)) #prepBody > #spellPanel .spellSlot.slotDragSrc {
  opacity: .45;
}
/* Banner drag ghost is the portrait circle only — styles in skysunder.css
   (.slotDragGhostPortrait). Clone is appended to body, outside #armySum. */
/* ITEMS — right edge on SQUADRON BANNERS; same vertical band as SPELLS; X gap = 1cqh (= Y gaps).
   Equal 50/50 split with SPELLS (restored pre-itemfan6). Icon sz scales inside this box. */
body:has(#prep:not(.hidden)) #prepBody > #itemsPanel {
  left: calc(71.7% + (27.7% - 1cqh) / 2 + 1cqh);
  top: 1.0%;
  width: calc((27.7% - 1cqh) / 2);
  height: 30.0%;
  display: flex;
  flex-direction: column;
  padding: 8px 8px 6px;
  overflow: hidden;
  /* Above spell/army neighbors so town bars / adjacent chrome cannot paint through stacks. */
  z-index: 4;
}
body:has(#prep:not(.hidden)) #prepBody > #itemsPanel > h2 {
  flex: 0 0 auto;
  margin: 0 0 6px;
}
body:has(#prep:not(.hidden)) #prepBody #itemCards {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  position: relative;
  z-index: 1;
}
body:has(#prep:not(.hidden)) #prepBody #itemCards .itemIconGrid {
  --peek: 0.20;
  /* Air between stack boxes only — each stack owns its own fan width via --faces. */
  --gap-extra: 0.14;
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  height: 100%;
  /* Flex-wrap pack by each stack’s real footprint (1–3 faces). Never fixed fan gutters. */
  display: flex;
  flex-wrap: wrap;
  justify-content: start;
  align-content: start;
  align-items: start;
  column-gap: calc(var(--item-sz, 71px) * var(--gap-extra));
  row-gap: calc(var(--item-sz, 71px) * 0.10);
  padding: 2px;
  box-sizing: border-box;
  overflow: visible;
  position: relative;
  z-index: 1;
}
/* Footprint width = sz × (1 + (faces-1)×peek): n=1 → sz; n=2 → 1.2×sz; n≥3 → 1.4×sz.
   Faces stay full --item-sz. Left origin; front rightmost. JS --faces = min(n, FAN_FACES). */
body:has(#prep:not(.hidden)) #prepBody #itemCards .itemStack {
  --peek: 0.20;
  --faces: 1;
  position: relative;
  display: block;
  flex: 0 0 auto;
  width: calc(var(--item-sz, 71px) * (1 + (var(--faces, 1) - 1) * var(--peek, 0.20)));
  height: var(--item-sz, 71px);
  max-height: var(--item-sz, 71px);
  min-width: 0;
  /* Fan peeks live inside width; no z-index so face z-index can beat neighbor peeks. */
  overflow: visible;
  cursor: default;
  box-sizing: border-box;
}
body:has(#prep:not(.hidden)) #prepBody #itemCards .itemStack.itemPad {
  visibility: hidden;
  pointer-events: none;
  border: 0;
  box-shadow: none;
  background: none;
  overflow: visible;
}
body:has(#prep:not(.hidden)) #prepBody #itemCards .itemIcon {
  position: absolute;
  /* Full cell face — stacks match singles; do not shrink by --faces.
     Pin to top:0 (no vertical centering transform): same-size absolute
     centering subpixel-rounds and clips the top under overflow:hidden. */
  width: var(--item-sz, 71px);
  height: var(--item-sz, 71px);
  top: 0;
  /* Left-origin fan: left = (faces-1 + i)×sz×peek.
     n=1 (--faces:1,--i:0) → left:0. n≥2 → backmost at 0, front rightmost. */
  left: calc((var(--faces, 1) - 1 + var(--i, 0)) * var(--item-sz, 71px) * var(--peek, 0.20));
  right: auto;
  transform: none;
  border-radius: 8px;
  box-sizing: border-box;
  border: 1px solid rgba(200, 163, 90, .45);
  background:
    linear-gradient(180deg, rgba(255, 255, 255, .06), transparent 50%),
    linear-gradient(180deg, #2a2932, #141418);
  box-shadow: 0 2px 6px rgba(0, 0, 0, .35);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
/* Count badge when a type stack is deeper than one (JS emits ×n for n>1). */
body:has(#prep:not(.hidden)) #prepBody #itemCards .itemCount {
  position: absolute;
  right: 2px;
  bottom: 1px;
  z-index: 20;
  min-width: 1.1em;
  padding: 0 3px;
  border-radius: 4px;
  background: rgba(10, 10, 12, .82);
  color: #f0e6c8;
  font: 700 11px/1.15 "Segoe UI", system-ui, sans-serif;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .85);
  pointer-events: none;
}
/* Rarity on the icon border only — no stack-level drop-shadow (avoids purple/black bar glitches). */
body:has(#prep:not(.hidden)) #prepBody #itemCards .itemIcon.ir1 {
  border-color: rgba(88, 214, 139, .7);
  box-shadow: 0 0 8px rgba(88, 214, 139, .28), 0 2px 6px rgba(0, 0, 0, .35);
}
body:has(#prep:not(.hidden)) #prepBody #itemCards .itemIcon.ir2 {
  border-color: rgba(176, 127, 232, .75);
  box-shadow: 0 0 9px rgba(176, 127, 232, .32), 0 2px 6px rgba(0, 0, 0, .35);
}
body:has(#prep:not(.hidden)) #prepBody #itemCards .itemIcon.ir3 {
  border-color: rgba(232, 182, 76, .8);
  box-shadow: 0 0 10px rgba(232, 182, 76, .38), 0 2px 6px rgba(0, 0, 0, .35);
}
body:has(#prep:not(.hidden)) #prepBody #itemCards .itemIcon .artic,
body:has(#prep:not(.hidden)) #prepBody #itemCards .itemIcon .iart,
body:has(#prep:not(.hidden)) #prepBody #itemCards .itemIcon img {
  width: 86%;
  height: 86%;
  object-fit: contain;
  display: block;
  /* Beat global .artic { vertical-align:-.28em } so the plate cannot shift up into the clip. */
  vertical-align: baseline;
}
/* SQUADRON BANNERS — right column under spells+items.
   Six slots must fit the panel: override the shared 118px lock and share height evenly so the last
   slot is not clipped by overflow:hidden. */
body:has(#prep:not(.hidden)) #prepBody > #armyPanel {
  left: 71.7%;
  top: 32.0%;
  width: 27.7%;
  height: 67.4%;
  display: flex;
  flex-direction: column;
  padding: 8px 10px 6px;
  overflow: hidden;
  box-sizing: border-box;
}
body:has(#prep:not(.hidden)) #prepBody > #armyPanel > h2 {
  flex: 0 0 auto;
  margin: 0 0 5px;
}
body:has(#prep:not(.hidden)) #prepBody > #armyPanel #armySum {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  overflow-x: hidden;
  overflow-y: auto;
  scrollbar-color: rgba(200, 163, 90, .48) rgba(14, 14, 16, .3);
  scrollbar-width: thin;
}
body:has(#prep:not(.hidden)) #prepBody > #armyPanel #armySum > .abat,
body:has(#prep:not(.hidden)) #prepBody > #armyPanel #armySum > .abat.empty,
body:has(#prep:not(.hidden)) #prepBody > #armyPanel #armySum > .abat.ss-banner-card,
body:has(#prep:not(.hidden)) #prepBody > #armyPanel #armySum > .abat.empty.ss-banner-card {
  flex: 1 1 0;
  height: auto !important;
  min-height: 84px !important;
  /* Ceiling as well as floor: Divine Selection can cut the rail to one banner, and an uncapped
     flex share would balloon that lone card to the full panel height. Room for 34px Disband/Exile. */
  max-height: 148px !important;
  padding: 5px 8px !important;
}
/* 🛡️ Gear icons inside `.bgearline` (portrait bottom-right). No longer absolute on the card —
   the line is a grid cell so it cannot collide with the framed top-right stack badge. */
body:has(#prep:not(.hidden)) #prepBody > #armyPanel #armySum > .abat {
  position: relative;
}
body:has(#prep:not(.hidden)) #prepBody > #armyPanel #armySum > .abat > .bgearline > .bgear > img,
body:has(#prep:not(.hidden)) #prepBody > #armyPanel #armySum > .abat > .bgear > img {
  width: 18px;
  height: 18px;
  object-fit: contain;
  display: block;
  border-radius: 5px;
  border: 1px solid rgba(200, 163, 90, .5);
  background: rgba(14, 14, 16, .82);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .55);
}
body:has(#prep:not(.hidden)) #prepBody > #armyPanel #armySum > .abat > .bgearline > .bgear > img + img,
body:has(#prep:not(.hidden)) #prepBody > #armyPanel #armySum > .abat > .bgear > img + img {
  margin-left: -6px;
}
/* SKY BAZAAR — bottom centre (recruits + roll/lock) */
body:has(#prep:not(.hidden)) #prepBody > #shopPanel {
  left: 17.4%;
  top: 55.4%;
  width: 53.9%;
  height: 44.0%;
  align-self: unset !important;
  /* Above YOUR TOWN / SPELLS so rarity glow spilling ±Y is not painted under them. */
  z-index: 3;
}
/* 🎁 MINIMIZED RELIC/BOON RESTORE (Josh 2026-08-04 / boonpill1). Was stage `right:18px; bottom:18px`
 * — under the banner rail. Pocket: above SKY BAZAAR ROLL, left of SQUADRON BANNERS. Same sketch
 * numbers as #shopPanel (top 55.4%) and #armyPanel (left 71.7% → right remainder 28.3%). Both pills
 * share this pocket for visual consistency. */
body:has(#prep:not(.hidden)) #prepBody > #relicPill,
body:has(#prep:not(.hidden)) #prepBody > #boonPill {
  /* !important on anchors — HTML still carries legacy right/bottom inline; without
     !important those win and park the pill under Squadron Banners (Josh 2026-08-07). */
  position: absolute !important;
  left: auto !important;
  right: calc(28.3% + 12px) !important;
  bottom: calc(44.6% + 12px) !important;
  top: auto !important;
  z-index: 29;
  margin: 0;
  white-space: nowrap;
  pointer-events: auto;
}
/* Header: odds | Sky Bazaar | ROLL+lock — one baseline row.
   overflow MUST stay visible: cards carry outer box-shadow / rarity glow (~14–18px). The shared
   panel rule sets overflow:hidden, which clipped that halo hardest on +Y/−Y (almost no vertical
   padding in the card row). Visible on both axes — mixing overflow-x:hidden with overflow-y:visible
   collapses to auto/hidden per CSS. */
body:has(#prep:not(.hidden)) #prepBody > #shopPanel {
  /* Symmetric chrome: pad above Roll (== panel top → Roll) equals row-gap
     below Roll → card row. Was padding 10 / row-gap 6 / #shopCards pad-top 14
     (20px to card tops). */
  --shop-roll-gap: 10px;
  display: grid !important;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) !important;
  grid-template-rows: auto minmax(0, 1fr) !important;
  grid-template-areas:
    "odds title right"
    "cards cards cards" !important;
  align-content: stretch;
  align-items: center;
  column-gap: 10px;
  row-gap: var(--shop-roll-gap);
  overflow: visible !important;
  padding: var(--shop-roll-gap) 12px var(--shop-roll-gap) !important;
}
body:has(#prep:not(.hidden)) #prepBody > #shopPanel > h2 {
  grid-area: title;
  margin: 0 !important;
  padding: 0 !important;
  min-height: 0 !important;
  border: 0 !important;
  align-self: center;
  justify-self: center;
  text-align: center;
  font-size: 16px;
  letter-spacing: .16em;
  line-height: 32px;
  height: 32px;
  white-space: nowrap;
}
body:has(#prep:not(.hidden)) #prepBody > #shopPanel > #shopHeadRight {
  grid-area: right;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  min-width: 0;
  justify-self: end;
  align-self: center;
  height: 32px;
}
body:has(#prep:not(.hidden)) #prepBody > #shopPanel > #oddsBox {
  grid-area: odds;
  flex: 0 0 auto !important;
  width: auto !important;
  margin: 0 !important;
  /* Nudge so the O of ODDS sits on the leftmost card's left edge — letter-spacing
     .16em + UI face side bearings read a hair left of the card border otherwise. */
  padding: 0 0 0 3px !important;
  border: 0 !important;
  text-align: left !important;
  white-space: nowrap;
  /* Scanned rarity % — UI face, not Cinzel (panel titles stay display). */
  font-family: var(--ss-font-ui, "Source Sans 3", "Segoe UI", system-ui, sans-serif);
  font-size: 12px;
  letter-spacing: .16em;
  line-height: 32px;
  height: 32px;
  color: var(--ink-dim);
  text-transform: uppercase;
  overflow: visible !important;
  align-self: center;
  justify-self: start;
}
body:has(#prep:not(.hidden)) #prepBody > #shopPanel > #oddsBox b {
  font-weight: 700;
  color: var(--gold);
  letter-spacing: .16em;
}
/* One yellow ROLL control — lock sits inside the same gold pill. */
body:has(#prep:not(.hidden)) #prepBody > #shopPanel > #shopHeadRight > #rollRow {
  display: inline-flex;
  align-items: stretch;
  gap: 0;
  margin: 0 !important;
  min-height: 32px;
  height: 32px;
  padding: 0;
  border: 2px solid #f4d681;
  border-radius: 9px;
  overflow: hidden;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, .42), transparent 28%),
    linear-gradient(180deg, #f5dfa0, #d2a74e 58%, #80571e);
  box-shadow:
    inset 0 0 0 2px rgba(72, 42, 12, .45),
    inset 0 -4px 8px rgba(62, 31, 6, .28),
    0 4px 12px rgba(0, 0, 0, .4);
}
body:has(#prep:not(.hidden)) #prepBody > #shopPanel > #shopHeadRight > #rollRow #rollBtn,
body:has(#prep:not(.hidden)) #prepBody > #shopPanel > #shopHeadRight > #rollRow #rollBtn.ss-button--primary {
  flex: 0 0 auto;
  min-height: 0 !important;
  height: 100%;
  min-width: 0;
  margin: 0 !important;
  padding: 0 14px !important;
  border: 0 !important;
  border-radius: 0 !important;
  background: transparent !important;
  box-shadow: none !important;
  color: #21170d !important;
  font-size: 12px !important;
  letter-spacing: .12em;
  line-height: 1;
}
body:has(#prep:not(.hidden)) #prepBody > #shopPanel > #shopHeadRight > #rollRow #rollBtn span {
  color: #4b2b0b !important;
  /* Match ROLL (button is 700); pilot had span at 900. */
  font-weight: inherit !important;
}
body:has(#prep:not(.hidden)) #prepBody > #shopPanel > #shopHeadRight > #rollRow #lockBtn {
  flex: 0 0 auto;
  width: 34px;
  min-width: 34px;
  min-height: 0 !important;
  height: 100%;
  margin: 0;
  padding: 0;
  border: 0 !important;
  border-left: 1px solid rgba(72, 42, 12, .38) !important;
  border-radius: 0 !important;
  background: rgba(72, 42, 12, .16) !important;
  box-shadow: none !important;
  color: #21170d !important;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
body:has(#prep:not(.hidden)) #prepBody > #shopPanel > #shopHeadRight > #rollRow #lockBtn:hover {
  background: rgba(72, 42, 12, .26) !important;
}
body:has(#prep:not(.hidden)) #prepBody > #shopPanel > #shopHeadRight > #rollRow #lockBtn.on {
  background: rgba(72, 42, 12, .34) !important;
  box-shadow: inset 0 0 0 1px rgba(72, 42, 12, .35) !important;
}
body:has(#prep:not(.hidden)) #prepBody > #shopPanel > #shopHeadRight > #rollRow #lockBtn img {
  height: 16px;
  width: auto;
  max-width: 22px;
  display: block;
  object-fit: contain;
  filter: brightness(0) saturate(100%);
  opacity: .78;
}
body:has(#prep:not(.hidden)) #prepBody > #shopPanel > #shopHeadRight > #rollRow #lockBtn.on img {
  opacity: 1;
}
/* Town no longer hosts odds — reclaim the space for the building list. */
body:has(#prep:not(.hidden)) #prepBody > #townPanel #oddsBox { display: none !important; }
body:has(#prep:not(.hidden)) #prepBody #shopCards {
  grid-area: cards;
  flex: unset;
  align-self: stretch !important;
  height: 100% !important;
  min-height: 0;
  /* Fixed N-column grid: empty/sold frames keep the same track as live cards. */
  display: grid !important;
  grid-template-columns: repeat(var(--shop-n, 4), minmax(0, 1fr));
  grid-template-rows: minmax(0, 1fr);
  /* Share the panel's 12px side pad with odds / ROLL — was padding 0 8px, so the
     rightmost card stopped 8px short of the lock pill's right edge. */
  gap: 10px;
  /* Top gap to cards is the panel row-gap (--shop-roll-gap). Keep bottom glow
     budget; side inset is the panel padding alone. */
  overflow: visible !important;
  align-items: stretch;
  justify-items: stretch;
  padding: 0 0 14px;
  box-sizing: border-box;
  scrollbar-gutter: auto;
}
/* Every slot fills its track — live, sold, or placeholder. No flex basis, no pilot 248px width. */
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.ss-unit-card,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.sold,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.sold.ss-purchased-card,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .ss-bazaar-slot-upgrade {
  flex: none !important;
  width: 100% !important;
  min-width: 0 !important;
  max-width: none !important;
  align-self: stretch !important;
  justify-self: stretch !important;
  height: 100% !important;
  max-height: 100% !important;
  min-height: 0 !important;
  display: flex !important;
  flex-direction: column;
  overflow: hidden;
  box-sizing: border-box;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.sold.ss-purchased-card {
  /* Keep the empty frame in the same box model as a live card; don't fall back to pilot's fixed px. */
  place-items: unset;
  padding: 10px !important;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .top {
  flex: 1 1 auto;
  min-height: 0;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .ss-pilot-stat-grid {
  flex: 0 1 auto;
  margin-top: auto;
  margin-bottom: 6px;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .ss-unit-buy,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card > .buy {
  flex: 0 0 auto !important;
  flex-shrink: 0 !important;
  margin-top: auto;
  margin-bottom: 8px;
  margin-inline: 8px;
  position: relative;
  z-index: 5;
}
/* Fallback only: the pilot normally lifts Imprison into `.ss-unit-buy` beside Recruit. If decoration
   never runs, keep the loose sibling as a compact jail tab instead of a full-width slab. */
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card > .ss-shop-exile {
  flex: 0 0 auto;
  align-self: flex-end;
  width: 34px;
  min-width: 34px;
  min-height: 34px;
  margin: auto 8px 8px auto;
  padding: 0;
}
/* Scale chrome down so Buy stays visible inside the absolute bazaar height. */
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .cic,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .cic.framed {
  width: clamp(52px, 28%, 72px);
  height: clamp(52px, 28%, 72px);
}
/* Item shop portraits — face size held at itemart5 (163px / 74%).
   WHY itemart7–9 still spilled: clipping .cic (incl. ::after frame) to circle(47.5%)
   shaved the gold PNG's outer metal while art still reached the clip edge / transparent
   frame corners; overflow+radius and dual clip-path on .iart were not enough in Chromium
   once stacking contexts (z-index / ::after) entered. itemart10: leave .cic unclipped so
   the full metal ring paints; hard-clip only an inner .iart-face (inset inside the rim)
   with overflow+clip-path circle(50%) of that face. Troop / relic untouched. Enchant opts out. */
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.ss-pilot-item-card .top {
  display: grid !important;
  grid-template-rows: 163px auto !important;
  align-items: stretch;
  justify-items: stretch;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.ss-pilot-item-card .cic,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.ss-pilot-item-card .cic.framed,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.ss-pilot-item-card .cic.roundframe {
  width: min(163px, 74%) !important;
  height: min(163px, 74%) !important;
  max-width: none !important;
  max-height: none !important;
  align-self: center !important;
  justify-self: center !important;
  position: relative !important;
  /* Soft round; do NOT clip-path the cic — ::after metal must paint full ring. */
  overflow: hidden !important;
  border-radius: 50% !important;
  clip-path: none !important;
  isolation: isolate;
}
/* Enchantment shop cards (enchnoframe1): no circular rarity frame — plain sigil art.
   Economy / gear / doctrine / artifacts keep the framed ring + .iart-face clip. */
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.ss-pilot-item-card .cic.ss-ench-cic {
  overflow: visible !important;
  border-radius: 0 !important;
  clip-path: none !important;
  isolation: auto;
}
/* Hard art mask: inset inside outer metal so no pixel can clear the gold ring. */
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.ss-pilot-item-card .cic .iart-face {
  position: absolute !important;
  inset: 8% !important;
  z-index: 2 !important;
  display: flex !important;
  align-items: center !important;
  justify-content: center !important;
  overflow: hidden !important;
  border-radius: 50% !important;
  clip-path: circle(50% at 50% 50%) !important;
  -webkit-clip-path: circle(50% at 50% 50%) !important;
  pointer-events: none;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.ss-pilot-item-card .cic .iart-face > img,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.ss-pilot-item-card .cic .iart-face .uart,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.ss-pilot-item-card .cic .iart-face .iart,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.ss-pilot-item-card .cic .iart-face .artic {
  max-width: 100% !important;
  max-height: 100% !important;
  width: 100% !important;
  height: 100% !important;
  object-fit: contain !important;
  position: relative !important;
  z-index: 1 !important;
  border-radius: 50% !important;
  clip-path: none !important;
}
/* Fallback if a face wrapper is missing (should not happen for framed items). */
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.ss-pilot-item-card .cic > img,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.ss-pilot-item-card .cic > .uart,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.ss-pilot-item-card .cic > .iart,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.ss-pilot-item-card .cic > .artic {
  max-width: 84% !important;
  max-height: 84% !important;
  width: 84% !important;
  height: 84% !important;
  object-fit: contain !important;
  position: relative;
  z-index: 2 !important;
  border-radius: 50% !important;
  clip-path: circle(50% at 50% 50%) !important;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.ss-pilot-item-card .cic.ss-ench-cic > img,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.ss-pilot-item-card .cic.ss-ench-cic .iart,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card.ss-pilot-item-card .cic.ss-ench-cic .artic {
  width: 100% !important;
  height: 100% !important;
  max-width: 100% !important;
  max-height: 100% !important;
  z-index: 1;
  border-radius: 0 !important;
  clip-path: none !important;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .cic.framed:not(.roundframe) .uart {
  max-width: 85%;
  max-height: 85%;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .cn {
  font-size: clamp(12px, 1.05vw, 15px);
}
/* Unit-targeted gear: short "For <unit>" under the title (Josh 2026-08-03). Sits in the cmeta
   stack with .tag / .pillrow — dim ink, not a bordered pill. Recruits / army-wide / econ omit it. */
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .cfor {
  display: block;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .04em;
  line-height: 1.2;
  color: var(--ink-dim, #9a8fc0);
  margin: 0;
}
/* Item effect line (Josh 2026-08-04): short mechanics under the title — not the full hover tip. */
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .ceff {
  display: block;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .02em;
  line-height: 1.25;
  color: var(--ink, #d4c8f0);
  margin: 0;
  max-width: 100%;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .ss-pilot-stat {
  min-height: 32px;
}
/* One outer box for Acquire and Recruit+Imprison — 34px border-box, same as item .buy.
   Inner Imprison/Recruit must not carry their own 34px min-height or the 2px wrapper
   border grows the troop pill past Acquire. */
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .ss-unit-buy,
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .ss-unit-buy .buy {
  box-sizing: border-box;
  height: 34px;
  min-height: 34px;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .ss-unit-buy:has(.ss-shop-exile) {
  display: flex;
  flex-direction: row;
  align-items: stretch;
  gap: 0;
  overflow: hidden;
  box-sizing: border-box;
  height: 34px;
  min-height: 34px;
  border: 2px solid #d6b35f;
  border-radius: 4px;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, .18), transparent 27%),
    radial-gradient(ellipse at 28% 18%, rgba(255, 255, 255, .3), transparent 24%),
    radial-gradient(ellipse at 72% 76%, rgba(184, 120, 255, .35), transparent 40%),
    linear-gradient(135deg, #542073, #2a123e 54%, #671f87);
  box-shadow:
    inset 0 0 0 2px #5b3d17,
    inset 0 0 0 4px rgba(237, 202, 111, .28),
    inset 0 -9px 18px rgba(18, 7, 29, .6),
    0 5px 16px rgba(0, 0, 0, .45);
}
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .ss-unit-buy:has(.ss-shop-exile) .buy {
  flex: 1 1 auto;
  width: auto;
  height: 100%;
  min-height: 0;
  margin: 0;
  border: 0 !important;
  border-radius: 0 !important;
  background: transparent !important;
  box-shadow: none !important;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .ss-unit-buy .ss-shop-exile {
  flex: 0 0 34px;
  box-sizing: border-box;
  width: 34px;
  min-width: 34px;
  max-width: 34px;
  height: 100%;
  min-height: 0;
  margin: 0;
  padding: 0;
  border: 0 !important;
  border-left: 1px solid rgba(237, 202, 111, .38) !important;
  border-radius: 0 !important;
  background: rgba(18, 7, 29, .38) !important;
  box-shadow: none !important;
  color: #f7e3a7;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .ss-unit-buy .ss-shop-exile:hover:not(:disabled) {
  background: rgba(72, 22, 32, .55) !important;
  color: #ffe6a8;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .ss-unit-buy .ss-shop-exile:disabled {
  opacity: .42;
  cursor: not-allowed;
}
body:has(#prep:not(.hidden)) #prepBody #shopCards > .card .ss-shop-exile-bars {
  width: 18px;
  height: 18px;
  display: block;
  pointer-events: none;
}

/* ── 🖼 DAVE'S BACKGROUND FRAMING (bg-art-editor, 2026-08-01) ────────────────────────────────────────
   Tuned by hand with the Shift+B tool and baked here so it ships without localStorage.
   Layer 1 is the pilot's tint gradient (never moves), layer 2 the town painting. */
body:has(#prep:not(.hidden)) #prep {
  background-position: 0 0, calc(50% - 250px) calc(46% + 15px) !important;
  background-size: auto, cover !important;
}

/* Server / Game chat — bottom-right corner (overrides pilot/theme top dock). */
#chatBox,
#chatMini {
  top: auto !important;
  bottom: 12px !important;
  right: 12px !important;
  left: auto !important;
}
#chatToast {
  top: auto !important;
  bottom: 14px !important;
  right: 66px !important;
  left: auto !important;
}
#chatBox.popIn,
#chatBox.popOut {
  transform-origin: 100% 100% !important;
}
#chatCollapse {
  margin-left: auto;
  min-width: 28px;
  height: 28px;
  padding: 0 8px;
  border: 1px solid rgba(200, 163, 90, .35);
  border-radius: 6px;
  background: rgba(20, 18, 28, .85);
  color: var(--ink-dim);
  font: inherit;
  letter-spacing: .18em;
  cursor: pointer;
  line-height: 1;
}
#chatCollapse:hover {
  color: var(--gold, #e8b64c);
  border-color: rgba(200, 163, 90, .7);
}
body:has(#prep:not(.hidden)) #chatBox {
  top: auto !important;
  bottom: 12px !important;
  right: 12px !important;
  max-height: min(260px, 34vh);
  overflow: hidden;
}
body:has(#prep:not(.hidden)) #chatBox :is(#schatLog, #chatLog) {
  max-height: min(120px, 16vh) !important;
}
body:has(#prep:not(.hidden)) #chatMini {
  top: auto !important;
  bottom: 12px !important;
  right: 12px !important;
}
body:has(#prep:not(.hidden)) #chatToast {
  top: auto !important;
  bottom: 14px !important;
  right: 66px !important;
}
body:has(#prep:not(.hidden)) #chatBox.popIn,
body:has(#prep:not(.hidden)) #chatBox.popOut {
  transform-origin: 100% 100% !important;
}

/* Prep toolbar: Compendium sits beside MAIN MENU; battle preview sits with TO BATTLE.
   Settings is not in the header (MAIN MENU covers exit).
   Bottom-left navrows stay for deploy/battle; hide the duplicates while shopping. */
body:has(#prep:not(.hidden)) #compRow,
body:has(#prep:not(.hidden)) #setRow {
  display: none !important;
}
body:has(#prep:not(.hidden)) #prepHead .prepToolBtn,
body:has(#prep:not(.hidden)) #prepHead #incomingBtn {
  flex: 0 0 auto;
  width: 40px;
  min-width: 40px;
  height: 40px;
  min-height: 40px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 9px;
  box-sizing: border-box;
}
body:has(#prep:not(.hidden)) #prepHead .prepToolBtn .compbtnart {
  width: 22px;
  height: 22px;
  object-fit: contain;
  display: block;
}
