/* css/ui.css — the chrome: everything the player reads.
 *
 * Read off `.design/sheet.png` panel by panel. The sheet's whole visual idea is
 * that every control is a PHYSICAL OBJECT — a key with a hard shadow under it, a
 * parchment card with a stitched edge, a ribbon nailed across the top of a
 * panel — so every rule here is in service of depth: two-stop gradients, a
 * dark outline, and an offset solid shadow that the object drops onto when it
 * is pressed.
 *
 * NOTHING HERE ANIMATES ON A TIMER. The only motion is a press and the combo
 * badge's pop, because everything else on screen is already moving.
 */

/* ---- pills: the readouts ------------------------------------------------ */

.pill {
  position: absolute;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 7px 13px;
  border-radius: var(--r-pill);
  background: linear-gradient(var(--parch-top), var(--parch-bottom));
  border: var(--edge) solid var(--parch-edge);
  box-shadow: 0 3px 0 var(--parch-shadow), 0 5px 10px rgba(0, 0, 0, .35);
  color: var(--ink);
  font-weight: var(--weight-heavy);
  font-variant-numeric: tabular-nums;
}

#score {
  top: 12px; left: 12px;
  flex-direction: column; align-items: flex-start; gap: 0;
  padding: 6px 15px; border-radius: 16px;
}
#score b { font-size: 22px; line-height: 1.25; letter-spacing: .5px; }
#score i { font-style: normal; font-size: 10px; opacity: .72; letter-spacing: .6px; }

#gems { top: 56px; right: 12px; font-size: 14px; }
#coins { top: 98px; right: 12px; font-size: 14px; }

#hearts { position: absolute; top: 12px; right: 12px; display: flex; gap: 5px; }
.hp { width: 22px; height: 22px; filter: drop-shadow(0 2px 0 rgba(0, 0, 0, .35)); }
.hp.off { opacity: .28; filter: grayscale(1); }

/* The combo badge — the sheet's starburst, as a rotated slab. */
#combo {
  position: absolute; top: 54px; left: 50%;
  transform: translateX(-50%) rotate(-6deg);
  display: none;
  padding: 6px 20px 8px;
  font-size: 26px; font-weight: var(--weight-heavy); letter-spacing: 1px;
  color: #fff;
  background: linear-gradient(var(--btn-gold), var(--combo-bottom));
  border: 4px solid var(--combo-edge); border-radius: var(--r-tile);
  text-shadow: 0 3px 0 var(--combo-text);
  box-shadow: 0 5px 0 var(--combo-edge);
}
#combo.on { display: block; }
#combo small { display: block; font-size: 10px; letter-spacing: 2px; margin-bottom: -4px; opacity: .9; }
@keyframes pop { from { transform: translateX(-50%) rotate(-6deg) scale(1.45); } to { transform: translateX(-50%) rotate(-6deg) scale(1); } }
#combo.pop { animation: pop .24s ease-out; }

/* THE BADGE GROWS WITH THE ROLL.
 *
 * `data-tier` is 1..4, set by src/ui/hud.js from Score.chainTier, and it is the
 * one part of a chain that persists between landings rather than flashing once.
 * Each step is bigger, hotter and leans further over — the whole point is that
 * a player at x18 can see they are somewhere they have not been, without
 * anything having to say so. `--scale` is multiplied into the pop keyframe too,
 * so a tier-4 badge pops from tier-4 size and does not snap back to tier-1. */
#combo { --scale: 1; }
#combo[data-tier="2"] { --scale: 1.12; background: linear-gradient(var(--tier-2), var(--tier-2-deep)); border-color: var(--tier-2-edge); }
#combo[data-tier="3"] { --scale: 1.26; background: linear-gradient(var(--tier-3), var(--tier-3-deep)); border-color: var(--tier-3-edge); }
#combo[data-tier="4"] { --scale: 1.42; background: linear-gradient(var(--tier-4), var(--tier-4-deep)); border-color: var(--tier-4-edge); }
#combo[data-tier] { transform: translateX(-50%) rotate(-6deg) scale(var(--scale)); }
@keyframes popTier {
  from { transform: translateX(-50%) rotate(-6deg) scale(calc(var(--scale) * 1.45)); }
  to   { transform: translateX(-50%) rotate(-6deg) scale(var(--scale)); }
}
#combo[data-tier].pop { animation: popTier .24s ease-out; }
/* ...and past the third tier it will not sit still. */
@keyframes throb {
  0%, 100% { transform: translateX(-50%) rotate(-6deg) scale(var(--scale)); }
  50%      { transform: translateX(-50%) rotate(-5deg) scale(calc(var(--scale) * 1.06)); }
}
#combo[data-tier="4"]:not(.pop) { animation: throb 1s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  #combo[data-tier="4"]:not(.pop) { animation: none; }
}

/* The perk column, down the left. One tile per card held, with its level. */
#perks { position: absolute; top: 150px; left: 12px; display: flex; flex-direction: column; gap: 8px; }
.perk {
  position: relative; width: 38px; height: 38px; border-radius: 11px;
  border: var(--edge) solid rgba(0, 0, 0, .45);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 3px 0 rgba(0, 0, 0, .35);
}
.perk small {
  position: absolute; right: -4px; bottom: -4px;
  font-size: 11px; font-weight: var(--weight-heavy); color: #fff;
  text-shadow: 0 1px 2px #000, 0 0 3px #000;
}

/* The side room's clock, along the bottom. */
#roomBar {
  position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%);
  width: 210px; height: 26px; display: none;
  background: rgba(8, 20, 36, .85);
  border: var(--edge) solid var(--panel-edge); border-radius: var(--r-pill);
  overflow: hidden;
}
#roomBar.on { display: block; }
#roomFill { height: 100%; width: 100%; background: linear-gradient(var(--btn-green), var(--btn-green-2)); transition: width .12s linear; }
#roomTxt {
  position: absolute; inset: 0; text-align: center;
  font-size: 12px; font-weight: var(--weight-heavy); line-height: 20px; letter-spacing: 1px;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .8);
}

/* ---- the square keys ---------------------------------------------------- */

.sq {
  position: absolute; width: 44px; height: 44px; border-radius: 13px;
  cursor: pointer; padding: 0;
  border: var(--edge) solid rgba(0, 0, 0, .5);
  background: linear-gradient(var(--btn-purple), var(--btn-purple-2));
  box-shadow: 0 4px 0 var(--btn-purple-3);
  display: flex; align-items: center; justify-content: center;
}
.sq:active { transform: translateY(4px); box-shadow: none; }
.sq.gold { background: linear-gradient(var(--btn-gold), var(--btn-gold-2)); box-shadow: 0 4px 0 var(--btn-gold-3); }
.sq.blue { background: linear-gradient(var(--btn-blue), var(--btn-blue-2)); box-shadow: 0 4px 0 var(--btn-blue-3); }
.sq.grey { background: linear-gradient(var(--btn-grey), var(--btn-grey-2)); box-shadow: 0 4px 0 var(--btn-grey-3); }
#pause { right: 12px; bottom: 12px; }
#full { right: 64px; bottom: 12px; }

/* ---- overlays ----------------------------------------------------------- */

/* THE OVERLAY SCROLLS, NOT THE PANEL.
 *
 * The panel used to carry `max-height: 100%; overflow-y: auto`, and it clipped
 * its own close button: the X is positioned to hang OUTSIDE the top-right
 * corner — that is how the design sheet draws it, overlapping the gold edge —
 * and a scroll container cannot paint outside itself. Measured at
 * `clippedRight: true, clippedTop: true`, which is why it looked wedged under
 * the border instead of sitting on it.
 *
 * So the overlay is the scroller and the panel overflows freely.
 * `align-items: flex-start` with `margin-block: auto` on the panel is the
 * standard pair: centred when it fits, scrollable from the top when it does
 * not — where `align-items: center` alone puts the top of a tall panel above
 * the scroll origin, out of reach for ever. */
.overlay {
  position: absolute; inset: 0; display: none;
  align-items: flex-start; justify-content: center;
  /* room at the top for the close button to hang into */
  padding: 22px 16px 16px;
  background: rgba(6, 16, 30, .72);
  backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
  overflow-y: auto; overflow-x: hidden;
}
.overlay.show { display: flex; }

.panel {
  position: relative;
  width: min(92vw, 372px);
  margin-block: auto;
  flex: 0 0 auto;
  background: linear-gradient(var(--panel-top), var(--panel-bottom));
  border: 4px solid var(--gold); border-radius: var(--r-panel);
  box-shadow: 0 0 0 4px var(--panel-edge), 0 14px 34px rgba(0, 0, 0, .6);
  padding: 22px 16px 18px;
  text-align: center;
}

/* The ribbon nailed across the top. Pulled up out of the panel so it overlaps
 * the gold edge, which is what makes it read as a separate object. */
.ribbon {
  display: inline-block; margin: -42px auto 10px;
  padding: 9px 30px;
  font-size: 22px; font-weight: var(--weight-heavy); letter-spacing: 1px;
  color: #fff; text-shadow: 0 3px 0 rgba(0, 0, 0, .35);
  border: 4px solid rgba(0, 0, 0, .35); border-radius: 12px;
  background: linear-gradient(var(--rib-pink), var(--rib-pink-2));
  box-shadow: 0 5px 0 rgba(0, 0, 0, .3);
}
.ribbon.blue { background: linear-gradient(var(--rib-blue), var(--rib-blue-2)); }
.ribbon.gold { background: linear-gradient(var(--rib-gold), var(--rib-gold-2)); }
.ribbon.green { background: linear-gradient(var(--rib-green), var(--rib-green-2)); }

.close {
  position: absolute; top: -12px; right: -8px;
  width: 36px; height: 36px; border-radius: 50%; cursor: pointer;
  border: var(--edge) solid var(--panel-edge);
  background: linear-gradient(var(--steel), var(--steel-2));
  color: #fff; font: var(--weight-heavy) 18px/1 var(--face);
  box-shadow: 0 4px 0 var(--steel-3);
}
.close:active { transform: translateY(4px); box-shadow: none; }

.lbl { font-size: 12px; font-weight: var(--weight-body); letter-spacing: 2px; opacity: .7; margin: 12px 0 2px; }
/* line-height 1 on a 46px face cuts the ascender off the top and the comma
 * off the bottom of "12,850" — 8px of the number, on the results screen,
 * which is the number the whole run was for. */
.big { font-size: 46px; font-weight: var(--weight-heavy); line-height: 1.3; text-shadow: 0 3px 0 rgba(0, 0, 0, .3); font-variant-numeric: tabular-nums; }
.sub { font-size: 12px; font-weight: var(--weight-body); opacity: .68; margin-top: 5px; letter-spacing: .5px; }

.rowc { display: flex; gap: 10px; justify-content: center; margin-top: 14px; flex-wrap: wrap; }
.stat {
  display: flex; align-items: center; gap: 7px;
  padding: 7px 16px; border-radius: var(--r-pill);
  font-weight: var(--weight-heavy); font-size: 16px;
  background: rgba(8, 20, 36, .55); border: var(--edge) solid rgba(0, 0, 0, .4);
  font-variant-numeric: tabular-nums;
}

/* ---- the buttons -------------------------------------------------------- */

.b {
  display: flex; align-items: center; justify-content: center; gap: 9px;
  width: 100%; margin-top: 12px; padding: 13px;
  font: var(--weight-heavy) 19px/1.15 var(--face); letter-spacing: .5px;
  color: #fff; cursor: pointer;
  border: var(--edge) solid rgba(0, 0, 0, .42); border-radius: var(--r-button);
  text-shadow: 0 2px 0 rgba(0, 0, 0, .32);
}
.b.green { background: linear-gradient(var(--btn-green), var(--btn-green-2)); box-shadow: 0 5px 0 var(--btn-green-3); }
.b.gold { background: linear-gradient(var(--btn-gold), var(--btn-gold-2)); box-shadow: 0 5px 0 var(--btn-gold-3); }
.b.purple { background: linear-gradient(var(--btn-purple), var(--btn-purple-2)); box-shadow: 0 5px 0 var(--btn-purple-3); }
.b.blue { background: linear-gradient(var(--btn-blue), var(--btn-blue-2)); box-shadow: 0 5px 0 var(--btn-blue-3); }
.b.grey { background: linear-gradient(var(--btn-grey), var(--btn-grey-2)); box-shadow: 0 5px 0 var(--btn-grey-3); }
.b:disabled { filter: grayscale(.7); opacity: .55; cursor: default; }
.b:active:not(:disabled) { transform: translateY(5px); box-shadow: none; }
.b.half { width: auto; flex: 1 1 0; font-size: 16px; padding: 11px 8px; }

/* The little television that means "this costs an advert". */
.adi {
  width: 24px; height: 17px; border-radius: 3px;
  background: #1A1A1A; border: 2px solid #fff;
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
}
.adi::after {
  content: ""; border-left: 6px solid #fff;
  border-top: 4px solid transparent; border-bottom: 4px solid transparent;
}

/* ---- the draft ---------------------------------------------------------- */

/* THE CARD IS THE ONE SCREEN THAT STOPS THE GAME, so it gets the room.
 *
 * It was three narrow slips with a 46px badge, an 11px name and 9.5px of prose
 * at 82% opacity. Every one of those numbers was chosen to fit three cards
 * across a 320px phone, and together they made the single most important
 * decision in a run the hardest thing on any screen to read — a child stopping
 * mid-climb to squint at grey 9.5px type.
 *
 * So it stacks instead. One card per ROW, full width, with the icon on the left
 * at 64px and the name and the line beside it. Three rows fit the height a
 * three-column layout was never actually short of, the type goes up to 15 and
 * 12.5 at full opacity, and the painted icon has somewhere to be seen. */
.cards { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
.card {
  width: 100%;
  background: linear-gradient(var(--parch-top), var(--parch-card));
  border: var(--edge) solid var(--parch-edge); border-radius: var(--r-tile);
  padding: 12px 14px; cursor: pointer;
  font: var(--weight-body) 12.5px/1.25 var(--face); color: var(--ink);
  box-shadow: 0 5px 0 var(--parch-shadow);
  display: grid; grid-template-columns: auto 1fr; gap: 4px 14px;
  align-items: center; text-align: left;
}
.card:active { transform: translateY(4px); box-shadow: none; }
/* A curse is a different colour before it is a different word. */
.card.curse { background: linear-gradient(var(--curse-top), var(--curse-bottom)); border-color: var(--curse-edge); box-shadow: 0 5px 0 var(--curse-shadow); }
.card.gold { border-color: var(--gold-card-edge); box-shadow: 0 5px 0 var(--gold-card-shadow), 0 0 0 2px rgba(255,210,74,.5); }
.card .ic {
  grid-row: span 2;
  width: 64px; height: 64px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  border: var(--edge) solid rgba(0, 0, 0, .35);
  box-shadow: inset 0 -4px 8px rgba(0, 0, 0, .2);
}
/* The painting overflows its badge a little, so it reads as an object sitting
   in the ring rather than as a sticker inside it. */
.card .ic .uic { width: 116%; height: 116%; object-fit: contain; }
.card b {
  align-self: end;
  font-size: 15px; font-weight: var(--weight-heavy);
  letter-spacing: .3px; line-height: 1.1; text-transform: uppercase;
}
.card > span {
  align-self: start;
  font-size: 12.5px; font-weight: var(--weight-body); line-height: 1.25; opacity: 1;
}
.stars { grid-column: 2; display: flex; gap: 3px; padding-top: 2px; }
.st { width: 13px; height: 13px; }
.st.off { opacity: .25; }

/* Two cards, or four with the shop's perk, still fit: the row shrinks rather
   than the type. */
@media (max-height: 620px) {
  .card { padding: 8px 12px; gap: 2px 10px; }
  .card .ic { width: 52px; height: 52px; }
  .card b { font-size: 13.5px; }
  .card > span { font-size: 11.5px; }
  .cards { gap: 7px; }
}

/* ---- shop --------------------------------------------------------------- */

.tabs { display: flex; gap: 5px; margin: 14px 0 4px; }
.tab {
  flex: 1 1 0; padding: 8px 2px;
  font: var(--weight-heavy) 11px/1 var(--face); letter-spacing: .6px; cursor: pointer;
  border: var(--edge) solid rgba(0, 0, 0, .4); border-radius: 10px 10px 0 0;
  color: #fff; background: rgba(8, 20, 36, .5);
}
.tab.on { background: linear-gradient(var(--btn-gold), var(--btn-gold-2)); color: var(--ink); }

.offer {
  position: relative;
  display: flex; align-items: center; gap: 10px; text-align: left;
  margin-top: 10px; padding: 11px;
  border: var(--edge) solid rgba(0, 0, 0, .4); border-radius: var(--r-button);
  background: linear-gradient(var(--btn-purple), var(--btn-purple-2));
}
.offer.gold { background: linear-gradient(var(--btn-gold), var(--btn-gold-2)); color: var(--ink); }
.offer.slate { background: linear-gradient(var(--slate-top), var(--slate-bottom)); }
/* THE BADGE ON AN OFFER. Every row in the shop has one now — the perk rows had
 * nothing at all, which made the coin shop the one screen in the game with no
 * picture on it, selling the same upgrades the draft sells with a painting
 * each. The ring is the draft card's ring at a smaller size, so a perk and the
 * card it grants are visibly the same object in two places. */
.offer .pic {
  flex: 0 0 auto;
  width: 52px; height: 52px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: rgba(8, 20, 36, .4);
  border: var(--edge) solid rgba(0, 0, 0, .35);
  box-shadow: inset 0 -4px 8px rgba(0, 0, 0, .2);
}
.offer .pic .uic { width: 116%; height: 116%; object-fit: contain; }
.offer .pic .hp, .offer .pic .st { width: 30px; height: 30px; }
.offer .pic svg { display: block; }
.offer .lvl { opacity: .6; font-size: 12px; }
.offer b { display: block; font-size: 15px; letter-spacing: .3px; }
.offer small { display: block; font-size: 11px; opacity: .88; font-weight: var(--weight-body); line-height: 1.3; }
.offer .buy {
  margin-left: auto; flex: 0 0 auto;
  padding: 8px 14px; border-radius: 12px;
  font: var(--weight-heavy) 14px/1 var(--face); cursor: pointer; color: #fff;
  border: var(--edge) solid rgba(0, 0, 0, .4);
  background: linear-gradient(var(--btn-green), var(--btn-green-2));
  box-shadow: 0 4px 0 var(--btn-green-3);
  display: flex; align-items: center; gap: 5px;
}
.offer .buy:active:not(:disabled) { transform: translateY(4px); box-shadow: none; }
.offer .buy:disabled { filter: grayscale(1); opacity: .55; cursor: default; }
.best {
  position: absolute; top: -9px; right: 8px;
  font-size: 9px; font-weight: var(--weight-heavy); letter-spacing: .8px;
  padding: 2px 7px; border-radius: 7px;
  background: var(--rib-pink-2); border: 2px solid rgba(0, 0, 0, .35); color: #fff;
}

/* ---- daily -------------------------------------------------------------- */

/* minmax(0, 1fr), NOT 1fr. A grid column's default minimum is min-content, so
 * a cell whose text is wider than its share pushes the whole row out of the
 * panel rather than shrinking — 32px past it on a 320px phone. */
.grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-top: 14px; }
.cell {
  position: relative; padding: 9px 3px 8px; border-radius: 12px;
  border: var(--edge) solid rgba(0, 0, 0, .4);
  background: rgba(8, 20, 36, .5);
  font-size: 10px; font-weight: var(--weight-heavy); letter-spacing: .5px; color: #fff;
}
.cell.on { background: linear-gradient(var(--btn-gold), var(--btn-gold-2)); color: var(--ink); }
.cell.done { background: linear-gradient(var(--btn-green), var(--btn-green-2)); }
.cell.wide { grid-column: span 2; }
.cell .amt { display: flex; align-items: center; justify-content: center; gap: 4px; margin-top: 6px; font-size: 14px; }

/* ---- skins -------------------------------------------------------------- */

.skins { display: grid; grid-template-columns: repeat(4, 1fr); gap: 9px; margin-top: 14px; }
.skin {
  position: relative; padding: 6px 2px 7px; border-radius: var(--r-tile); cursor: pointer;
  border: var(--edge) solid rgba(0, 0, 0, .4);
  background: rgba(8, 20, 36, .5);
  font-size: 9.5px; font-weight: var(--weight-heavy); letter-spacing: .3px; color: #fff;
  overflow: hidden;
}
.skin {
  /* the tile's index, overridden inline per tile so eight blobs bob out of
     phase; declared here so the default is a grid that at least animates */
  --i: 0;
}
.skin img {
  display: block; width: 100%; height: auto; aspect-ratio: 1;
  object-fit: contain; border-radius: 9px;
  /* THE PICKER IS EIGHT LIVE CHARACTERS, not eight photographs of them. `--i`
   * is the tile's index, set inline, and it staggers the phase so the grid
   * never pulses as one object — which is the difference between a shelf of
   * toys and a room with eight things in it. */
  animation: bob 2.6s ease-in-out infinite;
  animation-delay: calc(var(--i) * -0.31s);
  will-change: transform;
}
/* The one you are wearing bounces higher and slower, so the selection is
   visible from the corner of the eye and not only from the tick. */
.skin.sel img { animation-duration: 1.8s; }
.skin.sel { background: linear-gradient(var(--btn-green), var(--btn-green-2)); border-color: var(--green-edge); }
/* A locked skin is asleep: still, and dimmed, so the grid says what is yours
   before a single price is read. */
.skin.lock img { animation: none; filter: grayscale(.55) brightness(.75); }
@media (prefers-reduced-motion: reduce) {
  .skin img { animation: none; }
}
/* RAINBOW is the longest name in the shop and the one that decides this. */
.skin .name { display: block; margin-top: 5px; line-height: 1.1; letter-spacing: 0; font-size: 9px; }
.skin .price { display: flex; align-items: center; justify-content: center; gap: 3px; margin-top: 2px; font-size: 10px; color: var(--btn-gold); }
/* A LOCKED SKIN STILL HAS TO BE LEGIBLE. At brightness .55 and half
 * saturation every unowned blob was the same dark brown lump, so the picker
 * was showing five things to buy and no way to tell them apart — which is the
 * whole job of a picker. Dimmed enough to read as locked, bright enough to
 * read as a colour. */
.skin.lock img, .skin.lock .swatch { filter: brightness(.82) saturate(.8); }
.skin.lock { opacity: .88; }
.skin .tick { position: absolute; top: 4px; right: 5px; font-size: 13px; }

/* ---- settings ----------------------------------------------------------- */

.setrow {
  display: flex; align-items: center; gap: 10px;
  margin-top: 10px; padding: 9px 12px; border-radius: var(--r-tile);
  background: rgba(8, 20, 36, .5); border: var(--edge) solid rgba(0, 0, 0, .4);
  font-weight: var(--weight-heavy); font-size: 14px; letter-spacing: .5px;
}
.sw {
  margin-left: auto; width: 52px; height: 28px; border-radius: var(--r-pill);
  background: var(--switch-off); position: relative; cursor: pointer; flex: 0 0 auto;
  border: var(--edge) solid rgba(0, 0, 0, .35);
}
.sw.on { background: linear-gradient(var(--btn-green), var(--btn-green-2)); }
.sw::after {
  content: ""; position: absolute; top: 1px; left: 1px;
  width: 20px; height: 20px; border-radius: 50%; background: #fff; transition: left .15s;
}
.sw.on::after { left: 25px; }

/* ---- how to play -------------------------------------------------------- */

.howrow {
  display: flex; align-items: center; gap: 11px; text-align: left;
  margin-top: 10px; padding: 10px; border-radius: var(--r-tile);
  background: linear-gradient(var(--parch-top), var(--parch-bottom));
  border: var(--edge) solid var(--parch-edge); color: var(--ink);
}
/* FOUR BADGES, ONE SIZE. The four icons on this panel come from four different
 * places — a skin portrait, a drawn ledge, the HUD's own star, a starburst —
 * and each was authored at whatever size suited where it normally lives. Left
 * alone they arrive at 42, 34, 22 and 34 pixels, which reads as four rules of
 * four different importances. The box is fixed and everything inside it is
 * scaled to fill it. */
.howrow .ic {
  flex: 0 0 auto;
  width: 46px; height: 46px;
  display: flex; align-items: center; justify-content: center;
}
.howrow .ic > * { width: 100%; height: 100%; }
.howrow .ic svg { display: block; }
.howrow .ic .howblob { padding: 2px; }
.howrow b { display: block; font-size: 15px; }
.howrow small { display: block; font-size: 12px; font-weight: var(--weight-body); opacity: .88; line-height: 1.3; }

/* THE PAINT LEGEND, inside HOW TO PLAY.
 *
 * Two columns on anything wide enough for them and one on a 320px phone, so
 * four rows never push the GOT IT button off the bottom of a small screen —
 * which is the only failure mode a legend has: being below the fold on exactly
 * the device the player who needs it is holding.
 *
 * The dab is the paint's OWN colour with the colour-blind shape cut into it in
 * the outline brown, which is the same pairing the blob wears in the shaft. A
 * legend whose swatches do not match the game is worse than none.
 *
 * It is `.dab` and not `.sw` because `.sw` is the settings TOGGLE, and its
 * white ::after knob is absolutely positioned — so a swatch borrowing the name
 * came out with a white pill sitting on top of the shape it exists to show. */
.paints {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  gap: 6px; margin-top: 8px;
}
.paintrow {
  display: flex; align-items: center; gap: 9px; text-align: left;
  padding: 7px 9px; border-radius: var(--r-tile);
  background: linear-gradient(var(--parch-top), var(--parch-bottom));
  border: var(--edge) solid var(--parch-edge); color: var(--ink);
}
.paintrow .dab {
  flex: 0 0 auto; width: 36px; height: 36px; border-radius: 50%;
  border: 3px solid var(--line);
  display: flex; align-items: center; justify-content: center;
}
.paintrow b { display: block; font-size: 13px; letter-spacing: .02em; }
.paintrow small { display: block; font-size: 10px; font-weight: var(--weight-body); opacity: .8; line-height: 1.25; }

/* ---- the banner slot ----------------------------------------------------- */
/*
 * NOT SHOWN UNTIL IT IS FILLED. `display: none` is the default, and src/main.js
 * only adds `.on` when the portal reports a banner was actually placed — so a
 * build without banners never reserves a strip of screen, and a request that
 * goes unfilled leaves nothing behind.
 *
 * Above the overlay's scrim so it is not dimmed, and pinned to the bottom so it
 * sits below the panel rather than across it.
 */
#banner {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: none;
  z-index: 20;
  min-height: 50px;
  align-items: center; justify-content: center;
  background: rgba(6, 16, 30, .88);
  border-top: 2px solid rgba(0, 0, 0, .5);
}
#banner.on { display: flex; }

/* ---- the ad placeholder ------------------------------------------------- */

#adPanel .screen {
  margin-top: 12px; height: 140px;
  border: var(--edge) dashed rgba(255, 255, 255, .35); border-radius: var(--r-tile);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  font-size: 12px; font-weight: var(--weight-body); letter-spacing: 1.5px; opacity: .75;
}
#adPanel .screen b { font-size: 34px; }

/* ---- the menu ----------------------------------------------------------- */

/* The menu's panel is the one place the art shows through, so it is the one
 * panel with no gradient of its own. */
#menuPanel { background: linear-gradient(rgba(29, 62, 99, .82), rgba(19, 43, 71, .92)); }
/* EVERY IMAGE RESERVES ITS OWN BOX, AND `height: auto` IS THE WHOLE TRICK.
 *
 * The `width`/`height` ATTRIBUTES on the tag are what reserve the space: a
 * browser derives the intrinsic ratio from them and lays the box out before a
 * byte has arrived, which is what keeps the layout still. But the height
 * attribute is also a USED height, so with a CSS width of 260px the logo was
 * laid out 260 wide by 418 tall — its raw pixel height — and `object-fit: fill`
 * squashed a 1.34 image into a 0.62 box. Measured: box 260.0x418.0, natural
 * ratio 1.3397, box ratio 0.6220. It was two and a half times too tall.
 *
 * `height: auto` hands the height back to the ratio. The attributes still
 * reserve the space; nothing shifts; nothing stretches. */
#menuPanel .logo {
  display: block;
  width: min(100%, 260px); height: auto;
  margin: -4px auto 2px; border-radius: 10px;
}
/* THE ICON ROW, LABELLED. Four unlabelled glyphs is a row nobody reads: the
 * cup is the daily reward and the book is how-to-play, and neither is guessable
 * from its shape at 44px. */
/* FIXED COLUMNS, or the buttons stop being a row.
 *
 * Left to size themselves, each column took the width of its own label —
 * "HOW TO PLAY" is 72px against a 44px button — so the four buttons were
 * unevenly spaced and no longer read as a set. A fixed column with a wrapping
 * label keeps the buttons on an even pitch, and `align-items: flex-start`
 * keeps them level when one label takes two lines. */
.iconrow { display: flex; gap: 6px; justify-content: center; align-items: flex-start; margin-top: 16px; }
.iconbtn {
  display: flex; flex-direction: column; align-items: center;
  /* 10px, not 5: `.sq` casts a 4px hard shadow BELOW its border box, so a 5px
   * gap left one visible pixel between the key and its label. */
  gap: 10px;
  width: 68px;
}
.iconbtn .sq { position: relative; flex: 0 0 auto; }
/* A CHILD SELECTOR. As a descendant it also matched the "NEW" badge inside the
 * daily button and dimmed it to 72% — a badge whose entire job is to be
 * noticed. */
.iconbtn > span {
  font-size: 9px; font-weight: var(--weight-heavy);
  letter-spacing: .5px; line-height: 1.2; text-align: center;
  opacity: .72;
}

/* The run picker, on a build that carries more than one pack. */
.packlabel { font-size: 10px; font-weight: var(--weight-heavy); letter-spacing: 2px; opacity: .55; margin-top: 16px; }
.packs { display: flex; gap: 8px; margin-top: 6px; }
.pack {
  flex: 1 1 0; padding: 9px 6px; cursor: pointer;
  font: var(--weight-heavy) 12px/1.2 var(--face); color: #fff;
  border: var(--edge) solid rgba(0, 0, 0, .4); border-radius: var(--r-tile);
  background: rgba(8, 20, 36, .5);
}
.pack.on { background: linear-gradient(var(--btn-blue), var(--btn-blue-2)); border-color: var(--btn-blue-3); }
.pack b { display: block; letter-spacing: .4px; }
.pack small { display: block; font-size: 10px; font-weight: var(--weight-body); opacity: .7; margin-top: 2px; }

/* The wordmark, for a build with no logo painting. */
#menuPanel .wordmark {
  font-size: 44px; font-weight: var(--weight-heavy);
  letter-spacing: -1px; line-height: .9;
  color: var(--rib-pink); text-shadow: 0 4px 0 var(--rib-pink-2), 0 7px 14px rgba(0, 0, 0, .5);
}
#menuPanel .wordmark span { color: var(--coin); text-shadow: 0 4px 0 var(--btn-gold-3); }

/* A skin with no portrait yet still shows its colour. */
.skin .swatch { aspect-ratio: 1; border-radius: 9px; }
.skin .swatch.rainbow { background: linear-gradient(135deg, var(--tint-0), var(--coin), var(--btn-blue)); }

/* THE CIRCLE WAS EATING THE CHARACTER.
 *
 * A blob portrait is a round body that fills its 256px frame corner to corner —
 * horns, feet and all the silhouette that tells one skin from another live at
 * the very edge. Masking that square to a circle at `object-fit: cover` clips
 * it at exactly its widest point, so picking a skin showed you a blob with its
 * sides sliced off. `cover` was also a no-op that only LOOKED safe: the art is
 * square and the box is square, so it never scaled anything and never could
 * have rescued the crop.
 *
 * So the badge keeps its circle and the art is inset inside it. `contain` fits
 * the whole portrait into the padding box, and 10px of padding pulls the art
 * inside the inscribed circle with room to spare — the blob's widest point sits
 * 37px from the centre of a 54px circle. Nothing is cropped, at any skin. */
#menuPanel .hero,
#menuPanel .sidekick {
  display: block; aspect-ratio: 1;
  border-radius: 50%; padding: 10px;
  border: var(--edge) solid rgba(0, 0, 0, .35);
  box-shadow: 0 6px 0 rgba(0, 0, 0, .3);
  background: radial-gradient(circle at 50% 38%, rgba(255, 255, 255, .16), rgba(8, 20, 36, .5));
  object-fit: contain;
}
#menuPanel .hero { width: 108px; height: 108px; }

/* THE CAST, AND WHY IT MOVES.
 *
 * Three portraits on one row: the chosen skin in the middle at full size, and
 * two the player owns either side, smaller and set back. They bob on the same
 * keyframe at different delays and slightly different durations, so they never
 * sync up into one pulsing object — the whole trick of a group of characters
 * looking alive is that they are not doing it together.
 *
 * TRANSFORM AND NOTHING ELSE. No width, no margin, no filter: the animation
 * runs on the compositor, the menu never lays out again after it is built, and
 * an idle title screen costs a phone nothing.
 */
#menuPanel .cast {
  display: flex; align-items: center; justify-content: center;
  gap: clamp(4px, 2vw, 14px);
  margin: 6px auto 0;
}
#menuPanel .sidekick {
  width: 62px; height: 62px; padding: 6px;
  opacity: .82;
}
@keyframes bob {
  0%, 100% { transform: translateY(0) scale(1, 1) rotate(-2deg); }
  30%      { transform: translateY(-9px) scale(.96, 1.05) rotate(1deg); }
  55%      { transform: translateY(0) scale(1.07, .93) rotate(2deg); }
  75%      { transform: translateY(-3px) scale(.99, 1.02) rotate(0deg); }
}
#menuPanel .hero,
#menuPanel .sidekick {
  /* set per portrait, inline, so the three never bob in step */
  --delay: 0s;
  animation: bob 2.4s ease-in-out infinite;
  animation-delay: var(--delay);
  will-change: transform;
}
/* squash-and-stretch reads as weight, so the little ones are lighter and
   quicker and the hero has the slowest, heaviest bounce of the three */
#menuPanel .sidekick { animation-duration: 1.9s; }
#menuPanel .sidekick.left { animation-duration: 2.1s; }

/* Some people cannot look at a screen that never stops moving, and a title
   screen is the one they cannot get past to reach the setting. */
@media (prefers-reduced-motion: reduce) {
  #menuPanel .hero,
  #menuPanel .sidekick { animation: none; }
}

/* HOW TO PLAY, as a button that is actually pressable.
 *
 * Full width, directly under PLAY, with the book icon inline so it reads as a
 * thing rather than as a link. `.nudge` is for a player who has never finished
 * a run: it leans and settles once every few seconds, which is enough to draw
 * an eye and little enough to ignore. It stops for ever the moment they have
 * one run behind them. */
#menuPanel .b.how { display: flex; align-items: center; justify-content: center; gap: 10px; }
#menuPanel .b.how .bi { display: flex; }
#menuPanel .b.how .bi svg { display: block; }
@keyframes nudge {
  0%, 82%, 100% { transform: none; }
  86% { transform: rotate(-1.4deg) scale(1.03); }
  92% { transform: rotate(1.2deg) scale(1.03); }
}
#menuPanel .b.how.nudge { animation: nudge 3.4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  #menuPanel .b.how.nudge { animation: none; }
}

/* THE VOLUME ROWS. A settings row is normally a label and a switch on one line;
 * a slider needs the whole width to be draggable, so it takes a second line.
 * The track and the thumb are drawn rather than left to the platform, because
 * the platform's is a 2px grey line and this game does not have any of those. */
.setrow.vol { display: block; }
.setrow.vol label { display: block; margin-bottom: 6px; }

/* THE ELEMENT IS THUMB-SIZED AND THE TRACK IS DRAWN INSIDE IT.
 *
 * The input itself is 34px tall and transparent; the 14px groove is a styled
 * TRACK within it. Sizing the input to the groove instead — which is the
 * obvious way to write this — makes the whole control 14px of tappable height
 * on a game played with one thumb, and `tools/fits.mjs` fails it at a 30px
 * floor on every phone in the list. The visible groove is the same either way;
 * what changes is how much of the screen accepts a press. */
.setrow.vol input[type="range"] {
  -webkit-appearance: none; appearance: none;
  display: block; width: 100%; height: 34px; margin: 0;
  background: transparent; border: 0;
  cursor: pointer;
}
.setrow.vol input[type="range"]::-webkit-slider-runnable-track {
  height: 14px; border-radius: 9px;
  background: rgba(0, 0, 0, .3);
  border: 3px solid var(--line);
}
.setrow.vol input[type="range"]::-moz-range-track {
  height: 14px; border-radius: 9px;
  background: rgba(0, 0, 0, .3);
  border: 3px solid var(--line);
}
.setrow.vol input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 28px; height: 28px; border-radius: 50%;
  /* centre the thumb on a 20px-tall bordered track */
  margin-top: -7px;
  background: linear-gradient(var(--btn-gold), var(--combo-bottom));
  border: 3px solid var(--line);
  box-shadow: 0 3px 0 rgba(0, 0, 0, .35);
  cursor: pointer;
}
.setrow.vol input[type="range"]::-moz-range-thumb {
  width: 28px; height: 28px; border-radius: 50%;
  background: var(--btn-gold);
  border: 3px solid var(--line);
  cursor: pointer;
}

/* The blob on the first row of HOW TO PLAY: the skin the player is actually
 * wearing, so the panel is about their game rather than about a game. */
.howblob {
  display: block; width: 42px; height: 42px;
  padding: 3px; border-radius: 50%;
  object-fit: contain;
  background: rgba(8, 20, 36, .45);
}

/* PLAY is the biggest thing on the screen after the logo. */
#menuPanel .b.big { font-size: 1.22em; padding-top: .78em; padding-bottom: .82em; }

/* SKINS and SHOP share a row, which is what pays for HOW TO PLAY having one. */
#menuPanel .rowb { display: flex; gap: 10px; }
#menuPanel .rowb .b { flex: 1; margin-top: clamp(5px, calc(2.6vh - 9px), 12px); }

/* ---- the menu fits the height it has ------------------------------------ */

/* THE FIRST SCREEN MUST NOT SCROLL.
 *
 * `#menuPanel` was a fixed 786px stack — a 194px logo, a 108px portrait, three
 * 54px buttons and the rest — laid out with no reference to how much room there
 * was. On a full-height desktop that fits. In a real browser window with its
 * chrome, on a laptop, in the portal's own 1031x580 frame or on any phone
 * shorter than a modern flagship, it does not: the overlay took up the slack
 * and scrolled, so SHOP and SETTINGS sat under the fold on the first screen of
 * the game.
 *
 * The overlay's scroll is still right for MODALS — that is what put the close
 * button back on screen — but a menu that scrolls is a menu with a button
 * nobody found.
 *
 * So the rhythm is tied to the window instead of nailed down in pixels. `vh`
 * rather than breakpoints because the panel then shrinks CONTINUOUSLY: there is
 * no height at which it is one pixel too tall and no step to land badly on. The
 * clamps keep it from growing past the designed size on a tall screen or
 * collapsing into unreadability on a short one. `#column` is `height: 100%`, so
 * a vh is the column's own height to within the page's padding.
 */
/* THE COEFFICIENTS CARRY AN OFFSET — `calc(38vh - 100px)` rather than a bare
 * `27vh` — because a plain multiple of vh cannot do both jobs at once. Set high
 * enough to reach the designed size on a desktop it barely shrinks on a phone;
 * set low enough to shrink on a phone it never reaches the designed size at all,
 * and the menu is permanently smaller than it was drawn. A steeper line with a
 * negative offset SATURATES: full size from about 950px of height upward, and
 * genuinely compact by 568. The clamp bounds are the floor and the ceiling. */
#menuPanel { padding: clamp(9px, calc(4.6vh - 18px), 22px) 16px clamp(8px, calc(3.8vh - 15px), 18px); }
#menuPanel .logo { width: min(100%, clamp(104px, calc(38vh - 100px), 260px)); margin-bottom: clamp(0px, .4vh, 2px); }
#menuPanel .wordmark { font-size: clamp(30px, calc(8vh - 20px), 44px); }
#menuPanel .hero {
  width: clamp(46px, calc(20vh - 55px), 108px); height: clamp(46px, calc(20vh - 55px), 108px);
  padding: clamp(4px, calc(2vh - 6px), 10px);
}
#menuPanel .sidekick {
  width: clamp(28px, calc(12vh - 33px), 62px); height: clamp(28px, calc(12vh - 33px), 62px);
  padding: clamp(3px, calc(1.2vh - 4px), 6px);
}
#menuPanel .cast { margin-top: clamp(2px, calc(1.4vh - 5px), 6px); }
#menuPanel .b { margin-top: clamp(5px, calc(2.6vh - 9px), 12px); padding: clamp(7px, calc(3vh - 11px), 13px); font-size: clamp(15px, calc(3.4vh - 9px), 19px); }
#menuPanel .rowc { margin-top: clamp(5px, calc(3vh - 11px), 14px); }
#menuPanel .packlabel { margin-top: clamp(5px, calc(3.4vh - 12px), 16px); }
#menuPanel .pack { padding: clamp(5px, calc(2.4vh - 8px), 9px) 6px; }
#menuPanel .iconrow { margin-top: clamp(6px, calc(3.4vh - 12px), 16px); }

/* AND WHEN THERE IS GENUINELY NOT ENOUGH ROOM, SOMETHING GOES.
 *
 * The clamps above shrink the menu down to about 500px, which covers every
 * phone and every browser window. They cannot cover 360px of height — one of
 * the frames the portal serves a game in — because a logo, a portrait, three
 * buttons, a tower picker and a labelled icon row do not fit in 360px at any
 * size that is still legible. Shrinking further would only mean everything is
 * present and nothing is readable.
 *
 * So at that height the menu drops what it can afford to lose. The portrait is
 * the first to go: it is the one thing on this screen that is decoration rather
 * than navigation, and the skin it shows is also on the blob in play and on the
 * SKINS screen. Then the picker's label, whose two buttons already say THE
 * CLIMB and THE SPIRE. Then the icon captions, which is the only compromise
 * here that costs anything — and it costs less than a PLAY button under the
 * fold. */
@media (max-height: 430px) {
  /* the sidekicks go one step before the hero does: two of the three are pure
     decoration, and the middle one is at least the skin you are playing as */
  #menuPanel .sidekick { display: none; }
  #menuPanel .cast { display: none; }
  #menuPanel .packlabel { display: none; }
  #menuPanel .packs { margin-top: 8px; }
  #menuPanel .iconbtn > span { display: none; }
  #menuPanel .iconrow { margin-top: 8px; }
  #menuPanel .logo { width: min(100%, 78px); }
  #menuPanel .rowc { margin-top: 5px; }
  #menuPanel { padding: 5px 12px 5px; }
  /* the overlay's 22px of headroom is for a modal's close button; the menu has
     no close button and cannot spare it at this height */
  #menu.overlay { padding: 6px 10px 6px; }
  #menuPanel .b { margin-top: 5px; padding: 5px; font-size: 14px; }
  #menuPanel .pack { padding: 4px 6px; font-size: 11px; }
  #menuPanel .iconbtn { width: 54px; }
  #menuPanel .iconbtn .ic { width: 36px; height: 36px; }
  /* HOW TO PLAY keeps its place — it is the reason a cold player gets anywhere
     — but it stops being a full-height button and stops moving. */
  #menuPanel .b.how { padding: 4px; font-size: 12px; gap: 6px; }
  #menuPanel .b.how .bi svg { width: 16px; height: 16px; }
  #menuPanel .b.how.nudge { animation: none; }
  #menuPanel .b.big { font-size: 1em; padding-top: 5px; padding-bottom: 5px; }
  #menuPanel .rowb .b { margin-top: 5px; }
}
