/*
  Baller World — presentation stylesheet (readable, safe to edit).

  Pairs with intro.js. The blocks, in the order they appear:
    0. :root            — the wrapper's type scale. Every type size in this file is one of
                          these four steps; nothing here picks a type size off-scale.
    1. .ai-bw-splash    — the opening key-art card (added by intro.js).
    2. .ai-bw-title     — the engine's own title overlay, once intro.js has marked it. The
                          key art IS the title now: it is shown sharp and un-cropped, and
                          the engine's plain H1 is taken out of the picture. PAUSED and the
                          results screen are NOT marked, so they stay plain and stay readable.
    3-7. payout receipt, phone banner, shop rise, dock fit, phone sheet.
    8. reduced motion, LAST, so it actually wins the cascade.
    9. .hud             — the readout strip: one scale, one line wherever it can be one.

  Loaded after the shipped CSS on purpose: it is an override layer. The engine's buttons keep
  their own ids and handlers, and nothing here starts, pauses or advances the game.
*/

/* =========================================================
   0. the wrapper's type scale
   ========================================================= */

/*
  Four steps, and they are the whole scale. Before this existed the wrapper's own text — and the
  title-card lines it restyles — ran 8, 9, 9.5, 10, 10.5, 11, 11.5, 12, 13, 15, 16, 17, 19, 20 and
  24px: a different size for almost every rule, which is why the title card, the payout receipt
  and the HUD never read as one interface. Each step has one job:

    --bw-t1  10px   MICRO.  mono labels, key chips, build stamps, control hints.
    --bw-t2  12px   SMALL.  secondary copy, status chips, long-form values (a wave name).
    --bw-t3  15px   BASE.   primary readouts, button labels, the one line of title copy.
    --bw-d1  20px   DISPLAY. the two hero numerals only: payout multiplier, T5 countdown.

  10px is the floor: the engine's own HUD already ships 10.5-11px mono chips and this is a
  game read at arm's length, so nothing here goes below it. Each step is about 1.2x the last,
  which is why 15 and 20 read as the same family as 10 and 12.

  Scope, said plainly: this scale governs the text the wrapper itself draws -- the splash, the
  title card, the HUD, the payout receipt, the T5 telegraph, the counter labels. The shop
  panel's own type is NOT here: bwshopfx.js sets it from its own runtime sheet (SHOPFX-010),
  at every width, and the phone-sheet sizes in section 7 are superseded by it.
*/
:root {
  --bw-t1: 10px;
  --bw-t2: 12px;
  --bw-t3: 15px;
  --bw-d1: 20px;
}

/* =========================================================
   1. the opening splash
   ========================================================= */

.ai-bw-splash {
  position: fixed;
  inset: 0;
  z-index: 2147483000;
  margin: 0;
  background: #04070f;
  overflow: hidden;
  cursor: pointer;
  pointer-events: auto;
  opacity: 1;
  transition: opacity 0.52s ease, transform 0.52s ease;
}

/* the art is authored at 1672x941 (1.78:1) with its wordmark, tagline and side copy baked
   in, so it is shown as the hero and nothing is stacked on it but a scrim and one line.
   A blurred bleed fills the frame; the art itself is contain-fitted on wide screens so the
   artist's side copy is never cropped, and cover-fitted in portrait where the logo band
   matters more than the margins. */
.ai-bw-splash__bleed {
  position: absolute;
  inset: -10%;
  background-image: var(--aibw-art, none);
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center 42%;
  filter: blur(38px) saturate(1.15) brightness(0.4);
}

.ai-bw-splash__art {
  position: absolute;
  inset: 0;
  background-image: var(--aibw-art, none);
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center 42%;
  transform: scale(1.06);
  animation: aibw-pushin 2.9s cubic-bezier(0.16, 0.72, 0.24, 1) forwards;
  will-change: transform;
}

/* wide enough that cover would crop the art: show all of it instead */
@media (min-aspect-ratio: 1.6) {
  .ai-bw-splash__art {
    background-size: contain;
  }
}

@keyframes aibw-pushin {
  from { transform: scale(1.06) translateY(0.6%); }
  to   { transform: scale(1)    translateY(0); }
}

.ai-bw-splash__scrim {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse at 50% 45%, rgba(4, 7, 15, 0) 42%, rgba(4, 7, 15, 0.55) 100%),
    linear-gradient(to top, rgba(4, 7, 15, 0.85) 0%, rgba(4, 7, 15, 0) 34%);
  animation: aibw-scrim 2.9s ease forwards;
}

@keyframes aibw-scrim {
  from { opacity: 1; }
  to   { opacity: 0.72; }
}

.ai-bw-splash__foot {
  position: absolute;
  left: clamp(14px, 4vw, 40px);
  right: clamp(14px, 4vw, 40px);
  bottom: clamp(14px, 4vh, 34px);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}

.ai-bw-kicker {
  color: #d7f2ff;
  font-size: var(--bw-t1);
  letter-spacing: 0.34em;
  text-transform: uppercase;
  text-shadow: 0 0 18px rgba(88, 198, 255, 0.55), 0 1px 0 rgba(0, 0, 0, 0.6);
}

.ai-bw-hint {
  color: rgba(215, 242, 255, 0.62);
  font-size: var(--bw-t1);
  letter-spacing: 0.22em;
  text-transform: uppercase;
}

.ai-bw-splash.is-out {
  opacity: 0;
  transform: scale(1.03);
  pointer-events: none;
}

/* =========================================================
   2. the title card (the engine's own .menu, marked by intro.js)
   ========================================================= */

/*
  THE POSTER IS THE TITLE.

  art/title-wide.jpg is a finished 1600x900 illustrated poster: its own BALLER WORLD wordmark,
  the meme cast, the tagline copy and the billing lines, all already lettered in. It is the
  single best asset the game owns, and until now it sat behind a 17px blur, pushed up and out
  of frame. The reason is in the old comment: the painted wordmark would compete with the
  engine's plain ui-sans-serif H1. That is a real conflict with the wrong winner. Two titles in
  one frame is one title too many, so the engine's H1 goes (see below) and the painted one is
  promoted to the job it was drawn for.

  FRAMING IS THE SPLASH'S FRAMING, on purpose. `contain`, not `cover`: at 960x600 cover slices
  5.6% off each side, which cuts Pepe's beanie and clips the "skill issue" bubble (measured,
  shots/cover960.png). `contain` costs a 25px band top and bottom instead, and the blurred
  bleed of the same art already fills those. Same 42% optical centre, same art, same size, so
  the splash dissolves into a title card that is the same picture in the same place -- no zoom
  and no jump at the handover.

  THE COLUMN SITS IN THE BOTTOM THIRD, not the centre. Centred, the tagline and the buttons
  land on the poster's subject -- the character, mid-sprint, on the planet -- and a scrim heavy
  enough to keep them readable there would have to bury him. Anchored to the bottom they sit on
  the clouds and the poster's own billing lines, which is where a poster puts its billing block
  anyway, and the whole middle of the art stays clear.
*/
.menu.ai-bw-title {
  /* the engine ships this overlay with no gutter at all, which puts its own control list
     flush against both edges of a phone. Give it a real margin to breathe in. */
  padding: 0 clamp(18px, 5vw, 48px) clamp(30px, 5vh, 52px);
  /* flat fallback: if the art ever fails to load, the card is still dark and still readable */
  background: #04070f;
  justify-content: flex-end;
  gap: 14px;
}

/* THE BLEED: the same art, hard-blurred and darkened, filling whatever `contain` leaves over.
   The slow drift lives HERE and only here. The sharp layer is exactly the viewport at 16:9, so
   moving that one would open a gap at the frame edge; this one is inset -10% and has slack. */
.menu.ai-bw-title::before {
  content: "";
  position: absolute;
  inset: -10%;
  background-image: url("./art/title-wide.jpg");
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center 42%;
  filter: blur(38px) saturate(1.15) brightness(0.4);
  pointer-events: none;
  animation: aibw-drift 26s ease-in-out infinite alternate;
}

@keyframes aibw-drift {
  from { transform: scale(1.04) translate3d(-0.6%, 0, 0); }
  to   { transform: scale(1.09) translate3d(0.6%, -0.8%, 0); }
}

/* THE POSTER, sharp, with the scrim as the top background layer of the same box so the two can
   never drift apart or land a frame out of step. Three layers, painted top-down: vignette,
   scrim, art. */
.menu.ai-bw-title::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    radial-gradient(ellipse at 50% 118%, rgba(4, 7, 15, 0.55), rgba(4, 7, 15, 0) 62%),
    linear-gradient(to bottom, rgba(4, 7, 15, 0.4) 0%, rgba(4, 7, 15, 0) 8%),
    linear-gradient(to top, rgba(4, 7, 15, 0.93) 0%, rgba(4, 7, 15, 0.84) 22%,
      rgba(4, 7, 15, 0.42) 46%, rgba(4, 7, 15, 0) 70%),
    url("./art/title-wide.jpg");
  background-repeat: no-repeat;
  background-size: auto, auto, auto, contain;
  background-position: center bottom, center top, center bottom, center 42%;
  pointer-events: none;
}

/* keep the engine's own children above the art and the scrim */
.menu.ai-bw-title > * {
  position: relative;
  z-index: 1;
}

/* THE ENGINE'S H1 IS TAKEN OUT OF THE PICTURE. It still ships the game's name, so it stays in
   the document and stays the heading a screen reader announces -- it is only made 1px and
   clipped, because the painted wordmark at the top of the frame is the title now. This is the
   "subordinate it" half of the fix; the alternative, keeping a second H1 on screen, is the
   defect being repaired. */
.menu.ai-bw-title h1 {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* the two pieces of type the title card still adds, both on the wrapper scale (section 0) */
.menu.ai-bw-title .sub {
  font-size: var(--bw-t3);
  color: rgba(246, 230, 196, 0.94);
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.9), 0 1px 3px rgba(0, 0, 0, 0.95);
}

.menu.ai-bw-title .controls {
  font-size: var(--bw-t2);
  color: rgba(216, 233, 250, 0.9);
  line-height: 1.65;
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.9);
}

.menu.ai-bw-title .tag {
  font-size: var(--bw-t2);
  color: rgba(200, 220, 240, 0.72);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.85);
}

.ai-bw-stamp {
  position: absolute;
  left: clamp(12px, 3.5vw, 34px);
  bottom: clamp(10px, 2.4vh, 20px);
  z-index: 1;
  max-width: 60vw;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: var(--bw-t1);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(215, 242, 255, 0.55);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.7);
  pointer-events: none;
  text-align: left;
}

/* on a phone the engine's own "no maps. no edges." line sits right above the build stamp */
@media (max-width: 560px) {
  .ai-bw-stamp {
    max-width: calc(100% - 24px);
    letter-spacing: 0.1em;
  }
}

/* the buttons: glass pills, cyan edge, so the entry point is obviously the entry point */
.menu .btn {
  border: 1px solid rgba(88, 198, 255, 0.5);
  background: linear-gradient(180deg, rgba(14, 32, 58, 0.82), rgba(6, 12, 26, 0.86));
  box-shadow: 0 0 0 1px rgba(4, 7, 15, 0.6), 0 10px 30px rgba(4, 7, 15, 0.45),
    inset 0 1px 0 rgba(215, 242, 255, 0.14);
  /* Safari needs the prefixed copy or the glass simply does not happen */
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  /* phone ergonomics: no grey flash on tap, no 300ms double-tap-zoom hesitation */
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.14s ease, border-color 0.14s ease, box-shadow 0.14s ease;
}

.menu .btn:hover,
.menu .btn:focus-visible {
  border-color: rgba(150, 226, 255, 0.95);
  transform: translateY(-2px);
  box-shadow: 0 0 24px rgba(88, 198, 255, 0.35), 0 12px 34px rgba(4, 7, 15, 0.5),
    inset 0 1px 0 rgba(215, 242, 255, 0.2);
  outline: none;
}

.menu .btn:active {
  transform: translateY(0);
}

/* small screens: the art is a landscape poster, so favour the logo band in the middle */
@media (max-aspect-ratio: 4/5) {
  /* portrait: cover-fit crops a 1.78:1 poster to a centre strip, which sliced the artist's
     wordmark into "BallerW...". The wordmark is a WIDE element, so it must keep the full
     width -- which means the art can only ever be a band here. Unframed, a band in a dark
     0.46:1 frame reads as a broken page rather than a title card. So: stop stretching it to
     fill, and compose it instead. A bright enlarged copy of the same art fills the screen,
     and the art itself is drawn as a framed card with a rim glow at the optical middle. */
  .ai-bw-splash__bleed {
    filter: blur(26px) saturate(1.22) brightness(0.62);
  }

  .ai-bw-splash__art {
    background-image: none;
    transform: none;
    animation: none;
  }

  .ai-bw-splash__art::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 42%;
    transform: translateY(-50%) scale(1.05);
    height: 56.27vw; /* 941/1672 of the full width: the whole wordmark, never cropped */
    max-height: 100%;
    background-image: var(--aibw-art, none);
    background-repeat: no-repeat;
    background-size: contain;
    background-position: center center;
    border-radius: 14px;
    outline: 1px solid rgba(215, 242, 255, 0.18);
    outline-offset: -1px;
    box-shadow: 0 0 54px rgba(88, 198, 255, 0.22), 0 24px 70px rgba(4, 7, 15, 0.7);
    animation: aibw-cardin 2.4s cubic-bezier(0.16, 0.72, 0.24, 1) forwards;
  }

  @keyframes aibw-cardin {
    from { transform: translateY(-50%) scale(1.1); opacity: 0.6; }
    to   { transform: translateY(-50%) scale(1);   opacity: 1; }
  }

  .ai-bw-splash__scrim {
    /* darken only the bands the card does not occupy, so the art itself is not dimmed */
    background: linear-gradient(to bottom, rgba(4, 7, 15, 0.66) 0%, rgba(4, 7, 15, 0) 30%,
      rgba(4, 7, 15, 0) 66%, rgba(4, 7, 15, 0.78) 100%);
  }

  /* portrait: `contain` already keeps the whole 1.78:1 poster, wordmark included, so the only
     thing to tune is where that band sits. Lift it above the middle, because the menu column is
     anchored to the bottom third here and the two must not meet. */
  .menu.ai-bw-title::after {
    background-position: center bottom, center top, center bottom, center 36%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ai-bw-splash__art,
  .ai-bw-splash__art::after,
  .ai-bw-splash__scrim,
  .menu.ai-bw-title::before,
  .ai-bw-pay {
    animation: none;
  }
  /* the rules that live LATER in this file (payout, banner, shop) are neutralised by the
     final block at the bottom, where they can actually win the cascade */
  .ai-bw-splash,
  .ai-bw-splash__scrim,
  .menu .btn,
  .ai-bw-pay {
    transition: none;
  }
  .ai-bw-splash__art { transform: scale(1); }
}

/* =========================================================
   3. the payout receipt (rendered by intro.js from state.payout)
   ========================================================= */

/*
  Bottom-LEFT and above the toast line on purpose: .toast owns bottom-centre, .banner owns the
  21% band (and a level-up banner fires seconds after a payout), and .shop is a full-screen
  panel at z-index 6. This sits at z-index 4 with pointer-events off, so the panel wins any race
  and the card can never eat a click meant for the game.
*/
.ai-bw-pay {
  position: absolute;
  left: clamp(12px, 3.5vw, 30px);
  bottom: clamp(54px, 9vh, 74px);
  z-index: 4;
  display: grid;
  gap: 5px;
  max-width: min(74vw, 330px);
  padding: 10px 14px 11px;
  border-radius: 12px;
  border: 1px solid rgba(88, 198, 255, 0.45);
  background: linear-gradient(180deg, rgba(14, 32, 58, 0.86), rgba(6, 12, 26, 0.9));
  box-shadow: 0 12px 34px rgba(4, 7, 15, 0.55), inset 0 1px 0 rgba(215, 242, 255, 0.14),
    0 0 22px rgba(88, 198, 255, 0.18);
  -webkit-backdrop-filter: blur(7px);
  backdrop-filter: blur(7px);
  pointer-events: none;
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.42s ease, transform 0.42s ease;
  animation: aibw-payin 0.34s cubic-bezier(0.16, 0.72, 0.24, 1);
}

@keyframes aibw-payin {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}

.ai-bw-pay.is-out {
  opacity: 0;
  transform: translateY(-6px);
}

.ai-bw-pay__title {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.ai-bw-pay__head {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: var(--bw-t1);
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: #9fd8ff;
}

.ai-bw-pay__mult {
  font-size: var(--bw-d1);
  font-weight: 800;
  letter-spacing: 0.04em;
  color: #f6e6c4;
  text-shadow: 0 0 18px rgba(246, 230, 196, 0.35), 0 1px 0 rgba(0, 0, 0, 0.6);
}

.ai-bw-pay__parts {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
}

.ai-bw-pay__part {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: var(--bw-t1);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(215, 242, 255, 0.82);
  white-space: nowrap;
}

.ai-bw-pay__net {
  font-size: var(--bw-t2);
  font-weight: 700;
  letter-spacing: 0.06em;
  color: #8affb0;
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.7);
}

@media (max-width: 560px) {
  /* shrink to the content instead of stretching edge to edge: a full-width panel pushed
     "PAYOUT" to one edge and "x1.65" to the other and let the bonuses wrap raggedly. A
     receipt is a short left-aligned list, so stack the bonuses and let the card be narrow. */
  .ai-bw-pay {
    left: 12px;
    right: auto;
    max-width: calc(100vw - 24px);
    bottom: 60px;
  }
  .ai-bw-pay__parts {
    flex-direction: column;
    gap: 2px;
  }
}

/* =========================================================
   4. the engine's own announcements, landing below its own HUD
   ========================================================= */

/*
  Only on narrow screens, because desktop is already correct (measured: HUD ends at 90px and
  the banner starts at 152px -- zero overlap -- and this block is what keeps it that way).
  46px of clearance is the banner's own half-height plus a gutter, so a one-line announcement
  sits just under the chips instead of behind them. --aibw-hud-bottom is published by intro.js;
  if it never arrives the fallback is 0px + 46px, which is still below a phone HUD's first row
  and never worse than a fixed 21% inside the chips.
*/
@media (max-width: 620px) {
  #ui .banner {
    /* Anchor by the TOP edge, not the centre. The engine centres the banner on `top` with
       translate(-50%,-50%), so clearing the HUD by 46px still let a five-line announcement
       reach 19px back up behind the chips (measured: banner top 238 against a HUD that ends
       at 257). Here `top` literally is the top edge, so clearance holds at any text length.
       Own keyframes, because the engine's fadeup keyframes carry their own -50% Y and would
       fight the anchor; .banner--out only touches opacity, so the exit still works. */
    top: calc(var(--aibw-hud-bottom, 0px) + 10px);
    transform: translate(-50%, 0);
    animation: aibw-banner-in 0.45s ease;
    max-width: min(88vw, 420px);
    font-size: var(--bw-t3);
    line-height: 1.35;
    letter-spacing: 0.08em;
    padding: 7px 13px;
    border-radius: 10px;
    background: rgba(6, 12, 26, 0.66);
    border: 1px solid rgba(88, 198, 255, 0.26);
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
    text-shadow: 0 1px 10px rgba(0, 0, 0, 0.85);
  }

  @keyframes aibw-banner-in {
    from { opacity: 0; transform: translate(-50%, 8px); }
    to   { opacity: 1; transform: translate(-50%, 0); }
  }

  #ui .banner--big {
    font-size: var(--bw-d1);
    letter-spacing: 0.12em;
  }
}

/* =========================================================
   5. the shop rises out of the ground and sinks back into it
   ========================================================= */

/*
  Entrance replaces the shipped `fadepop .3s`: the panel grows up from the bottom edge with a
  small overshoot and settles, so it reads as something arriving from under the planet rather
  than a screenshot fading in. Transform and opacity only -- nothing here shifts layout, and the
  panel's final resting rect is exactly the engine's own.

  The sink belongs to the clone intro.js keeps, not to the real panel: closeShop() deletes the
  real one on its own frame so the new planet is already drawing underneath, which is the reveal
  Jordi asked for. z-index 5 keeps the ghost under any shop that opens again (6), and it is
  click-transparent, so nothing can be blocked by an animation nobody waited for.
*/
/*
  0.36,0.28,0.50,0.70 was chosen by measurement, not by taste. Sampled by pausing the animation
  and reading its own transform: 208px below at 0, still 85px below at 110ms (you SEE it come
  up), 26px ABOVE the line at 231ms (the overshoot), back to +11px at 328ms, exactly 0 at 420ms.
  The curve first shipped here was so front-loaded it was already 10px past the line at 110ms —
  a snap, not a burst.
*/
#ui .shop {
  transform-origin: 50% 100%;
  animation: aibw-shop-rise 0.42s cubic-bezier(0.36, 0.28, 0.50, 0.70);
}

@keyframes aibw-shop-rise {
  0%   { opacity: 0; transform: translateY(26%) scaleY(0.92); }
  55%  { opacity: 1; transform: translateY(-3.2%) scaleY(1.025); }
  78%  { transform: translateY(1.4%) scaleY(0.995); }
  100% { opacity: 1; transform: translateY(0) scaleY(1); }
}

#ui .shop.ai-bw-shop-sink {
  animation: aibw-shop-sink 0.3s ease-in forwards;
  transform-origin: 50% 100%;
  pointer-events: none;
  z-index: 5;
}

@keyframes aibw-shop-sink {
  from { opacity: 0.92; transform: translateY(0) scaleY(1); }
  to   { opacity: 0; transform: translateY(30%) scaleY(0.9); }
}

/* =========================================================
   6. the dock must never hide its own controls
   ========================================================= */

/*
  Measured on live (shop-fit-check, 2026-09-15) with the shelf at the maximum the generator can
  roll — bwShelf gives "three always, four when the feed is generous", a heal card fills a free
  4th slot, and a laid-by card SPLICES in on top, so five is the real ceiling:

    1280x800  4 offers  panel needs 878px in an 800px dock  START at y=787..840  hit-test: null
    1280x800  5 offers  needs 1036px                        START at y=983        hit-test: null
    1024x768  4 offers  needs 878 in 768                    START at y=825        hit-test: null
    1024x768  5 offers  needs 1021                          START at y=1032, and the 4th and 5th
                                                                   OFFERS are unreachable too

  The dock is `position:absolute; inset:auto 0 0 auto; overflow:auto`, so the surplus does not
  spill into the page — it hides the buttons below the dock's own clip line, reachable only by
  scrolling something that never looks scrollable. The engine stylesheet names the cause and
  answers it with a scrollbar: "The panel keeps overflow:auto … (4 cards at 900x600 genuinely do
  not fit)". A shop you can walk into but cannot buy from is a defect, not a look.

  The cause is arithmetic, one column wide by one pixel: the dock is min(470px,45vw) with padding
  20px 18px 26px and a 1px border, so its content box is 433px, while an offer card is authored at
  min-width:212px — two cards plus a 10px gap is 434px, so the flex row refuses to pair them and
  the shelf stacks. Every card then drags its own 🔒 hold button underneath it, so four offers cost
  588px of the 800px dock. Two columns halves that.

  Relaxing the min-width and letting the row pair the cards is enough on its own — 2 × 211.5 + 10
  = 433 exactly, and the cards were already painting 210px wide, so no text rewraps. The gate is
  the dock's own width, not a taste call: the dock is 45vw, so below 980px of viewport the cells
  fall under ~195px and pairing would crush the card art. Phones keep today's layout untouched
  (their own dock is 175px wide at 390px — a separate, bigger defect, flagged not fixed here).

  No engine CSS is edited. intro.css loads after the engine stylesheet (index.html line 13 vs 10)
  and every selector below is strictly more specific than the rule it answers.
*/
@media (min-width: 980px) {
  #ui .shop .shop-offers {
    gap: 9px 10px;
  }

  #ui .shop .shop-offers .owrap {
    /* beats the engine's `.shop-offers .owrap` (0,3,0): the wrapper is the flex item here, and it
       is allowed to give up its 212px floor because the card inside no longer needs it */
    flex: 1 1 calc(50% - 5px);
    min-width: 0;
    max-width: calc(50% - 5px);
  }

  #ui .shop .shop-offers .owrap > .offer {
    /* beats `.offer{min-width:212px}` (0,1,0) and `.shop-offers .owrap>.offer{max-width:210px}`
       (0,3,0): the card now fills whatever cell it is given instead of voting itself onto its own
       line. Measured cell is 211.5px at a 470px dock, 206.9px at 1024 — both still above the
       180px the card's own two-column grid needs before its text starts to rewrap. */
    width: 100%;
    min-width: 0;
    max-width: 100%;
  }
}

/* =========================================================
   7. the phone gets a shop: full-width sheet, not a 45vw slot
   ========================================================= */

/*
  Measured on live (shop-fit-check, 2026-09-15), every number below from the engine's own shelf
  reached through openShop/rerollShop/bwLock, panel measured at rest after the rise:

    390x844  dock paints 176px wide   3 offers surplus 173px   START hit-testable by luck only
                                            4 offers surplus 328px   REROLL hit: null
                                            5 offers surplus 471px   START hit: null, hold 5: null
    320x568  dock paints 144px wide   3 offers surplus 540px   START hit: null — you cannot leave
                                            4 offers surplus 729px   offers 4, holds 3-4: null
                                            5 offers surplus 884px   offers 4-5, holds 3-5: null

  At 45vw the dock is a 144-176px slot, and everything inside it degrades into a column of
 _wrappers: the keyboard-hint paragraph is 155-233px tall, the START button is 194x115 (its own
  label wraps to four lines), and cards still demand their authored min-width:212px, so they
  overflow the panel sideways as well. A phone player could not start the next wave even with an
  empty shop. Same doctrine as the desktop fix: a shop you can walk into but cannot buy from is a
  defect, not a look.

  On a phone the shop is the screen, so the sheet takes the screen: full width, and the shelf
  becomes one honest column of full-width cards whose hold button sits BESIDE the card instead of
  under it (that alone is 30px per offer at 390, 37 at 320). Header, wallet line, key hints and
  the owned-stat row are all tightened to one or two lines rather than wrapped into towers.

  What this trades, said plainly: at 45vw the player could still see a strip of the shop interior
  to the left of the dock; at 100% width there is no strip. That is the choice forced by the
  arithmetic — measured, the section budget for the maximum shelf (5 offers) is 539px at 320x568
  and 717px at 390x844, so a partial sheet (the obvious alternative, ~70% height = 398px at 320
  and 591px at 390) cannot hold 4 offers, let alone 5, without hiding a control. With the compact
  card re-flow below, a ~70% sheet DOES fit at 390 (539 < 591) and that is the variant to prefer
  if Jordi wants the interior visible on phones; it is a two-line change to `inset` and it needs
  his call, not mine.

  Barry's own dialogue node is unaffected either way: it is z-index 7 above the panel's 6 and
  pointer-events:none, so it can overlap the sheet but can never steal a tap from a button.
*/
@media (max-width: 719px) {
  #ui .shop {
    /* beats both engine `.shop` rules (0,1,0): the dock stops being 45vw of planet. height stays
       the engine's 100%, which inside #ui (inset:0) is already the viewport — verified 844 and 568
       painted, not assumed. */
    inset: auto 0 0 0;
    width: 100%;
    /* no bottom padding: a sticky child cannot leave its containing block, and 14px of bottom
       padding was 14px of shop floor the pinned buy bar could never reach — measured, the bar
       stopped at y=554 of 568 and left the next row's text peeking out under it. The gutter that
       padding used to give now lives on the last row instead. */
    padding: 10px 12px 0;
    gap: 6px;
  }

  #ui .shop h2 {
    /* "COSMIC SHOP" at clamp(20px,4vw,32px) wrapped to two lines in a 144px slot (46px tall). It
       is the shop's face, so it stays big — just big enough to be one line. */
    font-size: 20px;
    letter-spacing: 0.06em;
  }

  #ui .shop .shop-scoop {
    /* three numbers, one line: 37 characters at 11.5px measure ~210px against the 292px the 320
       phone gives this row, so it stops wrapping at both target sizes (measured 32px -> 17px) */
    font-size: 11.5px;
    line-height: 1.3;
    gap: 1px 7px;
  }

  #ui .shop .shop-scoop i {
    margin-left: 8px;
    font-size: 10.5px;
  }

  #ui .shop .shop-offers {
    gap: 8px;
  }

  #ui .shop .shop-offers .owrap {
    /* the hold button goes beside the card: the row costs the taller of the two instead of the
       sum. Engine authored `.shop-offers .owrap` as a column (0,2,0, triplicated three times in
       the engine stylesheet — the cascade winner is the last copy, and this beats all of them on
       specificity, so none of them needed editing). */
    flex-direction: row;
    align-items: stretch;
    gap: 8px;
    min-width: 0;
  }

  #ui .shop .shop-offers .owrap > .offer {
    /* the 212px floor is what overflowed a 144px dock sideways; the card takes the column */
    width: auto;
    min-width: 0;
    max-width: 100%;
    flex: 1 1 auto;
  }

  #ui .shop .shop-offers .olb {
    flex: 0 0 auto;
    align-self: center;
    max-width: 36%;
    line-height: 1.35;
    padding: 6px 8px;
  }

  #ui .shop .shop-cta {
    gap: 8px;
    /* The one control a player must never have to hunt for. On the smallest phone the maximum
       shelf is ~100px taller than the screen with nothing clipped (see the bake-off below), so the
       buy bar is pinned to the bottom edge of the sheet's own scroll box: sticky, inside the
       panel's clip, not `position:fixed`, because the engine stylesheet explains that #app is
       overflow:hidden and a fixed box escapes that clip. At 390 and at 320 with 3 offers the row
       already fits, and sticky is inert when its normal position is on screen — measured, the
       pinned rect equals the flow rect whenever there is no surplus. */
    /* No `order` here, deliberately: as the LAST flex child the bar gets pushed DOWN to the
       scrollport bottom even when everything already fits, which measured WORSE (START at y=614 of
       a 568 screen on a 3-offer shelf that was otherwise perfect). In its authored position the pin
       only engages when there is a surplus to pin against — which is exactly when it is needed. */
    position: sticky;
    bottom: 0;
    z-index: 2;
    margin: 0 -12px;
    padding: 6px 12px 8px;
    background: linear-gradient(180deg, rgba(11, 10, 26, 0) 0%, rgba(11, 10, 26, 0.92) 34%, #0b0a1a 100%);
  }

  #ui .shop .btn--big {
    /* START was 194x115 because 34px of side padding inside a 176px dock left room for four lines
       of label. Same button, fitted: one line, and the kbd chip stays on it. */
    padding: 10px 14px;
    font-size: 15px;
    gap: 7px;
  }

  #ui .shop .btn--small {
    margin: 0;
    padding: 8px 10px;
  }

  #ui .shop .shop-keys {
    font-size: 10.5px;
    line-height: 1.4;
    row-gap: 2px;
  }

  #ui .shop .shop-keys kbd {
    padding: 1px 5px;
    font-size: 10px;
  }

  #ui .shop .shop-stats {
    padding-top: 2px;
    padding-bottom: 12px;
  }

  #ui .shop .shop-stats h3 {
    font-size: 10px;
    margin-bottom: 4px;
  }

  #ui .shop .shop-stats .own {
    font-size: 10.5px;
    padding: 2px 8px;
  }

  #ui .shop .shop-empty {
    font-size: 11px;
  }

  #ui .shop .btn--small {
    font-size: 11px;
    padding: 6px 9px;
  }

  #ui .shop .shop-offers .owrap > .offer {
    padding: 9px 11px;
  }
}

/*
  Measured layout bake-off at 320x568 with the maximum shelf (5 offers), total panel content in
  parentheses, 568 is the budget — phone-layout-probe.mjs:

    hold beside the card, engine's own card grid   offers 442px   total 766px   0 clipped  <== shipped
    hold beside the card, card re-flowed to flex   offers 466px   total 790px   0 clipped
    hold under the card,   engine's own card grid   offers 666px   total 990px   0 clipped
    hold under the card,   card re-flowed to flex   offers 619px   total 944px   0 clipped

  So the engine's card is already the tightest honest shape and the hold button belongs beside it;
  an earlier draft of this section re-flowed the card (price up onto the name line) and it measured
  WORSE, so that draft was deleted rather than kept because it looked clever.

  What still does not fit, stated plainly: 5 offers need 442px of shelf and the tightened chrome
  (title, wallet line, CTA, keyboard legend, owned-stat row, gaps, padding) is 227px, so 320x568 is
  ~100px over with nothing clipped. 3 offers (262+227=489) and 4 offers (352+227=579, closed to
  ~565 by the last few declarations above) fit. Five do not, and the only ways to make five fit all
  cost something Jordi owns: clip offer descriptions to one line (words lost), shrink the legend and
  body type below ~10px (legibility), hide the keyboard legend (information), or move the hold
  button into the card's corner in the DOM so the description gets the full 289px (an engine edit).
  Numbers, options, no silent choice.
*/

/* =========================================================
   8. reduced motion, LAST, so it actually wins
   ========================================================= */

/*
  Media queries add no specificity, so an `animation: none` sitting here at line 300 loses to
  every animated rule below it. Measured before this block existed: with reduced motion the
  payout receipt and the phone banner still animated, because their rules sit further down the
  file. Anything new that moves must be listed here, after it.
*/
@media (prefers-reduced-motion: reduce) {
  .ai-bw-splash__art,
  .ai-bw-splash__art::after,
  .ai-bw-splash__scrim,
  .menu.ai-bw-title::before,
  .ai-bw-pay,
  #ui .banner,
  #ui .shop,
  #ui .shop.ai-bw-shop-sink {
    animation: none;
  }
  .ai-bw-pay,
  #ui .shop {
    transition: none;
  }
}

/* ---------------------------------------------------------------- staging v2, beat 5: the dark
   The cover for the planet swap. The engine owns no full-screen layer (worldNew has no consumer, and
   .shop has been a 45 vw dock since the shop aesthetic sign-off), so the veil that lets the world change
   unseen is this one element, driven by intro.js off sh.phase === "sink". Same colour as the boot splash
   two rules above, so "the world went away" and "the world arrived" are the same black. It is created and
   destroyed by the script -- never left in the DOM -- and pointer-events:none so it can never hold a click
   even in the frames it is fading. */
.ai-bw-swap {
  position: fixed;
  inset: 0;
  z-index: 2147483000;
  background: #04070f;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.26s ease;
}
.ai-bw-swap.is-in { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  /* the swap still happens; it just stops being animated. The dark is a cover, not a flourish. */
  .ai-bw-swap { transition: none; }
}

/* ----------------------------------------------------------------------- bwdiss
 * THE CROSS-DISSOLVE (bwr76). The frozen pre-swap frame, presented as a DOM layer, then dissolved
 * away over the new world. One step BELOW .ai-bw-swap in z-order on purpose: the blackout stays
 * able to dip over the top, and stays the fallback when a snapshot cannot be taken.
 * opacity starts at 0 so a layer that is created but never armed is invisible. */
.ai-bw-dissolve {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 2147482999;
  opacity: 0;
  pointer-events: none;
  background-color: #04070f;
  background-repeat: no-repeat;
  background-size: 100% 100%;
  transition: opacity 0.42s ease;
}
@media (prefers-reduced-motion: reduce) {
  /* no ramp means no dissolve, so honour it: the layer just clears. Never a stuck picture. */
  .ai-bw-dissolve { transition: none; }
}

/* ------------------------------------------------------------------------ bwt5
 * The shop-inbound telegraph (T5). PROVISIONAL — window.__BW_T5.ON = 0 stops the JS from ever adding these
 * classes, and nothing here applies on its own: .ai-bw-t5 and bw-nav--t5 exist only while the telegraph is
 * armed, so with the device off this block is inert CSS over today's markup.
 *
 * The strip sits bottom-centre on purpose: the banner is at 21% up and the payout card is bottom-left, so
 * this lane of the screen is empty, and the telegraph is over before the offers panel ever exists.
 * Visual family is the payout card's — mono micro-heading, warm number, plate — so it reads as the same game
 * rather than as a new system.
 *
 * ONE RULE THIS BLOCK OBEYS, AND IT IS NOT STYLE: assets/index-BlFMRGkz.css says it in as many words —
 *   "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."
 * That file contains zero real backdrop-filter declarations; its only occurrence is that comment. So this
 * plate is opaque gradient, every glow in here is text-shadow, and the two animations move transform,
 * opacity and text-shadow and nothing else. The payout card can afford a blur because the world is frozen
 * behind it; this strip runs while he is still sprinting, which is exactly when a main-thread composite
 * would show up as dropped frames in the one second the telegraph exists.
 */
.ai-bw-t5 {
  position: absolute;
  left: 50%;
  bottom: clamp(46px, 8vh, 78px);
  transform: translateX(-50%);
  z-index: 4;
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 8px 18px 10px;
  border-radius: 999px;
  border: 1px solid rgba(255, 196, 92, 0.5);
  /* opaque on purpose: a backdrop-filter here would cost frames over the live canvas (see the block header) */
  background: linear-gradient(180deg, rgba(52, 31, 9, 0.94), rgba(11, 8, 19, 0.96));
  box-shadow: 0 10px 30px rgba(4, 7, 15, 0.5), inset 0 1px 0 rgba(255, 236, 200, 0.14),
    0 0 26px rgba(255, 178, 66, 0.18);
  pointer-events: none;
  opacity: 1;
  transition: opacity 0.34s ease, transform 0.34s ease;
  animation: aibw-t5in 0.3s cubic-bezier(0.16, 0.72, 0.24, 1);
}

@keyframes aibw-t5in {
  from { opacity: 0; transform: translateX(-50%) translateY(9px); }
  to   { opacity: 1; transform: translateX(-50%); }
}

.ai-bw-t5.is-out {
  opacity: 0;
  transform: translateX(-50%) translateY(7px);
}

.ai-bw-t5__head {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: var(--bw-t1);
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: #ffd77a;
}

.ai-bw-t5__count {
  min-width: 1.05em;
  font-size: var(--bw-d1);
  font-weight: 800;
  line-height: 1;
  text-align: center;
  color: #fff2d6;
  text-shadow: 0 0 18px rgba(255, 196, 92, 0.55), 0 1px 0 rgba(0, 0, 0, 0.6);
}

/* HOLD: the digits are spent and the room has not come up yet. Calm, not strobing. */
.ai-bw-t5.is-ready .ai-bw-t5__count {
  min-width: 0;
  font-size: var(--bw-t2);
  font-weight: 700;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: #ffd77a;
  text-shadow: 0 0 14px rgba(255, 196, 92, 0.35);
}

.ai-bw-t5.is-tick .ai-bw-t5__count {
  animation: aibw-t5tick 0.42s cubic-bezier(0.16, 0.72, 0.24, 1);
}

/* no filter:brightness() in here even though it would be the one-line way to flash the digit: same reason
   as the plate. The pop is carried by transform, and the flash is carried by text-shadow, which is what the
   engine's own sign glow is made of. */
@keyframes aibw-t5tick {
  0%   { transform: scale(1.45); text-shadow: 0 0 34px rgba(255, 224, 160, 0.95), 0 1px 0 rgba(0, 0, 0, 0.6); }
  60%  { transform: scale(1); }
  100% { transform: scale(1); text-shadow: 0 0 18px rgba(255, 196, 92, 0.55), 0 1px 0 rgba(0, 0, 0, 0.6); }
}

/* The chevron the engine already draws, already positions, and already labels "cosmic shop / 2.3u out".
   T5 adds one class and one span; it never sets display, left, top or transform on #bw-nav, so the engine
   stays the only author of where the arrow points. */
#bw-nav.bw-nav--t5 .bw-nav__chev {
  animation: aibw-t5beacon 0.9s ease-in-out infinite;
}

#bw-nav.bw-nav--t5.bw-nav--t5-final .bw-nav__chev {
  animation-duration: 0.42s;
}

#bw-nav.bw-nav--t5 .bw-nav__count {
  position: absolute;
  left: 16px;
  top: 26px;
  font-size: var(--bw-t2);
  font-weight: 800;
  line-height: 1;
  white-space: nowrap;
  color: #fff2d6;
  text-shadow: 0 0 14px rgba(255, 196, 92, 0.6), 0 1px 0 rgba(0, 0, 0, 0.65);
}

@keyframes aibw-t5beacon {
  0%, 100% { opacity: 0.5; }
  50%      { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  /* the count still happens — it is information, not decoration — it just stops moving. */
  .ai-bw-t5,
  .ai-bw-t5.is-tick .ai-bw-t5__count { animation: none; }
  #bw-nav.bw-nav--t5 .bw-nav__chev { animation: none; opacity: 1; }
}

/* == bwr22 SHOPREAD-001: one big legible readout per counter object == */
.bw-r22-label{position:absolute;z-index:7;pointer-events:none;transform:translate(-50%,-100%);
  box-sizing:border-box;min-width:150px;max-width:min(300px,40vw);padding:7px 12px 8px;border-radius:11px;
  background:#0b0a18ee;border:1.5px solid rgba(140,180,255,.45);box-shadow:0 8px 22px rgba(0,0,0,.6);
  text-align:center;transition:border-color .18s ease,box-shadow .18s ease}
.bw-r22-label .bw-r22-n{display:block;font-size:var(--bw-t3);font-weight:900;letter-spacing:.05em;color:#fff;
  text-shadow:0 1px 6px rgba(0,0,0,.9)}
.bw-r22-label .bw-r22-t{display:block;margin-top:2px;font-size:var(--bw-t2);font-weight:600;color:#bfe0ff;
  text-shadow:0 1px 5px rgba(0,0,0,.9)}
.bw-r22-label .bw-r22-c{display:block;margin-top:3px;font-size:var(--bw-d1);font-weight:900;color:#ffd04f;
  text-shadow:0 1px 7px rgba(0,0,0,.95)}
.bw-r22-label[data-tier="1"]{border-color:rgba(255,195,92,.75)}
.bw-r22-label[data-tier="2"]{border-color:rgba(200,120,255,.8)}
.bw-r22-label[data-tier="3"]{border-color:rgba(255,170,60,.9);box-shadow:0 0 16px rgba(255,170,60,.35),0 8px 22px rgba(0,0,0,.6)}
.bw-r22-label.bw-r22--sel{border-color:#9fe8ff;box-shadow:0 0 18px rgba(120,210,255,.5),0 8px 22px rgba(0,0,0,.6)}
.bw-r22-label.bw-r22--ok{border-color:#8affb0;box-shadow:0 0 20px rgba(110,255,160,.55),0 8px 22px rgba(0,0,0,.6)}
.bw-r22-label.bw-r22--ok .bw-r22-c{color:#8affb0}
.bw-r22-label.bw-r22--no{border-color:#ff5b4d;box-shadow:0 0 20px rgba(255,80,60,.55),0 8px 22px rgba(0,0,0,.6);
  animation:bwR22Shake .34s ease}
.bw-r22-label.bw-r22--no .bw-r22-c{color:#ff5b4d}
@keyframes bwR22Shake{0%,100%{margin-left:0}25%{margin-left:-7px}55%{margin-left:6px}80%{margin-left:-3px}}


/* =========================================================
   9. the HUD: a readout strip, not a slab
   ========================================================= */

/*
  Measured 2026-10-10, before this block (lane evidence, shots/before-*.json, div.hud at 14,14):

    1440x810  arena   1412x76    1 row
     960x600  arena    932x135   2 rows
    1440x810  shop    1026x135   2 rows
     960x600  shop     620x202   3 rows

  The 960 shop frame is the bad one, and it is bad twice over: 620x202 is 65% of the width and
  36% of the height of the frame, and its right edge (634) is EXACTLY the shop dock's left edge
  (634), so the two panels tile. The whole top-left quadrant of a 960x600 screen is interface.

  Three things made it a slab, and all three are geometry, not decoration -- not one readout is
  removed, hidden or reworded:

  (1) THE AURA BAR WAS 240x22px. It is a progress meter; 240px of it is a quarter of a 960-wide
      screen spent on one number that the three characters above it already state. 150x11.
  (2) EVERY CHIP CARRIED 6px 10px OF PADDING on an 8px gap. 4px 9px on a 6px gap. Still a
      comfortable target, no longer upholstered.
  (3) THE WEAPON ROW WAS 220px OF EMOJI. Four 20px weapon chips plus their key badges was the
      widest object in the HUD and it is what forced the third row into existence at 960. The
      glyphs come down to 17px with a 10px key badge. 17px is an ICON size, not a type step --
      the emoji is the weapon's face and has to stay readable at a glance; the only type in
      that chip is the 10px key badge, which is on the scale.

  And the gutter. While the shop is up, bwshopfx.js pins the HUD's right edge to the dock's own
  width so the right-hand chips wrap into the visible area instead of sliding under the dock
  (SHOPFX-010). That reason is right; the number was one gutter short, so the two panels met.
  The same expression plus 28px keeps the reason and drops the tiling. It is scoped to
  `#ui:has(#bw-shop)` -- the arena HUD still spans exactly the frame the engine gave it -- and
  it needs `!important` and the extra `#ui` only because SHOPFX-010 is itself an important
  runtime rule; without both, this rule would lose the cascade and the gutter would never exist.
*/
.hud {
  gap: 6px;
}

.hud .stat {
  padding: 4px 9px;
  border-radius: 9px;
}

.hud .lbl {
  font-size: var(--bw-t1);
  letter-spacing: 0.13em;
}

.hud .num {
  font-size: var(--bw-t2);
}

.hud .big {
  font-size: var(--bw-t3);
}

/* the wave readout is a SENTENCE ("W1 · CAUSEWAY OF THE RATIO · 0 LEFT"), not a numeral, so it
   sits one step below the numeric readouts. That is what keeps it on one line at 960 without an
   ellipsis eating the wave name, and it keeps 15px meaning "a number you act on". */
.hud .stat.wave .big {
  font-size: var(--bw-t2);
}

.hud .stat.aura .lbl {
  font-size: var(--bw-t1);
}

.hud .stat.aura .num {
  font-size: var(--bw-t3);
}

.hud .bar {
  height: 11px;
  margin-top: 3px;
  border-radius: 6px;
}

.hud .stat.aura .bar {
  width: 150px;
  height: 11px;
}

.hud .chips {
  gap: 6px;
}

.hud .chip {
  font-size: var(--bw-t1);
  padding: 3px 7px;
  border-radius: 7px;
}

.hud .weapon {
  min-width: 0;
}

.hud .weapon .wrow {
  gap: 4px;
}

.hud .weapon .wchip {
  font-size: 17px;
  padding: 2px 5px;
}

.hud .weapon .wkey {
  font-size: var(--bw-t1);
  margin-right: 2px;
  padding: 1px 3px;
}

.hud .weapon .whint {
  font-size: var(--bw-t1);
  margin-top: 2px;
}

/* the dock is min(400px,34vw) wide; 28px of gutter on top of that is the lane of 3D that was
   missing. Both sides use the same expression, so they can never disagree at any width. */
#ui:has(#bw-shop) .hud {
  right: calc(min(400px, 34vw) + 28px) !important;
}
