/* shops.css: The original Dressing Room classes (.dr-*), which the NPC shops and shop hub reuse. The rebuilt room (.dr2-*) is dressing-room.css. */
/* ============================================================
   DRESSING ROOM (.dr-*)
   Own namespace for the same reason .mw-* is: this is a two-column
   canvas-plus-inventory layout, not another workbench variant.
   ============================================================ */
/* The pet artwork is always a 500x500 canvas, regardless of viewport. The image
   partial also contains its status text and regenerate button below the art, so
   the frame itself can be taller than the portrait while it is polling. The
   overlay below is the actual square coordinate surface. */
.dr-canvas-frame {
    position: relative;
    width: 100%;
    max-width: 500px;
    aspect-ratio: 1 / 1;
    margin: 0 auto;
}

/* _pet_image.html keeps a 500x500 intrinsic size for the generated asset, but
   the dressing-room frame is fluid on phone widths. Let the image follow that
   frame so the fixed canvas contract does not become page-level overflow. */
.dr-canvas-frame > [id^="pet-image-block-"] > img {
    display: block;
    width: 100%;
    max-width: 100%;
    height: auto;
}

/* overflow: visible (not hidden) is deliberate: an item may extend slightly
   beyond the canvas when its resolved artwork bounds allow it. pointer-events:
   none keeps the overlay from intercepting clicks meant for the image or
   regenerate button beneath it; each .dr-chip opts back in. */
.dr-canvas-overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: auto;
    aspect-ratio: 1 / 1;
    overflow: visible;
    pointer-events: none;
}

/* During a drag or keyboard nudge, show the real transparent apparel art at the
   marker position. Keep its intrinsic aspect ratio: some early apparel assets
   are tightly cropped rather than 500x500 canvases, and stretching those to the
   whole frame makes the drag preview look enormous. The Alpine component scales
   the intrinsic image with the responsive 500x500 canvas. */
.dr-live-layer {
    position: absolute;
    z-index: 1;
    width: auto;
    height: auto;
    max-width: none;
    max-height: none;
    transform: translate(-50%, -50%) scale(var(--dr-canvas-scale, 1));
    transform-origin: center center;
    pointer-events: none;
    opacity: 0.62;
    filter: drop-shadow(0 0 2px var(--bg-surface));
}

.dr-chip {
    position: absolute;
    transform: translate(-50%, -50%);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    padding: 0;
    border-radius: 50%;
    border: 2px solid var(--color-primary);
    background: var(--bg-elevated);
    box-shadow: var(--shadow-sm);
    cursor: grab;
    pointer-events: auto;
    /* Without this, a touch drag also scrolls/pans the page underneath it:
       the same reason every other pointer-drag surface in this codebase
       needs it (the browser's default touch gesture always wins otherwise). */
    touch-action: none;
    z-index: 2;
}
.dr-chip:active { cursor: grabbing; }
.dr-chip.is-selected {
    z-index: 5;
    border-color: var(--color-warning, var(--color-primary));
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 35%, transparent), var(--shadow-md);
}
.dr-chip img { width: 24px; height: 24px; border-radius: 50%; object-fit: cover; pointer-events: none; }
.dr-chip:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 3px; }

/* The handle variant: same button, same 36/44px hit area (the coarse-pointer bump above
   still applies), but it sits ON TOP of the real live-canvas art rather than standing in
   for missing art, so it reads as a small grab-point instead of a second copy of the item.
   Transparent button chrome + a small ::before dot, so a thin/tiny piece of art (a locket
   chain, a 15px marker) still has a guaranteed, easy-to-see, easy-to-hit place to grab it
   regardless of how small or oddly-shaped its own pixels are. */
.dr-chip--handle {
    background: transparent;
    border-color: transparent;
    box-shadow: none;
}
.dr-chip--handle::before {
    content: '';
    display: block;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: var(--bg-elevated);
    border: 2px solid var(--color-primary);
    opacity: 0.55;
    box-shadow: var(--shadow-sm);
    transition: opacity 0.15s ease, transform 0.15s ease;
}
/* Depth at a glance, on every handle at once, not just the selected one: a dashed
   ring reads as "this one is behind the pet" without having to select it first to
   find out (see apparelDressingRoom.refreshAllDepthStates). Border-STYLE only, so
   it survives being selected too (the .is-selected rules below still win on
   opacity/colour/scale, since they come later in the cascade at equal
   specificity); a solid ring stays the "normal, in front" reading. */
.dr-chip--handle.is-behind-pet::before { border-style: dashed; opacity: 0.35; }
@media (hover: hover) { .dr-chip--handle:hover::before { opacity: 1; transform: scale(1.2); } }
.dr-chip--handle:focus-visible::before { opacity: 1; transform: scale(1.2); }
.dr-chip--handle.is-selected {
    background: transparent;
    box-shadow: none;
}
.dr-chip--handle.is-selected::before {
    opacity: 1;
    transform: scale(1.3);
    border-color: var(--color-warning, var(--color-primary));
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 35%, transparent), var(--shadow-md);
}
body.reduced-motion .dr-chip--handle::before { transition-duration: 0.001s; }

.dr-layers { margin-top: 16px; }
.dr-layer-card {
    display: flex;
    /* Wrapping, not nowrap. The v2 card added view toggles, a position-actions group and
       a scale/rotate form to the row, and a nowrap flex line ran to 635px inside a 375px
       phone. `body` is `overflow-x: clip`, so the excess was not scrollable-to: it was
       cut off. "Take off" and the whole transform row were simply gone on a phone,
       present in the DOM and reachable by keyboard, invisible and untappable to touch.
       Wrapping is a no-op at any width where the row already fits, so the live channel's
       shorter card is unaffected. */
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    margin-bottom: 6px;
    background: var(--bg-subtle);
}
.dr-layer-card { position: relative; }
.dr-layer-card:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
/* Selection is the same signal as the canvas chip's (.dr-chip.is-selected): the ring
   plus a pulled-out accent edge, so a row and its marker read as one lit object. This is
   half of the "which layer did I grab" fix; the chip is the other half. */
.dr-layer-card.is-selected {
    border-color: var(--color-primary);
    background: var(--bg-hover);
    box-shadow: inset 3px 0 0 0 var(--color-primary),
                0 0 0 1px var(--color-primary),
                0 0 16px -6px color-mix(in srgb, var(--color-primary) 60%, transparent);
}
/* The grip is the touch drag origin (see pointerDown): it must not pan the list. */
.dr-layers .drag-handle { touch-action: none; }
.dr-layer-card.is-dragging {
    opacity: 0.55;
    cursor: grabbing;
    box-shadow: 0 10px 24px -12px rgba(0, 0, 0, 0.55);
}
/* Live insertion indicator for a pointer-drag reorder. Positioned by offsetTop against
   .dr-layers, which is position: relative in the v2 panel. */
.dr-drop-line {
    position: absolute;
    left: 4px;
    right: 4px;
    height: 2px;
    border-radius: 2px;
    background: var(--color-primary);
    box-shadow: 0 0 8px color-mix(in srgb, var(--color-primary) 55%, transparent);
    opacity: 0;
    pointer-events: none;
    transition: top 0.08s ease, opacity 0.12s ease;
    z-index: 4;
}
.dr-drop-line.is-on { opacity: 1; }
.dr-layer-card--body { background: var(--bg-hover); border-style: dashed; }
.dr-layer-card__thumb { border-radius: var(--radius-sm); object-fit: cover; }
.dr-layer-card__name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dr-layer-card > .dr-inline-form { display: contents; }
.dr-layer-card__position-actions { display: flex; flex-wrap: wrap; gap: 4px; }
.dr-item-card form { width: 100%; max-width: none; margin: 0; }

.dr-inventory-tabs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.dr-tab-btn {
    padding: 6px 12px;
    border-radius: var(--radius-pill);
    border: 1px solid var(--border-color);
    background: var(--bg-subtle);
    color: var(--text-secondary);
    font-size: 0.85rem;
    cursor: pointer;
    transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
}
@media (hover: hover) { .dr-tab-btn:hover { color: var(--text-primary); border-color: var(--color-primary); } }
.dr-tab-btn.is-active { color: var(--color-primary); border-color: var(--color-primary); background: var(--bg-hover); }
.dr-tab-btn:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }

@media (max-width: 480px) {
    .dr-inventory-tabs { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 4px; }
    .dr-tab-btn { flex: 0 0 auto; }
}

@media (pointer: coarse) {
    .dr-chip { width: 44px; height: 44px; }
    .dr-tab-btn { min-height: 44px; }
    .dr-layers .reorder-nudge__btn { width: 44px; height: 44px; }
    /* The grip is the touch drag origin for a reorder (a whole-card touch drag would eat
       the list's scroll), so it must be a real 44px target, not an 18px glyph. */
    .dr-layers .drag-handle {
        min-width: 44px;
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
    .dr2-layer-view__btn { min-width: 44px; min-height: 44px; }
}

.dr-inventory-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
.dr-item-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    text-align: center;
    padding: 12px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    background: var(--bg-subtle);
}
.dr-item-card.is-equipped { border-color: var(--color-primary); background: var(--bg-hover); }
.dr-item-card__art {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 64px;
    height: 64px;
    border-radius: var(--radius-md);
    background: var(--bg-hover);
    font-size: 1.8em;
    margin-bottom: 4px;
}
.dr-item-card__art img { border-radius: var(--radius-md); object-fit: cover; }
.dr-item-card__name { font-weight: 600; margin: 0; }

.dr-empty { text-align: center; padding: 40px 20px; color: var(--text-muted); }
.dr-empty__icon { font-size: 2.2em; margin: 0 0 8px; opacity: 0.6; }
.dr-empty p { margin: 0; }
.dr-empty--hint { padding: 20px 12px; margin-bottom: 16px; border: 1px dashed var(--border-color); border-radius: var(--radius-md); }
.dr-empty__actions { display: flex; justify-content: center; flex-wrap: wrap; gap: 8px; margin-top: 12px !important; }

/* Backdrop tab: a settings form, not a card grid, so it deliberately does not
   reuse .dr-inventory-grid's auto-fill grid-template-columns. */
.dr-backdrop-panel { display: flex; flex-direction: column; gap: 14px; max-width: 360px; }
.dr-backdrop-preview {
    width: 100%;
    height: 90px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    background: var(--bg-hover);
}

/* Visible, not .sr-only: the canvas marker position and the layer-stack save
   are each their surface's only feedback, and an .sr-only status leaves a
   sighted player with nothing (docs/ux-rules.md). Both nodes are also the
   aria-live region, so the two audiences read the same sentence. */
.dr-status {
    margin: 6px 0 0;
    min-height: 1.25em;
    font-size: 0.85rem;
    color: var(--text-muted);
}

body.reduced-motion .dr-chip { transition-duration: 0.001s; }

/* Wardrobe (/apparel/wardrobe/): item-centric, so each row leads with the
   item, not a pet. Reuses .rarity-chip/.empty-state/.page-header rather than
   introducing parallel tokens for things already styled elsewhere. */
.wardrobe-list { display: flex; flex-direction: column; gap: 10px; }
.wardrobe-filters {
    margin-bottom: 14px;
    padding: 12px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    background: var(--bg-subtle);
}
.wardrobe-filters__controls { display: flex; flex-wrap: wrap; gap: 10px; align-items: end; }
.wardrobe-filters__controls label { flex: 1 1 180px; margin: 0; }
.wardrobe-filters__controls input, .wardrobe-filters__controls select { width: 100%; }
.wardrobe-filters .form-hint { margin: 8px 0 0; }
.wardrobe-item {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    padding: 14px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    background: var(--bg-subtle);
}
.wardrobe-item__art {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    flex: 0 0 auto;
    border-radius: var(--radius-md);
    background: var(--bg-hover);
    font-size: 1.6em;
}
/* Sized to the frame: the <img> carries width/height 72 for layout stability, which
   overflowed the 56px frame (and the 48px phone frame) on every item. contain, not
   cover: apparel art is mostly transparent margin around the piece. */
.wardrobe-item__art img { width: 100%; height: 100%; border-radius: var(--radius-md); object-fit: contain; }
.wardrobe-item__body { min-width: 0; }
.wardrobe-item__name { font-weight: 600; margin: 0 0 4px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.wardrobe-item__worn-by { margin: 4px 0 0; font-size: 0.88em; color: var(--text-secondary); }
.wardrobe-dress-menu { margin-top: 10px; }
.wardrobe-dress-menu summary { cursor: pointer; list-style: none; display: inline-flex; }
.wardrobe-dress-menu summary::-webkit-details-marker { display: none; }
.wardrobe-dress-menu__options { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.wardrobe-dress-menu__options form { margin: 0; }

.apparel-inspection { display: grid; grid-template-columns: minmax(180px, 260px) minmax(0, 1fr); gap: 24px; align-items: start; }
.apparel-inspection__art { display: flex; align-items: center; justify-content: center; min-height: 220px; border: 1px solid var(--border-color); border-radius: var(--radius-md); background: var(--bg-subtle); font-size: 5em; }
.apparel-inspection__art img { width: 100%; height: auto; max-width: 220px; object-fit: contain; }
.apparel-inspection__body > p:first-child { display: inline-block; }
.apparel-inspection__facts { display: flex; flex-wrap: wrap; gap: 18px; margin: 18px 0; }
.apparel-inspection__facts div { min-width: 92px; }
.apparel-inspection__facts dt { color: var(--text-muted); font-size: .85em; }
.apparel-inspection__facts dd { margin: 2px 0 0; font-size: 1.2em; font-weight: 700; }
.apparel-source-list { padding-left: 20px; }
.apparel-source-list li { margin-bottom: 8px; }

.dr-looks { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--border-color); }
.dr-looks form { max-width: none; }
.dr-look-save-form, .dr-look-list, .dr-looks > form:last-child { margin-top: 12px; }
.dr-look-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--border-color); }
.dr-look-row span { min-width: 0; display: flex; flex-direction: column; }
.dr-look-row small { color: var(--text-muted); }
.dr-look-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.dr-look-feedback { margin-top: 12px; padding: 10px; border: 1px solid var(--color-warning); border-radius: var(--radius-md); color: var(--text-primary); }
.dr-look-feedback p { margin: 6px 0 2px; }
.dr-look-feedback ul { margin: 4px 0 0; padding-left: 20px; }

@media (max-width: 700px) {
    .apparel-inspection { grid-template-columns: 1fr; }
    .apparel-inspection__art { min-height: 160px; }
    .dr-layer-card__position-actions { width: 100%; }
}

/* NPC Shops (/shops/): reuses .dr-item-card/.dr-inventory-grid/
   .dr-inventory-tabs wholesale (same catalog-card shape as the dressing
   room's inventory), only adding the description line and the per-currency
   buy-button stack a purchasable card needs on top. Shared by all three
   shops (Prism, Stardust, Apparel). */
.shop-item-grid {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
    align-items: stretch;
}
.shop-item-card {
    align-items: stretch;
    min-width: 0;
    min-height: 100%;
    height: 100%;
    box-sizing: border-box;
    padding: 16px;
    gap: 8px;
    text-align: left;
}
.shop-item-card .dr-item-card__art {
    align-self: center;
    width: 84px;
    height: 84px;
    margin-bottom: 4px;
    border: 1px solid var(--border-subtle);
}
.shop-item-card .dr-item-card__name {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    min-height: 2.3em;
    text-align: left;
    line-height: 1.25;
}
.shop-item-card > .rarity-chip { align-self: flex-start; }
.shop-item-card__meta-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
}
.shop-item-card__meta-row > * { margin: 0; }
.shop-item-card__desc {
    min-height: 3.3em;
    font-size: 0.82em;
    line-height: 1.4;
    margin: 0;
}
.shop-item-card__buy {
    display: flex;
    flex-direction: column;
    gap: 8px;
    width: 100%;
    margin-top: auto;
    padding-top: 12px;
    border-top: 1px solid var(--border-subtle);
}
.shop-item-card__buy form { display: flex; flex-direction: column; gap: 4px; }
.shop-item-card__buy .btn {
    width: 100%;
    justify-content: center;
    min-height: 44px;
    padding-inline: 10px;
    line-height: 1.25;
    white-space: normal;
    overflow-wrap: anywhere;
}
.shop-item-card__buy .hint-warn {
    margin: 0;
    padding: 6px 8px;
    font-size: 0.76rem;
    line-height: 1.35;
    text-align: left;
}
/* Apparel shop stock rotation: an out-of-stock card is dimmed AND states the
   reason in real markup (never opacity alone, per the house accessibility
   rule); the variance chip carries a glyph + word so a discount/markup never
   rests on colour alone either. */
.shop-item-card--out-of-stock {
    opacity: 0.6;
}
.shop-item-card--out-of-stock .dr-item-card__art {
    filter: grayscale(1);
}
.price-variance-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    align-self: flex-start;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 0.74rem;
    font-weight: 600;
    border: 1px solid var(--border-subtle);
}
.price-variance-chip--discount {
    color: var(--color-success-text, var(--color-success));
    background: var(--color-success-bg, transparent);
    border-color: var(--color-success-border, var(--color-success));
}
.price-variance-chip--markup {
    color: var(--color-error-text, var(--color-error));
    background: var(--color-error-bg, transparent);
    border-color: var(--color-error-border, var(--color-error));
}

/* How many copies are left this rotation (ApparelShopStock.quantity_remaining).
   Same chip shape as .price-variance-chip, neutral by default; the --low
   variant (2 or fewer left) borrows the markup chip's warning color so a
   near-sellout reads as urgency without a third color needing a name. */
.stock-remaining-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    align-self: flex-start;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 0.74rem;
    font-weight: 600;
    color: var(--text-muted);
    border: 1px solid var(--border-subtle);
}
.stock-remaining-chip--low {
    color: var(--color-error-text, var(--color-error));
    background: var(--color-error-bg, transparent);
    border-color: var(--color-error-border, var(--color-error));
}

/* How many copies of this item the viewer already holds in their own
   inventory. Same neutral chip shape as .stock-remaining-chip (that one
   states the shop's shelf count, this one states the player's own), shown
   on every NPC/User Shop item card regardless of whether it's 0. */
.owned-count-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    align-self: flex-start;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 0.74rem;
    font-weight: 600;
    color: var(--text-muted);
    border: 1px solid var(--border-subtle);
}

/* Bulk-buy quantity row: a number input plus a few quick-pick buttons
   (5/10/99, clamped to the server-computed max-affordable for that item).
   Mirrors .crafting-quantity-row's shape. */
.shop-quantity-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) repeat(3, minmax(32px, auto));
    align-items: center;
    gap: 6px;
}
.shop-quantity-row label {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    gap: 4px;
    min-width: 0;
    font-size: 0.82em;
    color: var(--text-muted);
}
.shop-quantity-row input[type="number"] { width: 100%; min-width: 0; padding-inline: 8px; }
.qty-quick-btn {
    min-width: 32px;
    padding: 2px 8px;
    min-height: 26px;
    font-size: 0.76rem;
    border-radius: var(--radius-sm, 4px);
    border: 1px solid var(--border-subtle);
    background: var(--bg-surface);
    color: var(--text-secondary);
    cursor: pointer;
}
@media (hover: hover) { .qty-quick-btn:hover { border-color: var(--color-primary); color: var(--text-primary); } }

/* NPC Shop Hub (/shops/): three storefront cards, each a plain link carrying
   its own shopkeeper preview (avatar + name + a random line, resolved from the
   shared personas registry, see personas/registry.py) so the hub reads as three
   characters, not three currency readouts. The 290px floor keeps the card's
   identity, quote, balance, and action readable before the grid drops a column. */
.shop-hub-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 290px), 1fr));
    gap: 16px;
    align-items: stretch;
}
.shop-hub-card {
    display: flex;
    flex-direction: column;
    gap: 16px;
    min-width: 0;
    padding: 18px;
    text-decoration: none;
    color: inherit;
    position: relative;
    overflow: hidden;
    transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
.shop-hub-card:focus-visible { text-decoration: none; }
@media (hover: hover) { .shop-hub-card:hover { text-decoration: none; } }
.shop-hub-card::before {
    content: '';
    position: absolute;
    inset: 0 0 auto 0;
    height: 3px;
    background: linear-gradient(90deg, var(--color-primary), transparent);
    opacity: 0.7;
}
.shop-hub-card__avatar {
    flex: none;
    font-size: 2.2rem;
    line-height: 1;
    width: 60px;
    height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--bg-hover);
    border: 1px solid var(--border-subtle);
}
.shop-hub-card__top { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 12px; min-width: 0; }
.shop-hub-card__heading { min-width: 0; }
.shop-hub-card__kicker {
    display: block;
    margin-bottom: 3px;
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 0.64rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}
.shop-hub-card__heading .panel-title { margin: 0; }
.shop-hub-card__npc { margin: 0; font-size: 0.85em; color: var(--text-secondary); }
.shop-hub-card__body { display: flex; flex: 1 1 auto; flex-direction: column; gap: 10px; min-width: 0; }
.shop-hub-card__desc { margin: 0; line-height: 1.45; }
.shop-hub-card__quote {
    margin: auto 0 0;
    font-style: italic;
    color: var(--text-muted);
    font-size: 0.88em;
    line-height: 1.4;
    border-left: 2px solid var(--border-subtle);
    padding-left: 8px;
}
.shop-hub-card__footer {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding-top: 12px;
    border-top: 1px solid var(--border-subtle);
}
.shop-hub-card .cost-summary-balance { margin: 0; padding: 0; }
.shop-hub-card__cta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    width: 100%;
    box-sizing: border-box;
    padding: 8px 10px;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background: var(--bg-subtle);
    font-size: 0.82em;
    font-weight: 600;
    color: var(--color-primary);
}
@media (hover: hover) {
    .shop-hub-card:hover {
        border-color: var(--color-primary);
        box-shadow: var(--shadow-md, var(--shadow-sm));
        transform: translateY(-2px);
    }
}
.shop-hub-card:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 3px; }

@media (pointer: coarse) {
    .qty-quick-btn { min-height: 44px; min-width: 44px; }
}

/* Shared persona rendering. Domain surfaces may add a dialect-specific class
   (shop banner, workbench panel, or signal plate), but the registry always
   supplies the same portrait/placeholder semantics. */
.persona-surface { min-width: 0; }
.persona-surface__portrait { position: relative; overflow: hidden; }
.persona-surface__portrait img { display: block; width: 100%; height: 100%; object-fit: contain; }
.persona-surface__placeholder {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    font-size: 2.4rem;
    opacity: 0.72;
}
.persona-surface__body { min-width: 0; }
.persona-surface__name,
.persona-surface__role,
.persona-surface__line { margin-top: 0; }

/* The shopkeeper banner at the top of each individual shop page (Prism/
   Stardust/Apparel). Same portrait-box shape as .crafting-npc__portrait, laid
   out horizontally instead of as a sidebar card since a shop page is a wide
   item grid with no natural sidebar column. */
.shop-npc-banner {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 14px 18px;
    margin-bottom: 18px;
    border-radius: var(--radius-lg);
    background: var(--bg-surface);
    border: 1px solid var(--border-color);
    box-shadow: var(--shadow-sm);
}
.shop-npc-banner__portrait {
    flex: none;
    width: 72px;
    height: 72px;
    border-radius: 50%;
    background: var(--bg-hover);
    border: 1px solid var(--border-subtle);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}
.shop-npc-banner__portrait img { width: 100%; height: 100%; object-fit: contain; }
.shop-npc-banner__placeholder { font-size: 2.2em; opacity: 0.75; }
.shop-npc-banner__body { min-width: 0; }
.shop-npc-banner__name { font-family: var(--font-heading); font-weight: 700; margin: 0 0 2px; }
.shop-npc-banner__line { color: var(--text-muted); font-style: italic; margin: 0; }

.shop-switcher {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px;
    margin: -4px 0 16px;
}
.shop-switcher__label {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    margin-right: 4px;
}
.shop-switcher__link {
    display: inline-flex;
    align-items: center;
    min-height: 40px;
    padding: 7px 12px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-pill);
    background: var(--bg-subtle);
    color: var(--text-secondary);
    font-weight: 600;
}
.shop-switcher__link.is-active {
    border-color: var(--color-primary);
    background: var(--bg-hover);
    color: var(--color-primary);
}
.shop-filter-form {
    margin: 0 0 18px;
    padding: 14px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    background: var(--bg-surface);
}
.shop-filter-form__head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 12px;
}
.shop-filter-form__summary { margin: 3px 0 0; color: var(--text-secondary); font-size: 0.84rem; }
.shop-filter-form__grid {
    display: grid;
    grid-template-columns: minmax(180px, 2fr) repeat(3, minmax(120px, 1fr)) minmax(150px, 1.2fr);
    gap: 10px;
    align-items: end;
}
.shop-filter-form__grid .filter-group { min-width: 0; }
.shop-filter-form__grid label {
    display: block;
    margin-bottom: 5px;
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 0.67rem;
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
}
.shop-filter-form__grid input,
.shop-filter-form__grid select { width: 100%; min-width: 0; }
.shop-filter-form__price-fields { display: flex; align-items: center; gap: 5px; }
.shop-filter-form__price-fields input { min-width: 0; }
.shop-filter-empty { margin-top: 10px; }

@media (max-width: 1024px) {
    .shop-filter-form__grid { grid-template-columns: repeat(3, minmax(140px, 1fr)); }
    .shop-filter-form__search { grid-column: span 2; }
}
@media (max-width: 600px) {
    .shop-npc-banner { align-items: flex-start; padding: 12px; }
    .shop-filter-form { padding: 12px; }
    .shop-filter-form__grid { grid-template-columns: 1fr; }
    .shop-filter-form__search { grid-column: auto; }
    .shop-filter-form__price-fields { max-width: 18rem; }
}

