html,body{margin:0;padding:0;background:#04060b}#app{position:relative;width:100%;height:100vh;min-height:480px;margin:0 auto;overflow:hidden;background:#0a1424;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif;color:#eaf2ff;user-select:none}#app:focus{outline:none}#game{width:100%;height:100%;display:block;touch-action:none}#ui,#bubbles,.fx,.indicators{position:absolute;inset:0;pointer-events:none}#ui{z-index:5}#bubbles{z-index:4}.fx{z-index:3}.indicators{z-index:6}.indicator{position:absolute;width:0;height:0;border-left:11px solid #ffd35c;border-top:7px solid transparent;border-bottom:7px solid transparent;filter:drop-shadow(0 0 3px rgba(0,0,0,.7))}.indicator[data-kind=ranged]{border-left-color:#ff7ac0}.indicator[data-kind=boss]{border-left-color:#c98bff;border-left-width:14px;border-top-width:9px;border-bottom-width:9px}.hud{position:absolute;top:14px;left:14px;right:14px;display:flex;gap:14px;flex-wrap:wrap;align-items:flex-start;pointer-events:none;transition:opacity .3s}.hud .stat{background:#08102099;border-radius:10px;padding:6px 10px;border:1px solid rgba(160,200,255,.16)}.hud .lbl{font-size:11px;letter-spacing:.14em;color:#9fb6d1;display:block}.hud .big{font-size:15px;font-weight:700;color:#eaf2ff}.hud .num{font-size:13px;font-weight:700;color:#eaf2ff}.hud .bar{width:130px;height:12px;background:#0c162ccc;border-radius:8px;margin-top:4px;overflow:hidden}.hud .fill{height:100%;width:100%;background:linear-gradient(90deg,#ff7a6b,#8affb0);border-radius:8px}.hud .stat.aura .lbl{font-size:12px}.hud .stat.aura .bar{width:240px;height:22px}.hud .stat.aura .num{font-size:18px}.hud .right{margin-left:auto;text-align:right}.hud .weapon{min-width:150px}.hud .weapon .wrow{display:flex;gap:5px;align-items:center}.wchip{font-size:20px;line-height:1;padding:3px 6px;border-radius:7px;background:#14203899;border:1px solid rgba(160,200,255,.18);filter:saturate(.6) brightness(.9)}.wchip .wkey{display:inline-block;font-size:10px;font-weight:700;margin-right:3px;padding:1px 4px;border-radius:4px;background:#78b4ff38;border:1px solid rgba(160,200,255,.35);color:#bfe0ff;vertical-align:2px}.wchip.active .wkey{background:#96d2ff8c;border-color:#cfe7ff;color:#eaf6ff}.wchip.empty:before{content:""}.wchip--out{opacity:.4;filter:grayscale(.75)}.hud .weapon .whint{color:#8fd1ff;font-size:11px;font-weight:600;letter-spacing:.4px;margin-top:3px;animation:hintPulse 1.4s ease-in-out infinite}@keyframes hintPulse{0%,to{opacity:.45}50%{opacity:1}}.wchip.active{filter:none;border-color:#9fd4ff;background:#3c78c859;box-shadow:0 0 10px #78beff80}.wchip.empty{opacity:.28;filter:none}.hud .boss{border-color:#d9b8ff73}.hud .boss .bar{width:150px}.hud .boss .fill{background:linear-gradient(90deg,#7a3aff,#d9b8ff)}.hud .chips{display:flex;flex-wrap:wrap;gap:8px}.hud .chip{font-size:11px;font-weight:700;padding:4px 8px;border-radius:8px;background:#081020b8;border:1px solid rgba(160,200,255,.22);letter-spacing:.06em}.hud .chip--shield{color:#8affb0;border-color:#8affb059}.hud .chip--boost{color:#ff9b6b;border-color:#ff9b6b59}.hud .chip--dodge{color:#9fd4ff;border-color:#9fd4ff59}.toast{position:absolute;bottom:22px;left:50%;transform:translate(-50%);padding:8px 18px;border-radius:14px;background:#0c162cd9;border:1px solid rgba(160,200,255,.28);font-size:13px;font-weight:700;letter-spacing:.08em;animation:toast-in .25s ease-out;transition:opacity .4s;pointer-events:none}.toast--out{opacity:0}@keyframes toast-in{0%{opacity:0;translate:-50% 8px}to{opacity:1;translate:-50% 0}}.menu{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;text-align:center;background:radial-gradient(ellipse at 50% 40%,#1428508c,#060c1ae6);pointer-events:auto;animation:fadepop .35s ease}.menu--win{background:radial-gradient(ellipse at 50% 40%,#2e603c80,#060c1ae6)}.menu h1{font-size:clamp(26px,6vw,58px);margin:0;letter-spacing:.05em;text-shadow:0 0 24px rgba(160,200,255,.5)}.menu .sub{color:#aac4e4;margin:0}.menu .controls{color:#8fa8c2;margin:10px 0 0;line-height:1.7;font-size:13px}.menu .tag{color:#6f86a3;font-size:12px}.menu .stats{display:flex;gap:18px;margin:6px 0}.menu .stats div{background:#0a1428b3;border-radius:10px;padding:10px 18px;border:1px solid rgba(160,200,255,.16)}.menu .stats b{color:#9fb6d1;font-size:11px;letter-spacing:.12em;display:block}.menu .stats span{font-size:20px;font-weight:800}.menu .rank{color:#ffd04f;font-weight:800;letter-spacing:.14em}.btn-row{display:flex;gap:16px;margin-top:14px}.btn{background:#183056e6;color:#eaf2ff;border:1px solid rgba(160,200,255,.3);border-radius:12px;padding:12px 26px;font-size:16px;font-weight:700;cursor:pointer;transition:transform .12s,background .12s}.btn:hover{background:#284c7ee6;transform:translateY(-2px)}.bubble{position:absolute;pointer-events:none;max-width:220px;padding:7px 12px;border-radius:12px;background:#0c162ceb;border:1px solid rgba(60,120,220,.35);font-size:14px;font-weight:600;color:#eaf2ff;text-align:center;white-space:normal;line-height:1.25;overflow-wrap:break-word;transition:opacity .2s}.fx-float{position:absolute;pointer-events:none;font-weight:800;font-size:18px;text-shadow:0 2px 8px rgba(0,0,0,.7);transition:opacity .2s}.fx-float--small{font-size:15px}.banner{position:absolute;left:50%;top:21%;transform:translate(-50%,-50%);font-size:22px;font-weight:800;letter-spacing:.12em;text-shadow:0 2px 14px rgba(0,0,0,.7);animation:fadeup .5s ease}.banner--big{font-size:30px;letter-spacing:.2em}.banner--out{opacity:0;transition:opacity .6s}@keyframes fadeup{0%{opacity:0;transform:translate(-50%,-60%)}to{opacity:1;transform:translate(-50%,-50%)}}@keyframes fadepop{0%{opacity:0}to{opacity:1}}.bw-joy{position:absolute;width:88px;height:88px;border-radius:50%;border:2px solid rgba(160,200,255,.35);background:#0a142859;pointer-events:none;z-index:5}.bw-joy-knob{position:absolute;left:50%;top:50%;width:34px;height:34px;margin:-17px;border-radius:50%;background:#a0c8ff8c}@media(max-width:640px){#app{min-height:420px}.hud{gap:8px}.bubble{max-width:160px;font-size:13px}}.hud .scoop .big{color:#ffd04f}.hud .statsbtn{cursor:pointer;pointer-events:auto}.hud .statsbtn .lbl{color:#9fd4ff}.stats-panel{position:absolute;right:14px;top:64px;width:min(320px,70vw);max-height:62vh;overflow:auto;background:#081020e0;border:1px solid rgba(160,200,255,.25);border-radius:12px;padding:12px 14px;z-index:6;pointer-events:auto}.stats-panel h3{margin:0 0 8px;font-size:13px;letter-spacing:.14em;color:#9fd4ff}.slist{display:flex;flex-direction:column;gap:3px}.srow{display:grid;grid-template-columns:18px 1fr auto;gap:6px;font-size:12px;align-items:center;padding:2px 4px;border-radius:6px}.srow--on{background:#8affb014;color:#8affb0}.sglyph{text-align:center}.sname{font-weight:700}.sval{font-weight:800;text-align:right}.shop{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;gap:12px;padding:24px 16px;overflow:auto;background:#060c1ad1;z-index:6;pointer-events:auto;animation:fadepop .3s ease}.shop h2{margin:0;font-size:clamp(20px,4vw,32px);letter-spacing:.08em}.shop-sub{color:#aac4e4;margin:0}.shop-scoop{font-size:15px;color:#ffd04f;font-weight:800}.shop-offers{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;max-width:620px}.offer{display:grid;grid-template-columns:auto 1fr;grid-template-rows:auto auto auto;gap:2px 10px;min-width:180px;max-width:250px;padding:12px 14px;border-radius:12px;background:#183056e6;border:1px solid rgba(160,200,255,.3);color:#eaf2ff;cursor:pointer;text-align:left;transition:transform .12s,background .12s}.offer:hover:not(:disabled){background:#284c7ef2;transform:translateY(-2px)}.offer:disabled{cursor:not-allowed;opacity:.45}.offer--maxed{border-color:#8affb066}.offer--poor{opacity:.6}.offer .og{font-size:22px;grid-row:1 / 4;align-self:center}.offer .oname{font-weight:800;font-size:13px}.offer .olevel{color:#9fd4ff;font-size:11px}.offer .odesc{color:#8fa8c2;font-size:11px}.offer .ocost{color:#ffd04f;font-weight:800;font-size:13px}.offer--rare{border-color:#ffc35ce6;background:#3c2a0c8c;box-shadow:0 0 12px #ffc35c2e}.offer--rare:hover:not(:disabled){background:#543c12f2}.offer .orare{font-style:normal;font-size:10px;font-weight:900;color:#160d00;background:#ffc35c;border-radius:4px;padding:1px 5px;margin-left:6px;letter-spacing:.08em}.offer .oplus{color:#8affb0;font-size:12px}.shop-stats{max-width:620px;width:100%;background:#08102099;border-radius:12px;padding:10px 14px;border:1px solid rgba(160,200,255,.16)}.shop-stats h3{margin:0 0 6px;font-size:12px;letter-spacing:.14em;color:#9fd4ff}.shop-stats .slist{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:2px 10px}.btn--big{font-size:18px;padding:14px 34px;margin-top:4px}.btn--small{font-size:12px;padding:6px 12px;margin:12px 0 6px;opacity:.92}.btn--small:disabled{opacity:.4;cursor:not-allowed}

.bw-indicator[data-kind=ghost],.indicator[data-kind=ghost]{border-left-color:#b7a6ff}
.indicator[data-kind=big]{border-left-color:#ff6b35;border-left-width:13px;border-top-width:8px;border-bottom-width:8px}
.indicator[data-kind=fast]{border-left-color:#ffd23f}
.indicator[data-kind=small]{border-left-color:#ff8a5c;border-left-width:9px}

/*bwshop1*/.shop-cta{display:flex;gap:10px;align-items:center;justify-content:center;flex-wrap:wrap;margin-top:2px}
.shop .btn--big{margin-top:0;display:inline-flex;align-items:center;gap:10px}
.shop kbd{display:inline-block;padding:2px 7px;border-radius:6px;border:1px solid rgba(160,200,255,.45);border-bottom-width:2px;background:#0d1e38;color:#d7e9ff;font:800 11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.06em;vertical-align:middle}
.shop .btn--big kbd{background:#0a1729;font-size:12px}
.shop-keys{margin:0;font-size:12.5px;color:#8fadcf;display:flex;gap:5px;align-items:center;flex-wrap:wrap;justify-content:center}
.shop-stats{background:none;border:0;padding:4px 0 0;max-width:560px;width:100%;text-align:center}
.shop-stats h3{font-size:11px;letter-spacing:.18em;color:#7f9dc2;margin:0 0 7px;font-weight:800}
.ownrow{display:flex;flex-wrap:wrap;gap:6px;justify-content:center}
.own{display:inline-flex;gap:6px;align-items:center;padding:4px 10px;border-radius:999px;background:#10233c;border:1px solid rgba(160,200,255,.2);font-size:12px;font-weight:700;color:#dbe9ff}
.own i{font-style:normal;color:#8affb0}
.shop-empty{margin:0;font-size:12.5px;color:#8fadcf}
.shop h2{margin-bottom:0}
.shop .offer{position:relative}
.okey{position:absolute;top:-7px;right:-7px;display:grid;place-items:center;width:20px;height:20px;border-radius:7px;background:#0d1e38;border:1px solid rgba(160,200,255,.5);color:#d7e9ff;font:800 11px/1 ui-monospace,SFMono-Regular,Menlo,monospace;box-shadow:0 2px 6px #0006}
.offer--maxed .okey{opacity:.35}

/*bwbrot1*/.shop-offers{gap:12px;max-width:700px}
.offer{min-width:212px;max-width:252px;padding:13px 15px 12px;row-gap:1px}
.offer .og{font-size:24px}
.offer .oname{font-size:13.5px;line-height:1.2}
.otier{font-size:9.5px;font-weight:900;letter-spacing:.17em;color:#9fb6d4}
.odesc{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;font-size:11.5px;line-height:1.35;color:#a6c1e0;margin-top:2px}
.ocost{font-size:14px;margin-top:3px}
/* tier 0 COMMON keeps the plain steel card */
.offer--t1{border-color:rgba(84,176,255,.7);background:#12294de6;box-shadow:0 0 16px #54b0ff22}
.offer--t1 .otier{color:#7cc6ff}
.offer--t2{border-color:rgba(180,124,255,.75);background:#1d1840e6;box-shadow:0 0 20px #b47cff2e}
.offer--t2 .otier{color:#c99cff}
.offer--t2 .ocost{color:#e0c2ff}
.offer--t3{border-color:rgba(255,200,74,.85);background:#2a2010e6;box-shadow:0 0 24px #ffc84a33}
.offer--t3 .otier{color:#ffd77a}
.offer--t3 .ocost{color:#ffd77a}
.offer--heal{border-color:rgba(255,143,107,.75);background:#29161099;box-shadow:0 0 18px #ff8f6b26}
.offer--heal .otier{color:#ffb08e}
.offer--poor{opacity:.5;box-shadow:none}
/*bwthick1*/
body.bw-hidden::before{content:"";position:fixed;inset:0;pointer-events:none;z-index:38;
  box-shadow:inset 0 0 170px 34px rgba(26,84,42,.5);animation:bwbush .2s ease-out}
body.bw-hidden::after{content:"\1F343 LYING LOW";position:fixed;left:50%;bottom:88px;transform:translateX(-50%);
  padding:6px 14px;border-radius:999px;font:700 12px/1 ui-monospace,SFMono-Regular,Menlo,monospace;
  letter-spacing:.14em;color:#c9f2c9;background:rgba(13,31,18,.74);border:1px solid rgba(120,220,140,.45);
  box-shadow:0 0 22px rgba(60,180,90,.22);pointer-events:none;z-index:39;animation:bwbush .18s ease-out}
@keyframes bwbush{from{opacity:0}to{opacity:1}}

/*bwlby1: lay-by. one card held for later, allowed to look precious about it.*/
.shop-offers .owrap{display:flex;flex-direction:column;gap:5px;min-width:0}
.shop-offers .owrap>.offer{width:100%;max-width:210px}
.offer--locked{border-color:#ffd35c!important;box-shadow:0 0 0 1px #ffd35c55,0 8px 22px #00000099;position:relative}
.offer--locked::after{content:"🔒";position:absolute;top:6px;right:8px;font-size:11px;opacity:.9}
.olb{appearance:none;background:#0e1a2bcc;border:1px solid #ffffff22;color:#cfe0ff;font:inherit;font-size:10.5px;
     letter-spacing:.06em;text-transform:uppercase;padding:5px 8px;border-radius:8px;cursor:pointer}
.olb:hover:not(:disabled){border-color:#ffd35c99;color:#ffe9ad}
.olb:disabled{opacity:.25;cursor:not-allowed}
/*bwlby1: lay-by. one card held for later, allowed to look precious about it.*/
.shop-offers .owrap{display:flex;flex-direction:column;gap:5px;min-width:0}
.shop-offers .owrap>.offer{width:100%;max-width:210px}
.offer--locked{border-color:#ffd35c!important;box-shadow:0 0 0 1px #ffd35c55,0 8px 22px #00000099;position:relative}
.offer--locked::after{content:"🔒";position:absolute;top:6px;right:8px;font-size:11px;opacity:.9}
.olb{appearance:none;background:#0e1a2bcc;border:1px solid #ffffff22;color:#cfe0ff;font:inherit;font-size:10.5px;
     letter-spacing:.06em;text-transform:uppercase;padding:5px 8px;border-radius:8px;cursor:pointer}
.olb:hover:not(:disabled){border-color:#ffd35c99;color:#ffe9ad}
.olb:disabled{opacity:.25;cursor:not-allowed}

/*bwlby2: the wallet line — DUBS is the number that matters, SCOOP is a footnote.*/
.shop-scoop i{margin-left:12px;font-style:normal;opacity:.5;font-size:11px;letter-spacing:.08em}
/*bwnope1*/
/* an offer you cannot reach is not an offer that is dead: full colour, warm
   price, and a shrug when you poke it. */
.offer--pricey .ocost { color: #ffcaa6; }
@keyframes bwNope { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-6px) rotate(-1.2deg); } 45% { transform: translateX(6px) rotate(1.2deg); } 70% { transform: translateX(-3px); } }
.offer--nope { animation: bwNope .42s ease-out; border-color: #ff7a6b !important; box-shadow: 0 0 0 2px rgba(255,90,70,.35), 0 8px 20px rgba(0,0,0,.45) !important; }
/*bwlby1*/
/* a card held for later, allowed to look precious about it */
.shop-offers .owrap{display:flex;flex-direction:column;gap:5px;min-width:0}
.shop-offers .owrap>.offer{width:100%;max-width:210px}
.offer--locked{border-color:#ffd35c!important;box-shadow:0 0 0 1px #ffd35c55,0 8px 22px #00000099;position:relative}
.offer--locked::after{content:"🔒";position:absolute;top:6px;right:8px;font-size:11px;opacity:.9}
.olb{appearance:none;background:#0e1a2bcc;border:1px solid #ffffff22;color:#cfe0ff;font:inherit;font-size:10.5px;
     letter-spacing:.06em;text-transform:uppercase;padding:5px 8px;border-radius:8px;cursor:pointer}
.olb:hover:not(:disabled){border-color:#ffd35c99;color:#ffe9ad}
.olb:disabled{opacity:.25;cursor:not-allowed}

/*bwlby2: the wallet line — DUBS is the number that matters, SCOOP is a footnote.*/
.shop-scoop i{margin-left:12px;font-style:normal;opacity:.5;font-size:11px;letter-spacing:.08em}

/*bwshop2*/
/* ─────────────────────────────────────────────────────────────────────────────
   THE COSMIC SHOP INTERIOR — DOM layer.

   APPEND-ONLY. Everything here is either a brand new selector or a later
   override that wins the cascade; no existing rule is edited, because several
   selectors in this file (`.shop-offers .owrap`, `.offer--locked`, `#app`) are
   already duplicated two and three times by earlier hand-appended blocks and
   rewriting one would silently rewrite all of them.

   The three layout facts this whole block hangs off, verified against the
   rules above rather than assumed:
     · #app is position:relative; height:100vh; overflow:hidden — so NOTHING
       inside it can add document scroll, and nothing here needs position:fixed
       (which would also escape the clip and re-introduce that bug).
     · #ui is inset:0 with z-index:5, so it is a STACKING CONTEXT: no descendant
       of #ui can ever paint above `.indicators` (z-index:6, a sibling). The
       nav arrow therefore lives inside #ui and competes only with `.shop` (6).
     · `.shop` used to be inset:0 — a full-viewport panel at 82% opacity. That
       is the entire reason an interior is impossible without this block: the
       keeper and the room would be painted behind it. Docking it right is the
       one change that makes the whole feature visible.
   ───────────────────────────────────────────────────────────────────────────── */


/* ── 1. dock the panel right, so the left of the frame is the room ───────────
   Height is 100% (of #ui, which is inset:0) and never vh: 100vh here would be
   the viewport, not the clipped layer, and would reintroduce growth. Width uses
   vw only inside the absolutely-positioned, clipped layer, exactly as specified,
   and caps at 470px so a 1440px screen does not lose half its planet.
   `overscroll-behavior:contain` is not decoration: the panel is the one element
   here that scrolls, and without this a flick at the end of the offer list
   chains to the document on touchpads. The panel keeps overflow:auto from the
   rule above (4 cards at 900x600 genuinely do not fit) and pointer-events:auto
   because it holds the only clickable controls in the game. */
.shop{position:absolute;inset:auto 0 0 auto;width:min(470px,45vw);height:100%;max-height:100%;
  /* border-box, and it matters: nothing in this stylesheet sets box-sizing, so the
     authored content-box model made this dock paint 507px on a 1440px screen (470 +
     36 padding + 1 border) and eat 35vw of planet instead of 32. Measured, not assumed. */
  box-sizing:border-box;
  overflow:auto;overscroll-behavior:contain;pointer-events:auto;
  /* a wall, not a scrim: the interior should read as being on the other side of
     a counter, and a flat edge on the left sells that for free */
  padding:20px 18px 26px;gap:10px;
  background:linear-gradient(255deg,#0b0a1af2 0%,#150f2be6 68%,#1c1230d9 100%);
  border-left:1px solid rgba(255,190,90,.34);
  box-shadow:-18px 0 40px rgba(0,0,0,.5);
  animation:fadepop .3s}
/* The offer row was authored for a 700px centred column; inside a 405px dock it
   is the one child that could push a sideways scrollbar, so it is bounded to the
   panel instead. Cards keep their own max-width and simply wrap to one column. */
.shop .shop-offers{width:100%;max-width:100%}
.shop .shop-stats{width:100%;max-width:100%}
/* The hint row must survive intact (the keyboard path resolves nothing by CSS,
   but a row that clips would make the shop look broken): let it breathe on two
   lines inside a narrow dock rather than overflow. */
.shop .shop-keys{width:100%;max-width:100%;row-gap:4px;line-height:1.6}
/* Long stat descriptions and the "SOLD OUT" cost line are the other unbounded
   strings in here; clamp rather than let a 900px dock grow a horizontal bar. */
.shop .offer .odesc,.shop .shop-scoop{max-width:100%;overflow-wrap:anywhere}
.shop-scoop{display:flex;flex-wrap:wrap;gap:2px 8px;max-width:100%}
/* The header is the shop's face now that it is a place: warm, lit from below. */
.shop h2{color:#ffd77a;text-shadow:0 0 14px rgba(255,180,61,.45);max-width:100%}


/* ── 2. the way to the shop ──────────────────────────────────────────────────
   display:none by default and `.bw-nav--on` to show it, as specified, so a stale
   node can never haunt the title screen. `#ui:has(.shop)` is the belt to that
   brace: while the panel is open the arrow is impossible in CSS, whatever the JS
   class says. Where :has() is unsupported the rule is simply dropped and the JS
   class still governs it — it degrades to the specified behaviour, not to broken.
   z-index 5 puts it above `.hud` (auto) and below `.shop` (6), which is the
   order wanted: the panel is never something you navigate TO any more. */
#bw-nav{display:none;position:absolute;left:0;top:0;width:0;height:0;z-index:5;
  pointer-events:none;will-change:transform}
#bw-nav.bw-nav--on{display:block}
#ui:has(.shop) #bw-nav{display:none}
/* The chevron is a child, not a pseudo, and it is centred on the anchor point
   (16x22 box at -8/-11) because #bw-nav is 0x0: JS writes
   translate(-50%,-50%) rotate(Nrad) on the PARENT, so the parent's translate is a
   no-op and rotation happens about this origin. Anything that animated
   #bw-nav's transform would fight that write every frame; pulsing the child's
   opacity instead costs nothing and reads as an LED. */
#bw-nav .bw-nav__chev{position:absolute;left:-8px;top:-11px;width:0;height:0;
  border-left:16px solid #ffd35c;border-top:11px solid transparent;border-bottom:11px solid transparent;
  filter:drop-shadow(0 0 6px rgba(255,180,70,.9));
  animation:hintPulse 1.1s ease-in-out infinite}
/* Both labels are pseudos of #bw-nav because that is where data-dist lives
   (data-arrow is for rigs; the transform, not CSS, does the pointing). They are
   COUNTER-ROTATED by --bw-nav-rot, which JS must set alongside the rotate() it
   already writes:
       nav.style.setProperty('--bw-nav-rot', th.toFixed(3) + 'rad')
   Without that line the labels simply tilt with the arrow — visible, readable,
   slightly daft — rather than disappearing. */
#bw-nav::before,#bw-nav::after{position:absolute;left:0;top:0;pointer-events:none;
  transform:rotate(calc(-1 * var(--bw-nav-rot,0rad)));
  white-space:nowrap;max-width:40vw;overflow:hidden;text-overflow:ellipsis;
  text-shadow:0 2px 8px rgba(0,0,0,.85);font-weight:800;letter-spacing:.1em}
/* the word, so there is no guessing what the arrow is for */
#bw-nav::before{content:"cosmic shop";left:16px;top:-4px;font-size:11px;color:#ffd77a;text-transform:uppercase}
/* the distance, in the game's voice, only once JS has actually put a number
   there — otherwise it would read "u out" */
#bw-nav[data-dist]::after{content:attr(data-dist) "u out";left:16px;top:10px;font-size:10.5px;color:#9fd4ff;font-weight:700}


/* ── 3. the shopkeeper talks ─────────────────────────────────────────────────
   He is NOT in #bubbles. That layer is z-index:4, below #ui, so anything in it
   is painted behind the shop panel — he would mouth silently for the whole
   shopping phase. It is also one-bubble-global with a hard 3.2s life, which is
   an enemy courtesy, not a shopkeeper. So: his own node, in #ui, z-index 7.
   No width is set on purpose: an absolutely-positioned box with only a max-width
   shrinks to its content, which is what keeps the typing caret at the end of the
   sentence instead of drifting to the right of an empty box. */
.bw-keeper{position:absolute;left:0;top:0;z-index:7;pointer-events:none;
  /* same reason as the dock: max-width is the box the keeper is allowed to occupy,
     so the padding has to be inside that number or clearance is a lie */
  box-sizing:border-box;
  max-width:min(330px,44vw);padding:8px 13px 10px;border-radius:13px;
  background:#130d24ef;border:1px solid rgba(255,190,90,.42);
  box-shadow:0 10px 26px rgba(0,0,0,.55);
  font-size:13.5px;font-weight:600;line-height:1.3;color:#ffeccd;text-align:left;
  transition:opacity .25s ease-out}
/* The panel is projected onto the keeper's HEAD, so the tail has to leave from
   the bottom and point down-left at him. It is a rotated square carrying two
   borders, which is the only way to get a tail with the panel's rounded border
   without a clip-path or a background image (both banned here: no url(), no
   filters on ancestors of the canvas). */
.bw-keeper::before{content:"";position:absolute;left:20px;bottom:-6px;width:11px;height:11px;
  background:#130d24;border-right:1px solid rgba(255,190,90,.42);border-bottom:1px solid rgba(255,190,90,.42);
  transform:rotate(45deg)}
/* Opacity only, never transform: projectWorld rewrites style.transform every
   frame, and transitioning it would lag the keeper's head around the screen. */
.bw-keeper.bw-keeper--out{opacity:0}
/* the plate: a name is the difference between a mechanic and a guy.
   JS sets data-name on this element and leaves its textContent empty — CSS
   cannot branch on "attribute absent, use text instead" without duplicating. */
.bw-keeper .bw-keeper__name{display:block;margin:-2px 0 4px;font-size:9.5px;font-weight:800;
  letter-spacing:.2em;text-transform:uppercase;color:#ffb43d;
  max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bw-keeper .bw-keeper__name::before{content:attr(data-name)}
/* Four lines is the longest line anybody reads while enemies are spawning.
   line-clamp is the mechanism that actually does multi-line ellipsis;
   text-overflow:ellipsis is kept because that is the property that renders it. */
.bw-keeper .bw-keeper__text{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;
  line-clamp:4;overflow:hidden;text-overflow:ellipsis;overflow-wrap:anywhere}
/* the caret, on the panel, blinking with the HUD's EXISTING hintPulse so it is
   the same pulse as "PRESS 1-3 TO SWITCH" and no new keyframes enter the file.
   Because the box shrinks to its content, bottom-right is the end of the
   sentence for the one- and two-line cases that are almost all Barry says. */
.bw-keeper.bw-keeper--typing::after{content:"▌";position:absolute;right:9px;bottom:8px;
  font-size:12px;line-height:1;color:#ffd35c;
  /* one glyph, and the box says so: a caret that could grow would push the panel wider
     mid-sentence and make the text reflow under the reader's eye */
  max-width:1.2em;overflow:hidden;
  animation:hintPulse 1.4s ease-in-out infinite}
/* While the panel is docked the keeper must not stand behind it. 42vw is the
   worst case at which a centred anchor still clears a 45vw dock; 36vw keeps the
   panel's own box inside the room even at 900px wide. */
#ui:has(.shop) .bw-keeper{max-width:min(300px,36vw)}


/* ── 4. the sign's DOM label ─────────────────────────────────────────────────
   For the words on the giant sign IF a DOM label is used instead of (or over)
   the CanvasTexture. Glow is text-shadow only: filter/backdrop-filter on this or
   any ancestor would promote a layer over the WebGL canvas and force it to
   composite on the main thread — the one way a CSS class can cost the game
   frames. The pulse is opacity, which the compositor does for free. */
.bw-sign-glow{color:#fff3c4;letter-spacing:.14em;font-weight:900;text-transform:uppercase;
  text-shadow:0 0 6px rgba(255,214,102,.95),0 0 16px rgba(255,168,42,.6),0 0 34px rgba(255,120,20,.35);
  max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bw-sign-glow.bw-sign-glow--led{animation:hintPulse 1.1s ease-in-out infinite}
/* an offer the sign is advertising, when the sign names it: still bounded */
.bw-sign-glow .bw-sign-glow__item{font-weight:700;color:#ffd77a;text-shadow:0 0 10px rgba(255,180,61,.8)}


/* ── 5. reduced motion ───────────────────────────────────────────────────────
   Turns OFF the pulses and the caret blink — and explicitly restores opacity to
   1, because hintPulse animates opacity and killing the animation alone would
   strand an element at whatever the last painted keyframe was. Nothing here sets
   display or visibility: reduced motion must cost nobody any text. */
@media (prefers-reduced-motion: reduce){
  #bw-nav .bw-nav__chev,
  .bw-keeper.bw-keeper--typing::after,
  .bw-sign-glow.bw-sign-glow--led{animation:none;opacity:1}
  /* the dock keeps its shape and simply stops sliding in */
  .shop{animation:none}
  .bw-keeper{transition:none}
  .bw-keeper.bw-keeper--out{opacity:0}
}
