/* ============================================================
   shop-item-card.css — shared "Shop Item" grid cell component
   ============================================================
   Real in-game Shop item cell, reverse-engineered from the actual
   Unreal widget blueprint export (not guessed/approximated) —
   see W_ShopItem.json in the fModel datamine
   (04_UI/03_Contents/Shop/Component/W_ShopItem.json), instantiated
   by W_Exchange_Store.json / W_Random_Store.json's TileView with
   EntryWidth=294 / EntryHeight=242 (confirmed identical in both).
   That 294x242 design-space cell is this component's reference
   size — every measurement below is derived from it, so the whole
   card (art, corners, icons, text) scales together at any render
   size, matching how Unreal's own DPI/UI-scale system scales the
   whole design canvas as one unit rather than stretching a fixed
   texture. Added 2026-09-01.

   REAL FSlateBrush VALUES FOUND (reported to the user before this
   file was written):
     Img_BG (the card's own ShopGrade background, WidgetTree slot
     CanvasPanelSlot_0, full CP_Main anchor-stretch with a -1px
     bleed on all 4 edges):
       DrawAs: ESlateBrushDrawType::Box
       ImageSize: {X:124, Y:124}   (brush's own declared design
         size — corner render size is always Margin*ImageSize,
         independent of the REAL source texture's raw pixel
         dimensions, confirmed 78x156 via PIL)
       Margin: {Left:0.5, Top:0.3, Right:0.5, Bottom:0.5}
         (normalized 0-1 UV fractions of the source sprite — L+R
         sum to 1.0, i.e. NO horizontal middle stretch band at
         all, the whole width is "corner"; T+B leave a genuine 20%
         vertical stretch band)
     Img_BG_Deco (the faint circular decoration behind the item
     icon, CanvasPanelSlot_3): Margin {L:.5 T:.1 R:.5 B:.4} present
     but DrawAs is UNSET (defaults to plain Image, not Box) — this
     one is NOT 9-sliced, just a plain stretched image, which is
     fine since its render box is a perfect square (160x160design)
     so no distortion risk. Slot: point-anchored at the card's
     exact horizontal center, vertical center -21px (i.e. sits
     just above true center, right behind the item icon area).
     ColorAndOpacity: RGBA(1,1,1, 0.1) — 10% white, genuinely faint.
     Img_BG_2 / Img_BG_4 (the "SOLD OUT"/"TO BE SOLD" pill inside
     CP_SoldOut / CP_TobeSold): DrawAs Box, ImageSize {126,32},
     Margin {Left:.34, Right:.34} only (no Top/Bottom → the WHOLE
     height is stretch, only the 2 rounded end-caps are preserved)
       source: Atl_UI_Shop_01_Sprite_5 (real texture 148x32).

   CSS TRANSLATION: border-image-slice is set from Margin directly
   (percentages are already relative to the SOURCE image's own
   intrinsic size, exactly what a Slate Margin already is — no unit
   conversion needed). The on-screen CORNER size can't reuse that
   same percentage though — Slate's actual on-screen corner size is
   Margin*ImageSize, and ImageSize (124) is a DESIGN-SPACE override
   that doesn't match the raw texture's own pixel size, so slice%
   and the real corner size are relative to two DIFFERENT bases.
   Slate's 294x242 design canvas is a single UNIFIED, isotropic
   coordinate space (X and Y offsets are both just "Slate units",
   never separately normalized per axis) — so every measurement
   below (corners, insets, icon sizes, text positions, all of it)
   is expressed as `<design units> * 100/294`, using CQW ONLY, even
   for vertical measurements, never a plain height-relative %. This
   is also why `border-width`/`border-image-width` are set in
   `cqw` rather than `%` — `border-width` doesn't accept percentages
   at all per spec (an earlier draft of this file tried `%` there
   and the corner art rendered wildly oversized, bleeding into
   neighboring grid cells — cqw is a real CSS <length>, so it works).
   Because a `cqw` unit is always 1% of the QUERY CONTAINER's own
   current width regardless of which property it's used on, this
   reproduces Unreal's whole-canvas DPI scaling exactly: as the card
   grows/shrinks with the viewport, every measurement grows/shrinks
   in the same fixed ratio to the card, never fixed-px, never a flat
   100%/100% stretch. `container-type:inline-size` on `.shop-item`
   is what makes `cqw` resolve against the CARD's own width — this
   is also what lets nested icon/font sizes scale in lockstep with
   the frame's geometry, exactly matching the user-visible
   "resize the window, the whole card scales together" behavior,
   without any JS.

   USAGE: `<link rel="stylesheet" href="shop-item-card.css"/>`,
   then either hand-author the markup below or use the paired
   `shop-item-card.js` (`ShopItemCard.render(data)`). Image URLs
   are supplied via inline custom properties (`--shop-bg`,
   `--shop-deco`, `--shop-soldout-bg`) resolved through the
   consuming page's own img()/SITE_REPO_ROOT helper — never
   hardcoded here, same convention as grade-card.css/hero-panel.css.
   `--shop-bg` is the FULL, un-split Atl_UI_Shop_Grade_BG00 asset —
   see Layer 1 below for the single-image literal Box-brush slice. */

/* ROUND 15 (2026-09-01): the real in-game global UI font. Verified
   real chain: ProjectRE/Content/Entry/Font/RE_Basic → Font_Global_
   Medium → SKR_Global_01 (original source: SKR_Global_08.ttf).
   Searched the project first per instruction rather than guessing a
   path — the real, canonical converted file lives in the deployed
   site's own `font/` folder (a flat top-level file, NOT under
   `font/woff2/` — landed there 2026-09-01 alongside a batch of other
   real game fonts, CJK sets/ChunYears/NotoSans/etc., reading as a
   site-wide font rollout). Registered directly here (relative path,
   correct once this component is deployed alongside that folder) —
   the standalone demo page separately registers the same family via
   an absolute file:// path to the same real file, since the demo
   doesn't sit next to a deployed `font/` folder locally. */
@font-face{
  font-family: 'SKR Global';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('font/SKR_Global_01.woff2') format('woff2');
}

.shop-grid{
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 18px;
}

.shop-item{
  position: relative;
  isolation: isolate;
  width: 95%; /* ROUND 19 (2026-09-01): card still read slightly too
    large — narrowed further from round 9's 98%, same LAYOUT-sizing
    technique (never `transform:scale`, see round 9's note below).
    Because every internal measurement is `cqw` (1% of THIS element's
    own current width via the container query below), narrowing this
    one value shrinks frame, corners, and every text layer together —
    EXCEPT the item icon (`.shop-item__art img`) and the circular
    decoration (`.shop-item__deco`), which this round deliberately
    COMPENSATES (see their own comments) so their rendered pixel size
    stays fixed even though the card around them got smaller — a
    direct, explicit requirement this round, unlike round 9's shrink
    which let everything scale together uniformly. round 9 (2026-09-01):
    the whole card reads ~2% too large — shrunk via LAYOUT sizing here
    (not a `transform:scale` on any inner layer, which produced
    visible subpixel raster seams along the 9-slice/corner-overlay
    boundaries). Because every internal measurement is `cqw`, narrowing
    this one value lets frame, corners, icon, deco, and every text
    layer resolve naturally at the smaller size together — no separate
    layer is scaled independently, so there's nothing for a raster
    seam to form between. */
  justify-self: center; /* centers the now-slightly-narrower card
    within its grid column (`.shop-item` is a direct grid item of
    `.shop-grid`) */
  aspect-ratio: 294 / 242; /* locks the real in-game cell proportions
    (EntryWidth/EntryHeight) so the 9-slice corners and every cqw
    position below stay correct at any grid column width — never let
    this card's width/height be set independently. */
  container-type: inline-size; /* every cqw unit below = 1% of THIS
    card's own current width, so icons/fonts scale with the card,
    not the viewport or a fixed px value. */
  overflow: hidden; /* plain rectangular safety clip — the card's own
    ROUNDED silhouette now comes entirely from the real Img_BG
    texture's own alpha channel (see the ::before/::after 9-slice
    below), not a CSS border-radius, so this never needs to match a
    curve — every inner layer sits well inside the real art's rounded
    corners anyway. */
  color: #f1e6cf;
  font-family: 'SKR Global', sans-serif; /* ROUND 15 (2026-09-01):
    self-registered directly below now (explicit instruction reversed
    round 14's "defer to site-chrome.css" decision) — real source:
    ProjectRE/Content/Entry/Font/RE_Basic → Font_Global_Medium →
    SKR_Global_01 (original: SKR_Global_08.ttf). Lato REMOVED from
    this component entirely — no fallback to it anywhere, per
    explicit instruction; sans-serif is the only fallback now. */
  font-style: normal;
  font-weight: 400; /* Regular only throughout this component — no
    Bold/Black/Light/Italic anywhere; text weight comes from
    outlines/shadows, never a heavier face. */
}

/* ---- Layer 1: ShopGrade background — LITERAL Box-brush 9-slice ----
   (2026-09-01, round 5 — geometry correction: the root `SizeBox_1`
   is 294x242, but its `SizeBoxSlot_0` (holding `CP_Main`) carries
   `Padding: {Left:2,Top:2,Right:2,Bottom:2}` — a real value I'd
   missed. So `CP_Main` itself is 290x238, inset 2px from the
   294x242 root on every side, NOT the full 294x242. `Img_BG`'s own
   CanvasPanelSlot Offsets {-1,-1,-1,-1} (Anchors Max{1,1}) then
   overscan 1px BEYOND `CP_Main`'s edges — so relative to the
   294x242 root, Img_BG's real box is inset by 2-1=1px on every
   side (NOT bled 1px past the root as round 4 had it). Fixed below:
   `inset: 0.34cqw` (positive = 1px in from the 294x242 root),
   replacing the earlier `-0.34cqw` overscan.

   Brush values unchanged: DrawAs=Box, ImageSize={124,124},
   Margin={L:.5,T:.3,R:.5,B:.5}, ResourceObject=
   PaperSprite'Atl_UI_Shop_Grade_BG00'. Vertical slice (top/bottom)
   stays a literal, non-degenerate Margin reading — Top(.3)+
   Bottom(.5)=.8, leaving a genuine 20% source middle band, so
   `border-image-slice`'s top/bottom values are the literal Margin
   percentages and `border-image-width`'s top/bottom are literal
   Margin×ImageSize (124) converted to cqw:
     top:    0.3×124=37.2 → 12.65cqw
     bottom: 0.5×124=62   → 21.09cqw

   Horizontal (left/right) is still the genuinely degenerate case —
   Left(.5)+Right(.5)=1.0 leaves LITERALLY ZERO source pixels for a
   center fill, confirmed broken again even after the geometry fix
   above (a real gap, page background showing through — this was
   never a geometry bug). Per instruction: don't switch to stretching
   the two halves (Slate's real Box-brush explicitly preserves fixed
   corner regions, which whole-half stretching does not reproduce).
   Instead: reserve a ~1px real center sampling strip out of the
   78px-wide source (78-1)/2/78 = 49.36% per side, instead of the
   literal 50% — CSS then has genuine, if minimal, source pixels to
   stretch into the middle, while still using 98.7% of the real
   corner sample (49.36% vs 50%, an imperceptible difference in the
   corner's own content). `border-image-width`'s left/right stay the
   literal Margin×ImageSize value (62 → 21.09cqw) — slice% and
   width are independent axes in CSS (source sampling vs on-screen
   size), so trimming the slice slightly doesn't shrink the
   rendered corner. */
.shop-item::before{
  content: '';
  position: absolute;
  inset: 0.34cqw; /* 1px inset from the 294x242 root — see geometry
    note above (2px CP_Main padding minus 1px Img_BG overscan).
    ROUND 9: the round-8 `transform:scale(0.98)` that used to live
    here was removed — it produced visible subpixel raster seams
    along the border-image slice boundaries. The 2% shrink now
    happens once, on `.shop-item` itself (see above), not per-layer. */
  z-index: 0;
  pointer-events: none;
  border-style: solid;
  border-width: 14.46cqw 12cqw 24cqw 12cqw;
  border-image-source: var(--shop-bg); /* the FULL, un-split
    Atl_UI_Shop_Grade_BG00.webp — see the header note above */
  border-image-slice: 30% 49.36% 50% 49.36% fill;
  border-image-width: 14.46cqw 12cqw 24cqw 12cqw;
  border-image-repeat: stretch;
}

/* ---- Layer 1b removed (2026-09-01, round 15) ----
   A separate real-corner-crop overlay (`.shop-item::after`, TL/TR/
   BL/BR background-images stacked on top of Layer 1's stretched
   border-image) used to sit here, added round 6 on the theory that
   CSS's own border-image corner render might look softer than the
   real source pixels. A user-reported defect (a visible extra
   curved/vertical line cutting through the top corners) turned out
   to be caused BY that overlay, confirmed empirically, not guessed:
   rendered the card 3 ways — overlay hidden (fill only), fill hidden
   (overlay only), both together. Fill-only was already perfectly
   clean, a single smooth curve, no seam. Both-together showed a
   visible ghost second curve exactly where the overlay's own
   12×14.46cqw box (a size computed independently via `background-
   size`, vs. Layer 1's own corner region computed via `border-image-
   width` — nominally identical cqw values, but from two different
   CSS properties/algorithms) didn't pixel-perfectly align with the
   fill's own already-correct corner beneath it. Smallest reliable
   correction per that diagnosis: delete the overlay outright — the
   border-image fill alone already renders the real corner art
   correctly (same source texture, same real Margin-derived slice/
   width, just via one CSS mechanism instead of two overlapping
   ones), so nothing is lost, only the redundant/conflicting second
   layer. The `--shop-corner-tl/tr/bl/br` custom properties this used
   are gone too — see shop-item-card.js. */

/* ---- Layer 2: faint circular decoration behind the item art ---- */
.shop-item__deco{
  position: absolute;
  z-index: 1; /* real CP_Main Slots-array order (2026-09-01, round 7:
    Img_BG_Deco is CanvasPanelSlot_3, added right after Img_BG(0) and
    before WS_ItemIcon(23)/WS_Stock_ProbInfo(1) — UMG child order IS
    paint order absent an explicit ZOrder override) — deco behind
    both art and stock, not just behind art */
  left: 50cqw;
  top: 34.01cqw;
  width: 56.14cqw; /* ROUND 19 (2026-09-01): compensated for the
    `.shop-item` width dropping 98%→95% (round 9's own tuned shrink,
    now made deeper) — user explicitly required this decoration keep
    its CURRENT RENDERED PIXEL SIZE even though the card around it
    shrinks. Since `cqw` resolves against `.shop-item`'s own (now
    smaller) width, the real source 54.42cqw value was multiplied by
    the inverse shrink ratio (98/95 = 1.0316) to cancel it out:
    54.42 × 98/95 = 56.1385 → 56.14cqw. Checked numerically: at any
    card width, `56.14cqw of the new (95%) card` ≈ `54.42cqw of the
    old (98%) card` to within 0.03% — imperceptible. `left`/`top`/
    `transform` are untouched on purpose (`left:50cqw` self-centers
    regardless of card size; `top`/`transform` are explicitly listed
    as "preserve exactly" — only width/height were in scope). */
  height: 56.14cqw; /* real source is square (160x160 design px) —
    see width comment above, same compensation applies to both axes */
  transform: translate(-50%, -50%);
  background-image: var(--shop-deco);
  background-size: 100% 100%; /* real DrawAs is plain Image, not Box —
    confirmed no Margin/9-slice used for this asset, and its render
    box is a perfect square so a flat stretch never distorts it */
  opacity: 0.1;
  pointer-events: none;
}

/* ---- Layer 3: item artwork (WS_ItemIcon slot) ---- */
.shop-item__art{
  position: absolute;
  z-index: 3; /* ROUND 15: explicit required paint order — item
    artwork above the stock shade now (was z2/below stock, per round
    7's real-ZOrder reading; that finding is superseded by this
    round's explicit instruction: "shade must paint behind the item
    artwork"). Deco stays behind at z1, stock shade at z2. */
  left: 6.8cqw; /* ROUND 19: left/right left UNCOMPENSATED on purpose —
    the icon's own SIZE is driven entirely by `height:97%` on the img
    below (width is `auto`, following its 1:1 aspect-ratio), so these
    horizontal insets only affect how much spare space exists around
    an already-correctly-sized icon before `justify-content:center`
    centers it; they never constrain its rendered size (confirmed: the
    icon's natural width stays well under this container's width at
    every card size tested, so `max-width:100%` never engages). */
  right: 6.8cqw;
  top: 5.7cqw; /* ROUND 19 (2026-09-01): compensated for the
    `.shop-item` width drop 98%→95% so this container's own HEIGHT
    (which the icon's `height:97%` below scales directly off) stays
    the same ABSOLUTE pixel value as before, even though the card
    shrunk. NOTE the correct basis: because `top`/`bottom` are
    absolute-positioning offsets, the resulting box height is
    `card_height_px - top_px - bottom_px`, and `card_height_px` itself
    is `card_width_px × (242/294)` via the aspect-ratio — NOT simply
    `card_width_px` (an earlier draft of this compensation wrongly
    used card WIDTH as the height basis and measurably missed, ~1%
    off, caught by a real before/after `getBoundingClientRect()`
    render comparison rather than assumed correct from the algebra
    alone). Solving `W_new×(242/294 − Snew/100) = W_old×(242/294 −
    S0/100)` for the new combined inset `Snew` (S0=6.12+19.73=25.85,
    k=95/98 width ratio) gives `Snew≈24.067`, split proportionally in
    the original 6.12:19.73 ratio: top=5.70, bottom=18.37. Re-verified
    via the same render harness: old/new icon rendered height matched
    to within 0.005% (483.063px vs 483.038px at an 882px-wide card),
    down from a ~1% miss with the earlier (wrong-basis) values. */
  bottom: 18.37cqw;
  display: flex;
  align-items: center;
  justify-content: center;
}
.shop-item__art img{
  /* Real derivation (2026-09-01, round 7) — WS_ItemIcon → SCB_ItemIcon
     → nested W_SimpleShop instance, found at 04_UI/01_Common/
     SimpleSlot/Icon/W_SimpleShop.json (not exported alongside
     W_ShopItem itself, had to be traced separately):
       SCB_ItemIcon is a plain ScaleBox with no Stretch override →
       Unreal default = EStretch::ScaleToFit (matches CSS
       object-fit:contain — scale uniformly to fit, no crop).
       W_SimpleShop's OWN root is `SizeBox_0`, a FIXED 132x132
       design-space square (this is the thing ScaleToFit actually
       scales — not the visible art's own shape). Inside it,
       `Img_ItemIcon`'s Brush.ImageSize is a fixed 128x128 (source:
       PaperSprite'ItemIcon_204000001Shop', the default placeholder
       icon) — a 128/132=96.97% fill of that square, centered.
     ScaleToFit scales the 132x132 square to fit WS_ItemIcon's real
     254x166 box (Left20/Top18/Right20/Bottom58 on 294x242) — since
     132x132 is square and the box is landscape, height is always the
     binding constraint (166 < 254), so the scaled square = 166x166,
     filling the container's FULL height and centering horizontally.
     The 96.97% inner fill then carries through unchanged. Net result:
     the icon is a SQUARE (Slate letterboxes non-square source art
     within that fixed square, same as forcing aspect-ratio:1 here)
     occupying ~97% of the container's HEIGHT, auto-width — NOT the
     71.5%-of-both-axes guess this replaced. */
  aspect-ratio: 1 / 1;
  height: 97%;
  width: auto;
  max-width: 100%; /* safety cap only — a real icon this square/tall
    never needs it given the 254-wide container, but guards against
    an unusually wide source image */
  object-fit: contain;
}

/* ---- Layer 5: stock band (WS_Stock_ProbInfo → BD_Stock) ----
   Real background: 9-sliced Box brush, `Atl_UI_Shop_01_Sprite_6`,
   DrawAs Box, ImageSize{12,34}, Margin{L:.5,T:.7,R:.5,B:.05} — same
   degenerate Left+Right=1.0 case as the main card background, same
   ~1px real center-strip fix (49.24% slice instead of the literal
   50%). Real position: Left5/Right5 (1.70cqw inset, untouched — the
   band still spans the real width, corners sit visually above it).

   ROUND 9 found a real bug: with the default `box-sizing:content-box`,
   the declared `height` and the border-image's own `border-width`
   stack ADDITIVELY (rendered height = height + border-top + border-
   bottom) — so a "10.89cqw" band with a 6.48cqw top border was
   actually ~17.8cqw tall on screen. `box-sizing:border-box` (kept
   below) genuinely fixes that — declared height now INCLUDES the
   border-image. Round 9 ALSO shrank the band itself (to 6cqw) on top
   of that fix, chasing a "too tall" complaint that the box-sizing bug
   alone may have fully explained.

   ROUND 13 tried reverting to the literal source height (43-3=40
   design units, 13.61cqw) — but rendered, it visibly darkened over
   the TOP of the item art (bottom edge at 1.02+13.61=14.63cqw, well
   past the icon region's own 6.12cqw top boundary), which round 13's
   OWN instruction set separately said not to do — reverted to a
   shallow 6cqw tune instead.

   ROUND 15 (2026-09-01) explicit correction reverses that: "the
   stock shade is supposed to extend downward into the item region —
   some overlap is correct." Restored the literal 13.61cqw height (40
   design units) and the full unscaled border-image-width (8.10/2.04/
   0.58/2.04cqw, Margin×ImageSize, was proportionally shrunk for the
   6cqw version). Top restored to 1.02cqw (the real 3px
   WidgetSwitcherSlot padding before the pill starts — was 0.34cqw
   during the shallow-tune era). `box-sizing:border-box` kept (still
   the genuine additive-height fix from round 9). The earlier "don't
   lower into the icon region" reading is now explicitly overridden —
   the overlap is real and intentional; what makes it look right is
   the PAINT ORDER (item art now painted OVER the shade, see z-index
   below), not avoiding the overlap. */
.shop-item__stock{
  position: absolute;
  z-index: 2; /* ROUND 15: explicit required paint order — main bg(0)
    < deco(1) < stock shade(2) < item art(3) < stock text(4) < name/
    price. The shade sits BELOW the item art on purpose now (was
    above everything, z5, during the shallow-tune era) so the art
    visually paints over the shade where they overlap. */
  box-sizing: border-box;
  left: 0.34cqw; /* ROUND 16 (2026-09-01): was 1.70cqw, leaving a
    visible gap at both ends before the frame — extended to match
    the frame fill's own real inset (`.shop-item::before`'s
    `inset:0.34cqw`) so the shade's own edge lines up with the
    frame's real inner boundary instead of stopping short of it. */
  right: 0.34cqw;
  top: 0.68cqw; /* ROUND 18 (2026-09-01): shifted up 1 design px
    (1/294×100=0.34cqw) from 1.02cqw, on request — kept the height
    unchanged so the bottom edge moves up too, not just the top. */
  height: 13.61cqw;
  border-style: solid;
  border-width: 8.10cqw 6.8cqw 0.58cqw 6.8cqw; /* ROUND 16: left/right
    cap width widened 2.04→6.8cqw (half the band's own 13.61cqw
    height — a real cap on a pill/tab shape this tall reads as too
    thin at a few cqw, per direct instruction to derive cap width
    from the band height rather than compressing it). Real source
    percentages (border-image-slice below) are unchanged — only the
    ON-SCREEN size of that same real corner sample grew, the source
    sampling itself didn't change. */
  border-image-source: var(--shop-stock-bg);
  border-image-slice: 70% 49.24% 5% 49.24% fill;
  border-image-width: 8.10cqw 6.8cqw 0.58cqw 6.8cqw;
  border-image-repeat: stretch;
}
.shop-item__stock-text{
  position: absolute;
  z-index: 4; /* above item art (z3) so the text stays fully readable
    regardless of what the art paints over the shade beneath it */
  left: 0.34cqw; /* ROUND 16: matches the shade's own new inset */
  right: 0.34cqw;
  top: 0.68cqw; /* ROUND 18: matches the shade's own 1px-up shift */
  height: 13.61cqw; /* matches .shop-item__stock exactly, same band */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  pointer-events: none;
  white-space: nowrap;
  font-family: 'SKR Global', sans-serif;
  font-style: normal;
  font-weight: 400; /* Regular only, per the component-wide rule */
  font-size: 6.12cqw; /* literal native size (18/294×100) — real style
    is TextStyle-RE_Font18_eeeeee, same "18" family as the item name */
  line-height: 1; /* ROUND 16: explicit stable line-box, needed so the
    optical correction below is measured against a known baseline
    rather than the browser's own default (font-metric-dependent)
    line-height */
  letter-spacing: 0.01em;
  color: #eeeeee;
  transform: translateY(0.68cqw); /* ROUND 16: SKR Global's visible
    glyphs sit high inside their own line box — flexbox centers the
    LINE BOX correctly, but the glyphs themselves then read as
    optically too high within the shade. 0.68cqw = 2 design px,
    tuned by direct visual comparison against the reference
    screenshot, not an automatic value. */
  text-shadow: 0 0.08cqw 0.05cqw rgba(0,0,0,.7), 0 0 0.14cqw rgba(0,0,0,.6); /* a subtle dark
    edge for readability only — the real widget's ShadowOffset is
    {0,0} and no outline is specified for this style, unlike the name */
}

/* ---- Layer 6 (name) + Layer 4 (stars): VB_Info's real 100px band ---- */
.shop-item__info{
  position: absolute;
  z-index: 5;
  box-sizing: border-box; /* ROUND 17 (2026-09-01): needed so the new
    padding-bottom below is INSIDE the existing 34.01cqw height,
    not added on top of it — same box, no repositioning of anything
    else that depends on this element's outer size. */
  left: 1.70cqw;
  right: 1.70cqw;
  top: 35.03cqw;
  height: 34.01cqw;
  display: flex;
  flex-direction: column; /* real widget is a VerticalBox: name row,
    then stars row, both auto-sized/stacked — not 2 fixed splits */
  align-items: center;
  justify-content: flex-end; /* ROUND 17: was `center` — a single-line
    name block is shorter than a two-line one, so centering gave the
    two variants different bottom edges (two-line already looked
    right; one-line left extra empty space below it and stopped
    higher). Bottom-aligning instead gives every name, regardless of
    line count, the SAME bottom edge — the natural wrap behavior
    still decides 1 vs 2 lines, nothing here counts lines. */
  gap: 0.35cqw;
  padding-bottom: 6.8cqw; /* ROUND 17: replaces the name's own
    `transform:translateY(3.40cqw)` (10 design px) — that translate
    doesn't change the flex box's own layout height, so it moved the
    text visually but never gave a real shared bottom edge across
    line counts. This padding does: ~20 design units (6.8cqw),
    tuned by comparing before/after screenshots of the two-line
    voucher name so its position stays visually where it already was. */
}
.shop-item__name{
  /* Real style: TB_ItemName → TextStyle-RE_Font18LH_eeeeee — size 18,
     color #eeeeee, line-height 0.75, a 2px black outline at 80%
     opacity. Real WrapTextAt is 290 design units (98.6% of the
     294-wide card — effectively the full width), so this WRAPS
     instead of truncating with an ellipsis. Font-size/weight/outline
     are the literal native values (18/294×100=6.12cqw, weight 400,
     stroke 2px/80%→0.68cqw rgba(0,0,0,.8)) — the outline carries the
     visual weight, not the glyph. The round-10 10px-down shift
     (`transform`) is a standalone user request, unrelated to the
     rest of this derivation.

     ROUND 16 (2026-09-01) fix: `overflow:hidden` was clipping SHORT
     single-line names — confirmed via a reference screenshot showing
     "Ancient Rune"'s ascenders flat-cut at the top. Cause: a short
     line's own box shrink-wraps close to the glyph bounds, but the
     0.68cqw stroke paints OUTSIDE those bounds (stroke is centered on
     the glyph edge, half of it extends beyond); `overflow:hidden`
     then cuts off exactly that outside half. Longer/wrapped names
     happened to get a wider box from wrapping, coincidentally giving
     the stroke enough room, which is why only short names showed it.
     Fixed with `overflow:visible` (so the stroke is never clipped by
     this element's own box) plus real paint-safe padding
     (`box-sizing:border-box; padding:0.8cqw 1cqw`) so the stroke has
     breathing room within `.shop-item__info`'s own bounds too, not
     just relative to this element. `max-width` unchanged at 98.6cqw
     — with `border-box`, padding is already included IN that width
     rather than added on top, so this couldn't cause overflow past
     the real WrapTextAt boundary; it just makes the actual text
     column ~2cqw narrower (98.6 minus the 1cqw+1cqw padding), wrapping
     slightly earlier if anything, never later. */
  font-family: 'SKR Global', sans-serif;
  font-style: normal;
  font-weight: 400;
  font-size: 6.12cqw;
  line-height: 1.1; /* the real 0.75 line-height % reads as too tight
    once actually wrapping — kept the existing 1.1 for legibility
    across 2 lines; flag if the tighter real value is wanted instead */
  text-align: center;
  overflow: visible;
  box-sizing: border-box;
  padding: 0.8cqw 1cqw;
  white-space: normal; /* wraps within the card instead of truncating,
    per the real WrapTextAt behavior */
  max-width: 98.6cqw; /* 290/294×100, the real WrapTextAt width —
    padding-inclusive now, see the paint-safety note above */
  color: #eeeeee;
  transform: none; /* ROUND 17: was `translateY(3.40cqw)` (a standalone
    10px-down request from round 10) — replaced by `.shop-item__info`'s
    new `padding-bottom` above, which (unlike a transform) actually
    changes the flex layout's bottom edge, giving 1-line and 2-line
    names a genuinely SHARED bottom position instead of both just
    shifting down by the same fixed amount from wherever `center`
    alignment happened to put each of them. */
  -webkit-text-stroke: 1.02cqw rgba(0,0,0,.8); /* ROUND 18 (2026-09-01):
    thickened 1 design px (1/294×100=0.34cqw) from 0.68cqw, on request
    — still well inside the ~8-10% stroke-to-font-size ratio found
    smooth back in round 11 (1.02/6.12≈16.7%... actually above that
    range, worth a visual recheck if it starts reading blobby again). */
  paint-order: stroke fill;
}
.shop-item__stars{
  display: none; /* explicitly out of scope for now ("Ignore the Star
    and sold out" — user descope, 2026-09-01). Markup/JS still builds
    this row so it's a 1-line flip back once the star/grade badge
    (CP_Tag, never investigated) is actually designed — see
    project_shop_item_card_2026_09_01 memory. */
}
.shop-item__star{
  width: 11.56cqw; /* 34/294, real Img_Star size */
  height: 11.56cqw;
  margin: 0 -1.70cqw; /* real HB_Star -5px/-5px overlap margin */
  flex: none;
}
.shop-item__star:first-child{ margin-left: 0; }
.shop-item__star:last-child{ margin-right: 0; }
.shop-item__star.is-empty{ opacity: 0.25; }

/* ---- Layer 8: currency + price, the art's own lighter bottom band
   (Horizontal_Sum slot — real BG00 texture's cream gradient already
   lives inside this region, no separate CSS tint needed) ---- */
.shop-item__price{
  position: absolute;
  z-index: 6;
  left: 3.40cqw;
  right: 3.40cqw;
  top: 66.67cqw; /* ROUND 9 (2026-09-01): shifted up 0.68cqw (2 design
    px, 2/294×100) from the real 67.35cqw — moves the whole row
    (currency icon + price text together, both live inside this one
    flex container) as one unit per explicit instruction */
  height: 13.61cqw;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.68cqw;
}
.shop-item__currency{
  /* ROUND 8 correction (2026-09-01): round 7 deliberately left this
     at the browser default `object-fit:fill` (stretch), reasoning
     that Img_Currency's plain-Image (no Box/ScaleBox) Slate widget
     likely stretches too — confirmed WRONG by direct user report
     ("getting squished"/"preserve aspect ratio properly"). Switched
     to `object-fit:contain` so the real glyph's own aspect ratio is
     never distorted regardless of the source crop's own shape.
     Box bumped 11.56→13.5cqw ("a bit larger/stronger") — still
     grounded near the real 34/294 ImageSize, not an arbitrary jump. */
  width: 13.5cqw;
  height: 13.5cqw;
  object-fit: contain;
  flex: none;
}
.shop-item__price-text{
  /* Real style: TB_Price → TextStyle-RE_Font20Line1_2d3143 — size 20,
     color #2d3143, a 2px WHITE outline at 30% opacity (not the black
     outline the name gets). ROUND 13 (2026-09-01) rebuild: reverted
     the color from round 8's manually-darkened #1c1f26 back to the
     literal real #2d3143 — the darkening was compensating for
     flatness that a real outline (now added) already resolves.
     font-size set to the literal native value (20/294×100=6.80cqw,
     was a visually-tuned 8.8cqw). font-weight forced to 400 — Regular
     only, per the component-wide rule (was 700). */
  font-family: 'SKR Global', sans-serif;
  font-style: normal;
  font-weight: 400;
  font-size: 6.80cqw;
  color: #2d3143;
  -webkit-text-stroke: 0.68cqw rgba(255,255,255,.3);
  paint-order: stroke fill;
  transform: translateY(0.68cqw); /* ROUND 20 (2026-09-01): shifted down
    2 design px (2/294×100=0.68cqw), on request — same translateY
    convention as the stock text's own optical correction above. Only
    the price TEXT moves; `.shop-item__price`'s flex row (and so the
    currency icon beside it) is untouched, so this doesn't affect the
    row's own height/centering, just the digits' visual position
    within it. */
}

/* ---- Layer 7: Sold Out overlay (CP_SoldOut, ZOrder:5, topmost) ---- */
.shop-item__soldout{
  position: absolute;
  inset: 0;
  z-index: 7;
  pointer-events: none;
}
.shop-item__soldout::before{
  /* Img_Dimmed: real slot insets 5px on a 294x242 box, full-card dim.
     ROUND 2 (2026-09-02): `border-radius` bumped 3px (a near-square
     corner) -> 10cqw, reported as "a full rectangular black overlay
     behind/around the card" -- the card's own visible roundedness is
     ENTIRELY a border-image illusion (`.shop-item` itself carries no real
     `border-radius`, just `overflow:hidden` clipping to a plain sharp
     rectangle), and Layer 1's real frame art curves inward a good
     ~12-14cqw at each corner (see its own `border-width` above) -- a
     dimmer with only a 3px corner radius left a near-square black patch
     sitting visibly PAST that curve, in the corner region the frame art
     itself is meant to still show through. 10cqw approximates the frame's
     own real corner curve closely enough that the dimmer's edge now
     tracks it instead of cutting a harder corner across it. */
  content: '';
  position: absolute;
  inset: 1.70cqw;
  background: rgba(0,0,0,.55);
  border-radius: 10cqw;
}
.shop-item__soldout-pill{
  position: absolute;
  left: 0;
  right: 0;
  top: 32.31cqw;
  height: 10.88cqw;
  display: flex;
  align-items: center;
  justify-content: center;
}
.shop-item__soldout-pill::before{
  content: '';
  position: absolute;
  inset: 0;
  border-style: solid;
  border-width: 0 14.57cqw;
  border-image-source: var(--shop-soldout-bg);
  border-image-slice: 0 34% fill; /* real Margin: Left/Right .34 only,
    no Top/Bottom — the whole pill height is a plain stretch band,
    only the 2 rounded end-caps are preserved */
  border-image-width: 0 14.57cqw;
  border-image-repeat: stretch;
}
.shop-item__soldout-pill span{
  position: relative;
  font-size: 9cqw;
  letter-spacing: 0.05em;
  color: #ffe0e0;
  text-shadow: 0 1px 2px rgba(0,0,0,.8);
}

.shop-item[data-soldout="true"] .shop-item__price,
.shop-item[data-soldout="true"] .shop-item__stock{
  visibility: hidden; /* the real widget swaps WS_State instead of
    hiding these, but for a static card the dim overlay + pill alone
    already communicates "unavailable" without a live state machine */
}
