/* ==========================================================================
   Goblin Miners — components.css
   Every element in CONTRACT §5 and every state in its data-* table.

   Two conventions worth knowing before reading:

   1. Icons arrive as <svg><use href="#id"></use></svg> with no width or height
      attribute. An unsized SVG defaults to 300x150, so every icon class here
      sets an explicit box. A new icon with no rule will blow out its row.

   2. Bars (rock HP, event timer) are driven by transform: scaleX(var(--gm-fill))
      per CONTRACT §4/§8. The game sets --gm-fill with setProperty; this file is
      the only place the transform itself is written, so the two never compete.
   ========================================================================== */

/* `hidden` is the contract's show/hide mechanism. Every display rule below is
   written assuming this wins. */
[hidden] { display: none !important; }

/* --------------------------------------------------------------- surfaces */

.gm-core > .gm-res,
.gm-panel .gm-tabs,
.gm-tabpanes {
  background: var(--gm-bg-2);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
}

/* ========================================================================== */
/* MINE VIEW                                                                  */
/* ========================================================================== */

/* ==========================================================================
   THE CAVE

   The brief was that it read as a loading screen: flat horizontal stripes on a
   dark panel. Four things make a space feel underground, and none of them is a
   stripe:

   1. Strata are never level. Sedimentary rock in a cut face runs at a slight
      angle, so every band here is a few degrees off horizontal. That one change
      is most of the difference between "rock face" and "progress bar".
   2. Rock is broken, not woven. Near-vertical fracture lines cross the bands at
      two different irregular periods (53px and 89px — coprime, so the pattern
      does not visibly repeat), which stops the eye finding the tile.
   3. Darkness closes in. A vignette pulls the corners toward black so the space
      has no visible edges — you are inside something, not looking at a panel.
   4. Light has a source. A warm pool sits over the working area, falling off in
      hard steps. Warm dark, warmer light — the shadow here is earth, not night,
      so the vignette closes toward umber rather than toward black. Ore stays
      the only saturated thing on screen (ARTSTYLE §1).

   Every stop is hard. A smooth gradient anywhere in here would immediately
   read as airbrush against the pixel art it surrounds.
   ========================================================================== */

.gm-mine {
  background-color: var(--gm-bg);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);

  /* One number for the size of the rock, because four things now depend on it:
     the sprite, the HP bar under it, and the two radii the goblin crew stands
     at. It used to be typed separately in four media queries, which is the same
     shape of bug as the floor slab and .gm-goblins' padding both being 40px in
     two files. Whole multiples of the 32px source grid only. */
  --gm-rock-size: 192px;                       /* 6x */
  --gm-goblin-size: 48px;                      /* 2x the 24px goblin grid */

  /* Within-band depth, 0 at the top of the current ore band and 1 at the
     bottom, published by the game (see the DESCENT block below). The fallback
     is what the cave looks like the moment you enter a band, so the stylesheet
     is correct whether or not the property is ever set. */
  --gm-depth: 0;
}

/* The far wall: tilted sedimentary banding, crossed by fractures.
   The fractures deepen with --gm-depth: rock under more rock is more broken,
   and it is one of the three continuous descent cues (see DESCENT below). */
.gm-mine__sky {
  background-color: var(--gm-bg);
  background-image:
    /* fractures — two coprime periods so the repeat never becomes visible */
    repeating-linear-gradient(94deg,
      transparent 0 53px, rgb(0 0 0 / calc(40% + 26% * var(--gm-depth))) 53px 55px),
    repeating-linear-gradient(87deg,
      transparent 0 89px, rgb(0 0 0 / calc(28% + 24% * var(--gm-depth))) 89px 91px),
    /* bedding planes, a few degrees off level */
    repeating-linear-gradient(174deg,
      var(--gm-bg-2)  0    11px,
      var(--gm-bg-3)  11px 19px,
      var(--gm-bg-2)  19px 28px,
      var(--gm-bg)    28px 36px);
}

/* ==========================================================================
   THE CEILING  (owner's pick from propuestas/descenso.html, option A)

   A band of rock across the top of the cave that descends with --gm-depth, with
   stalactites hanging off it, answered by stalagmites off the floor. This is
   what turns the descent from a change of brightness into a change of SPACE.

   Both live on .gm-mine__sky's pseudo-elements and not on .gm-mine__strata's,
   which would have been the tidier place: .gm-mine__strata carries the band
   opacity, from .28 to .62, and a pseudo-element inherits its parent's opacity,
   so the ceiling would have been two-thirds transparent in a coal seam. They
   also cannot go on .gm-mine's own ::before/::after — those are the floor slab
   and the lantern.

   WHAT IT MUST NOT COVER. The depth readout is grid-row 1 at z-index 3 and the
   event card is absolute at z-index 4; this sits inside .gm-mine__sky at
   z-index 0, so both draw over it and neither can be obscured. The reach is
   also capped at 42px, which is about the height of the readout, so at full
   depth the metres sit ON the rock rather than in front of teeth.

   THE TEETH DO NOT GROW. The band descends and takes its stalactites with it,
   which reads as a ceiling coming down. Teeth that lengthen read as the same
   rock stretching, which is not a thing rock does. Stalagmites do not move at
   all — a floor that sprouts spikes as you descend is a different idea, and a
   worse one.

   They are scenery, not particles: data-particles="off" leaves them alone. It
   turns off the pooled ore chips, and switching off the cave with them would be
   a settings switch that redecorates the game.
   ========================================================================== */

/* ---- v1.9: three sizes of tooth, and every one of them shaded ----

   The owner: *"Currate un poco las estalactitas y estalagmitas, hazlas de tamaños
   variados, dales un poco de volumen"*. Both were one size and flat: a single
   silhouette in --gm-stone, repeated at two pitches. Two things change.

   SIZE. Three families a side rather than one. v1.10 grew all six, the owner:
   *"Estalactitas y estalagmitas me gusta el diseño, pero más grandes y visibles de
   manera general"* — so 28px, 20px and 12px against the old 20 / 12 / 8, bases
   16px / 10px / 6px wide against 10 / 6 / 4, on tighter pitches (71 / 43 / 29 up
   top, 83 / 53 / 37 below) so more of them are in view at once. All six pitches
   are still prime, so no two families ever share a period, the roof does not
   repeat inside a viewport of any width, and no floor spike lines up under a
   ceiling one — which is what would make the cave read as a zip.

   The roof band gave up the 6px the teeth took (8+14 to 6+12), so the total reach
   is 46px against the old 42 and the depth readout still sits on rock rather than
   behind a mouthful. The gradient stacks are GENERATED, not typed: thirty
   repeating-linear-gradients with a matching background-position list is how a
   tooth ends up one pixel away from its own shadow.

   VOLUME. Every course carries its own shading, encoded in the same gradient as
   its body: 1px of --gm-stone-lt down the left edge, the body in --gm-stone, 1px
   of --gm-bg-3 down the right. Light from the top-left, the same rule the sprites
   follow (ARTSTYLE section 2). One gradient per course, ten a side — the whole
   thing is still a static background, painted once and never animated.

   The reach stays capped at about 42px: the band is 8px of rock at the top of a
   band and 22px at the bottom, plus 20px of the tallest tooth. That is the height
   of the depth readout, which draws over this at z-index 3 and must never be
   fought for space. */
.gm-mine__sky::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  --gm-roof: calc(6px + 12px * var(--gm-depth));
  height: calc(var(--gm-roof) + 28px);
  background-image:
    repeating-linear-gradient(90deg, transparent 0 18px, var(--gm-stone-lt) 18px 19px, var(--gm-stone) 19px 33px, var(--gm-bg-3) 33px 34px, transparent 34px 71px),
    repeating-linear-gradient(90deg, transparent 0 19px, var(--gm-stone-lt) 19px 20px, var(--gm-stone) 20px 32px, var(--gm-bg-3) 32px 33px, transparent 33px 71px),
    repeating-linear-gradient(90deg, transparent 0 20px, var(--gm-stone-lt) 20px 21px, var(--gm-stone) 21px 31px, var(--gm-bg-3) 31px 32px, transparent 32px 71px),
    repeating-linear-gradient(90deg, transparent 0 21px, var(--gm-stone-lt) 21px 22px, var(--gm-stone) 22px 30px, var(--gm-bg-3) 30px 31px, transparent 31px 71px),
    repeating-linear-gradient(90deg, transparent 0 23px, var(--gm-stone-lt) 23px 24px, var(--gm-stone) 24px 28px, var(--gm-bg-3) 28px 29px, transparent 29px 71px),
    repeating-linear-gradient(90deg, transparent 0 24px, var(--gm-stone-lt) 24px 25px, var(--gm-stone) 25px 27px, var(--gm-bg-3) 27px 28px, transparent 28px 71px),
    repeating-linear-gradient(90deg, transparent 0 25px, var(--gm-stone-lt) 25px 26px, var(--gm-bg-3) 26px 27px, transparent 27px 71px),
    repeating-linear-gradient(90deg, transparent 0 7px, var(--gm-stone-lt) 7px 8px, var(--gm-stone) 8px 16px, var(--gm-bg-3) 16px 17px, transparent 17px 43px),
    repeating-linear-gradient(90deg, transparent 0 8px, var(--gm-stone-lt) 8px 9px, var(--gm-stone) 9px 15px, var(--gm-bg-3) 15px 16px, transparent 16px 43px),
    repeating-linear-gradient(90deg, transparent 0 9px, var(--gm-stone-lt) 9px 10px, var(--gm-stone) 10px 14px, var(--gm-bg-3) 14px 15px, transparent 15px 43px),
    repeating-linear-gradient(90deg, transparent 0 10px, var(--gm-stone-lt) 10px 11px, var(--gm-stone) 11px 13px, var(--gm-bg-3) 13px 14px, transparent 14px 43px),
    repeating-linear-gradient(90deg, transparent 0 11px, var(--gm-stone-lt) 11px 12px, var(--gm-bg-3) 12px 13px, transparent 13px 43px),
    repeating-linear-gradient(90deg, transparent 0 21px, var(--gm-stone-lt) 21px 22px, var(--gm-stone) 22px 26px, var(--gm-bg-3) 26px 27px, transparent 27px 29px),
    repeating-linear-gradient(90deg, transparent 0 22px, var(--gm-stone-lt) 22px 23px, var(--gm-stone) 23px 25px, var(--gm-bg-3) 25px 26px, transparent 26px 29px),
    repeating-linear-gradient(90deg, transparent 0 23px, var(--gm-stone-lt) 23px 24px, var(--gm-bg-3) 24px 25px, transparent 25px 29px),
    linear-gradient(180deg,
      var(--gm-bg)    0 2px,
      var(--gm-bg-3)  2px calc(100% - 3px),
      var(--gm-stone) calc(100% - 3px) 100%);
  background-size:
    100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% var(--gm-roof);
  background-position:
    0 var(--gm-roof), 0 calc(var(--gm-roof) + 4px), 0 calc(var(--gm-roof) + 8px), 0 calc(var(--gm-roof) + 12px), 0 calc(var(--gm-roof) + 16px), 0 calc(var(--gm-roof) + 20px), 0 calc(var(--gm-roof) + 24px), 0 var(--gm-roof), 0 calc(var(--gm-roof) + 4px), 0 calc(var(--gm-roof) + 8px), 0 calc(var(--gm-roof) + 12px), 0 calc(var(--gm-roof) + 16px), 0 var(--gm-roof), 0 calc(var(--gm-roof) + 4px), 0 calc(var(--gm-roof) + 8px), 0 0;
  background-repeat: no-repeat;
}

/* Stalagmites. They stand ON the floor slab, so the strip is parked 38px up — the
   slab is 40px tall and its lit edge is its top 2px. Same three sizes as the roof
   and the same shading, built bottom-up: the widest course is the one on the
   ground. The strip is 20px tall to hold the biggest of them. */
.gm-mine__sky::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 38px;
  height: 28px;
  background-image:
    repeating-linear-gradient(90deg, transparent 0 42px, var(--gm-stone-lt) 42px 43px, var(--gm-bg-3) 43px 44px, transparent 44px 83px),
    repeating-linear-gradient(90deg, transparent 0 41px, var(--gm-stone-lt) 41px 42px, var(--gm-stone) 42px 44px, var(--gm-bg-3) 44px 45px, transparent 45px 83px),
    repeating-linear-gradient(90deg, transparent 0 40px, var(--gm-stone-lt) 40px 41px, var(--gm-stone) 41px 45px, var(--gm-bg-3) 45px 46px, transparent 46px 83px),
    repeating-linear-gradient(90deg, transparent 0 38px, var(--gm-stone-lt) 38px 39px, var(--gm-stone) 39px 47px, var(--gm-bg-3) 47px 48px, transparent 48px 83px),
    repeating-linear-gradient(90deg, transparent 0 37px, var(--gm-stone-lt) 37px 38px, var(--gm-stone) 38px 48px, var(--gm-bg-3) 48px 49px, transparent 49px 83px),
    repeating-linear-gradient(90deg, transparent 0 36px, var(--gm-stone-lt) 36px 37px, var(--gm-stone) 37px 49px, var(--gm-bg-3) 49px 50px, transparent 50px 83px),
    repeating-linear-gradient(90deg, transparent 0 35px, var(--gm-stone-lt) 35px 36px, var(--gm-stone) 36px 50px, var(--gm-bg-3) 50px 51px, transparent 51px 83px),
    repeating-linear-gradient(90deg, transparent 0 16px, var(--gm-stone-lt) 16px 17px, var(--gm-bg-3) 17px 18px, transparent 18px 53px),
    repeating-linear-gradient(90deg, transparent 0 15px, var(--gm-stone-lt) 15px 16px, var(--gm-stone) 16px 18px, var(--gm-bg-3) 18px 19px, transparent 19px 53px),
    repeating-linear-gradient(90deg, transparent 0 14px, var(--gm-stone-lt) 14px 15px, var(--gm-stone) 15px 19px, var(--gm-bg-3) 19px 20px, transparent 20px 53px),
    repeating-linear-gradient(90deg, transparent 0 13px, var(--gm-stone-lt) 13px 14px, var(--gm-stone) 14px 20px, var(--gm-bg-3) 20px 21px, transparent 21px 53px),
    repeating-linear-gradient(90deg, transparent 0 12px, var(--gm-stone-lt) 12px 13px, var(--gm-stone) 13px 21px, var(--gm-bg-3) 21px 22px, transparent 22px 53px),
    repeating-linear-gradient(90deg, transparent 0 29px, var(--gm-stone-lt) 29px 30px, var(--gm-bg-3) 30px 31px, transparent 31px 37px),
    repeating-linear-gradient(90deg, transparent 0 28px, var(--gm-stone-lt) 28px 29px, var(--gm-stone) 29px 31px, var(--gm-bg-3) 31px 32px, transparent 32px 37px),
    repeating-linear-gradient(90deg, transparent 0 27px, var(--gm-stone-lt) 27px 28px, var(--gm-stone) 28px 32px, var(--gm-bg-3) 32px 33px, transparent 33px 37px);
  background-size:
    100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px, 100% 4px;
  background-position:
    0 0px, 0 4px, 0 8px, 0 12px, 0 16px, 0 20px, 0 24px, 0 8px, 0 12px, 0 16px, 0 20px, 0 24px, 0 16px, 0 20px, 0 24px;
  background-repeat: no-repeat;
}

/* ==========================================================================
   THE VEIN

   This one element carries the identity of the band you are standing in, and
   until v1.7 it drew two straight 2-3px stripes every 96px at 14% opacity. Do
   the arithmetic and it is 5px of ore per 96px of wall — **0.7% to 1.8% ore ink
   on the whole cave**, depending on the band. That is why the owner called it
   "dos líneas perpendiculares": it was not a faint vein, it was very nearly
   nothing, drawn straight.

   A vein is now three things, and the third is what makes it a vein:

   1. It is THICK. Ten pixels of ore with a five pixel stringer below it, not
      two and three. The band colour has to be legible as a colour.
   2. It still runs with the bedding at 174deg, so it reads as part of the rock
      rather than as an overlay (ARTSTYLE section 5, rule 1).
   3. It is BROKEN. Two near-vertical mask layers at 47px and 89px — coprime, so
      the combined break pattern does not repeat for 4183px — cut it into runs
      of uneven length with uneven gaps, and a third mask across the vein gives
      it a bright core and a dimmer rim. Ore does not run in ruled lines; it
      pinches out, jumps a fracture and picks up again.

   Masks compose with `intersect`. If a browser has mask-image but not
   mask-composite the layers union instead, which degrades to an unbroken thick
   vein — still a vein. If it has neither, the @supports gate leaves the old
   stripes alone. Nothing here can fail into a wall painted solid ore.
   ========================================================================== */
.gm-mine__strata {
  color: var(--gm-ore-coal);
  opacity: .28;
  background-image:
    repeating-linear-gradient(174deg,
      transparent   0    26px,
      currentColor  26px 38px,   /* the vein: twelve pixels of actual ore */
      transparent   38px 50px,
      currentColor  50px 56px,   /* a stringer running with it */
      transparent   56px 96px);
}

/* The break layers are deliberately MOSTLY opaque — 39 of 47 and 73 of 89. They
   are there to make the vein irregular, not to make it scarce, and the first
   pass had them cutting half the ore away and landing back where it started. */
@supports (mask-image: linear-gradient(#000, #000)) {
  .gm-mine__strata {
    -webkit-mask-image:
      repeating-linear-gradient(84deg,  #000 0 39px, transparent 39px 47px),
      repeating-linear-gradient(96deg,  #000 0 73px, transparent 73px 89px),
      repeating-linear-gradient(174deg, #000 0 6px,  rgb(0 0 0 / 50%) 6px 13px);
    mask-image:
      repeating-linear-gradient(84deg,  #000 0 39px, transparent 39px 47px),
      repeating-linear-gradient(96deg,  #000 0 73px, transparent 73px 89px),
      repeating-linear-gradient(174deg, #000 0 6px,  rgb(0 0 0 / 50%) 6px 13px);
    -webkit-mask-composite: source-in, source-in;
            mask-composite: intersect, intersect;
  }
}

/* Loose ore in the surrounding rock: single specks and the odd nodule, off the
   vein proper. Two coprime tiles — 167x97 and 89x131 — so the scatter never
   lines up with itself or with the fractures behind it, and it inherits both
   the band colour and the band opacity from the parent.

   These ARE masked. A mask applies to the whole element subtree, generated
   content included, so a pseudo-element cannot escape its parent's mask — the
   first draft of this comment claimed the opposite and it was simply wrong. It
   turns out to be the better behaviour: the specks are 1-3px against 8px and
   16px gaps, so about two thirds survive whole and the rest vanish or dim, and
   loose ore that pinches out on the same fractures as the vein is more coherent
   than loose ore that ignores them. */
.gm-mine__strata::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    radial-gradient(circle 2px at 41px 23px,  currentColor 0 100%, transparent 100%),
    radial-gradient(circle 1px at 118px 71px, currentColor 0 100%, transparent 100%),
    radial-gradient(circle 3px at 26px 96px,  currentColor 0 100%, transparent 100%),
    radial-gradient(circle 1px at 63px 14px,  currentColor 0 100%, transparent 100%);
  background-size: 167px 97px, 167px 97px, 89px 131px, 89px 131px;
}

/* The cave floor: a lit top edge catching the lantern, then a hard drop into
   shadow, so the space has a bottom rather than fading out.

   It used to be paired with a 40px padding-bottom on .gm-goblins in layout.css
   — two declarations in two files that had to be changed together. The crew has
   moved up around the rock face and that pairing is gone; this height is now
   free to change on its own. */
.gm-mine::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 40px;
  z-index: 1;
  pointer-events: none;
  background-image:
    /* rubble notches along the lit edge */
    repeating-linear-gradient(90deg,
      transparent 0 17px, rgba(0, 0, 0, .45) 17px 20px),
    linear-gradient(180deg,
      var(--gm-stone)  0    2px,
      var(--gm-bg-3)   2px  7px,
      var(--gm-bg)     7px  100%);
}

/* Lantern light and the dark closing in. Sits above the walls and the floor
   but below the rock, the goblins and the effects, so it lights them rather
   than washing over them — which is also why deepening the vignette never makes
   the rock or the crew harder to see, only the space around them.

   Every stop is a named custom property because a hard stop needs BOTH sides to
   resolve to exactly the same number. Written inline, two calc() expressions
   that happen to be equal are still two expressions, and one rounding step apart
   is a visible 1px smear against pixel art. */
.gm-mine::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;

  --gm-lit-1: 38%;
  --gm-lit-2: 62%;
  --gm-lit-3: 82%;
  --gm-close-1: calc(44% - 15% * var(--gm-depth));
  --gm-close-2: calc(60% - 14% * var(--gm-depth));
  --gm-close-3: calc(76% - 12% * var(--gm-depth));

  background-image:
    /* warm pool over the working area, falling off in steps. It shrinks and
       dims with depth: one lantern lights less the further it is carried. */
    radial-gradient(ellipse calc(48% - 11% * var(--gm-depth))
                            calc(44% - 10% * var(--gm-depth)) at 50% 46%,
      rgb(255 162 58 / calc(17% - 5% * var(--gm-depth))) 0 var(--gm-lit-1),
      rgb(255 162 58 / calc(9% - 3% * var(--gm-depth))) var(--gm-lit-1) var(--gm-lit-2),
      rgb(255 162 58 / calc(4% - 1% * var(--gm-depth))) var(--gm-lit-2) var(--gm-lit-3),
      transparent var(--gm-lit-3)),
    /* the cave closing in — banded, not photographic. With depth the first
       dark band starts nearer the centre and every band gets heavier, so the
       lit area contracts around the working face. */
    radial-gradient(ellipse 76% 74% at 50% 48%,
      transparent 0 var(--gm-close-1),
      rgb(16 11 8 / calc(28% + 14% * var(--gm-depth))) var(--gm-close-1) var(--gm-close-2),
      rgb(16 11 8 / calc(56% + 16% * var(--gm-depth))) var(--gm-close-2) var(--gm-close-3),
      rgb(16 11 8 / calc(80% + 18% * var(--gm-depth))) var(--gm-close-3) 100%);
}

/* Ambient tint keys off data-band on the mine — the *depth's* dominant ore,
   which changes rarely. It must never key off .gm-rock's data-ore: under the
   Gaussian roll each rock picks its own type several times a second, so the
   whole cave would strobe. The rock sprite follows data-ore; everything
   ambient follows data-band. (This also drops the :has() dependency.)

   Two axes, not one. After the palette went warm, hue alone could no longer
   separate the three warm bands — copper and gold tinting an already-brown
   wall land in nearly the same place, and coal barely registers at all. So
   each band also sets the seam *density* via opacity. That ladder does double
   duty: it makes the eight bands distinguishable, and it makes the wall
   visibly richer as you descend, which is the same progression already built
   into the rock sprites' seam counts. Coal is a poor, nearly bare wall;
   voidstone is shot through with the stuff. */
/* The ladder moved up with the redraw. It had to: the vein used to put 0.7% to
   1.8% ore ink on the wall and the top of that range was still invisible. The
   shape of the ladder is unchanged — poor bare rock at coal, shot through with
   the stuff at voidstone — but the whole thing is about 3.5x heavier, which is
   the difference between a band you can name and a band you cannot. */
.gm-mine[data-band="coal"]       .gm-mine__strata { color: var(--gm-ore-coal);       opacity: .28; }
.gm-mine[data-band="copper"]     .gm-mine__strata { color: var(--gm-ore-copper);     opacity: .42; }
.gm-mine[data-band="iron"]       .gm-mine__strata { color: var(--gm-ore-iron);       opacity: .40; }
.gm-mine[data-band="silver"]     .gm-mine__strata { color: var(--gm-ore-silver);     opacity: .48; }
.gm-mine[data-band="gold"]       .gm-mine__strata { color: var(--gm-ore-gold);       opacity: .52; }
.gm-mine[data-band="mithril"]    .gm-mine__strata { color: var(--gm-ore-mithril);    opacity: .54; }
.gm-mine[data-band="adamantite"] .gm-mine__strata { color: var(--gm-ore-adamantite); opacity: .58; }
.gm-mine[data-band="voidstone"]  .gm-mine__strata { color: var(--gm-ore-voidstone);  opacity: .62; }
.gm-mine[data-band="starmetal"]  .gm-mine__strata { color: var(--gm-ore-starmetal);  opacity: .66; }

/* ==========================================================================
   DESCENT  (HANDOFF section 4.3)

   data-band gives the cave a STEP at every ore boundary and nothing in
   between, so a thousand metres of copper looks exactly like the first metre
   of copper. --gm-depth is the missing continuous half: 0 at the top of the
   current band, 1 at the bottom, and every reading of it is above — the
   fractures in .gm-mine__sky, and the lantern and the vignette in
   .gm-mine::after. Nothing else keys off it.

   The property is published by the game on .gm-mine, next to data-band. It has
   a 0 fallback declared on .gm-mine, so until the game sets it the cave simply
   looks the way the top of a band looks, which is the correct picture for a
   player who has just arrived — there is no broken state.

   Two things about the shape of it, because they are easy to get wrong:

   - It RESETS at every boundary. That is deliberate and it is why the band step
     has to be the louder of the two: crossing into iron must read as a change
     of place, not as the cave suddenly brightening. The continuous ramp is
     therefore kept smaller than the step it sits inside — roughly 15% of the
     vignette's radius against a full change of hue and seam density.
   - Everything it moves sits BELOW the sprites in the stack. The cave closes
     in; the rock, the crew and the numbers never dim with it.
   ========================================================================== */

/* ---- depth readout ---- */

.gm-mine__depth {
  display: flex;
  align-items: baseline;
  gap: var(--gm-gap-sm);
  padding: var(--gm-gap);
  /* Reserves the corner .gm-morale sits in (same grid cell, layout.css) so
     .gm-climb's wrapped seam chips - the one thing still flowing this far
     right - stop short of it instead of running a chip row under the
     Morale card. (.gm-mine__layer-name used to need this too; it is
     `position: absolute` now and centred instead, so it no longer does.) */
  padding-right: calc(var(--gm-morale-w) + var(--gm-gap) * 2);
  flex-wrap: wrap;
}

.gm-mine__depth-value {
  font-family: var(--gm-font-num);
  font-size: 22px;
  font-weight: 700;
  color: var(--gm-ink);
  line-height: 1;
}

.gm-mine__depth-unit {
  font-family: var(--gm-font-num);
  font-size: 13px;
  color: var(--gm-ink-dim);
}

/* Centred on .gm-mine's own width (position:absolute + left:50% resolve
   against the ancestor's padding box, which is the full row - unaffected
   by .gm-mine__depth's own padding, so this stays centred regardless of
   what .gm-mine__depth reserves on its right for .gm-morale). Framed the
   same way that card is - border, translucent fill, same radius - so the
   two read as one family instead of a plain label floating loose. Taken
   out of the flex flow entirely, so it can no longer chase the same
   corner .gm-morale sits in the way it used to.

   A flex row now, not a single label (v1.18.4): the owner wanted the one
   button that decides how you dig paired with the seam name rather than
   buried in Settings - *"no quiero que esté ahí quiero que esté al lado
   del nombre de la veta en el centro arriba"*. `.gm-mine__stay` is the
   button; `.gm-mine__layer-name` keeps its own truncation so a long seam
   name cannot push the button out of the plaque. */
.gm-mine__seam {
  position: absolute;
  top: var(--gm-gap);
  left: 50%;
  transform: translateX(-50%);
  max-width: min(280px, 65%);
  display: flex;
  align-items: center;
  gap: var(--gm-gap-sm);
  padding: 3px 10px;
  background: color-mix(in srgb, var(--gm-bg-2) 82%, transparent);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
}

.gm-mine__layer-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--gm-amber);
}

/* Same toggle language as .gm-rail__btn[aria-pressed] - gold and its own
   border when on, so "you are camped here" reads the same way "this panel
   is open" already does. `hidden` until Depth Control is owned (ui.js),
   the same rule the three-button Settings row it replaced followed. */
.gm-mine__stay {
  appearance: none;
  -webkit-appearance: none;
  flex: none;
  font: inherit;
  font-size: 11px;
  letter-spacing: .02em;
  padding: 2px 8px;
  color: var(--gm-ink-dim);
  background: var(--gm-bg-2);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
  cursor: pointer;
}

.gm-mine__stay:hover { color: var(--gm-ink); }

.gm-mine__stay[aria-pressed="true"] {
  color: var(--gm-gold);
  border-color: var(--gm-gold);
  background: var(--gm-bg-3, var(--gm-bg-2));
}

/* ---- the rock ---- */

.gm-rock {
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  background: none;
  padding: var(--gm-gap);
  margin: 0;
  cursor: pointer;
  display: grid;
  justify-items: center;
  gap: var(--gm-gap-sm);
  color: inherit;
  font: inherit;
  touch-action: manipulation;
}

/* The hero of the screen. Sized in whole multiples of the 32px source grid
   (--gm-rock-size, 6x by default) — a fractional scale would render some source
   pixels one device pixel wide and their neighbours two, which is exactly the
   mush that makes pixel art look cheap. */
.gm-rock__sprite {
  width: var(--gm-rock-size);
  height: var(--gm-rock-size);
}

.gm-rock__hpbar {
  width: calc(var(--gm-rock-size) - 16px);
  max-width: 100%;
  height: 8px;
  background: var(--gm-bg);
  border: 1px solid var(--gm-line);
  overflow: hidden;
}

.gm-rock__hpfill {
  width: 100%;
  height: 100%;
  transform-origin: left center;
  transform: scaleX(var(--gm-fill, 1));
  background: var(--gm-red);
}

/* Plenty of HP left reads as healthy rock, not as danger. */
.gm-rock__hpbar .gm-rock__hpfill { background: var(--gm-green); }

.gm-rock__hptext {
  font-family: var(--gm-font-num);
  font-size: 11px;
  color: var(--gm-ink-dim);
}

/* ==========================================================================
   THE CREW  (HANDOFF section 4.2)

   Twelve pooled goblins used to sit in a centred flex row under the DIG button,
   which read as a status bar: a count of something you own rather than a gang
   of idiots hitting a rock. They are now placed around the rock face.

   How the placement works, and why it is done this way:

   - The layer shares the rock's grid area (layout.css), so 50%/50% of this box
     IS the centre of the rock, at every breakpoint, with no magic numbers. That
     sharing needs `grid-column: 1` named on BOTH .gm-rock and .gm-goblins and
     it shipped once without them — see the note on .gm-rock in layout.css. Every
     number below is meaningless if those two declarations go away.
   - Each of the twelve slots is a fixed station, polar about that centre. The
     game only decides HOW MANY are visible and always fills the pool in order,
     so :nth-child is a stable identity: slot 1 is always the front-left post.
   - Stations alternate left, right, left, right outward, so the crew is
     symmetrical at any headcount from one goblin to twelve, and it grows
     outward from the rock rather than filling a row left to right.
   - The inner ring crosses the boulder's edge by four to seven pixels on
     purpose: that is the pick tip landing, and it is the difference between a
     crew working the face and a crew standing near it. Nothing covers the rock
     itself, and the layer is pointer-events: none regardless, so nothing here
     can eat the tap that breaks it. The floating numbers live at z-index 4,
     above the crew, and spawn over the rock's centre, which the crew vacates.

   Placement uses the `translate` LONGHAND, never `transform`. transform belongs
   to the swing keyframes in animations.css; the two compose instead of the
   animation wiping the position out (same rule as the pooled effects, CONTRACT
   section 8). `scale: -1 1` mirrors the right-hand stations — the goblin sprite
   faces right with its tool in the right six columns (ARTSTYLE section 3), so
   an unmirrored goblin on the right of the rock would be swinging at the wall.
   Because `scale` applies before `transform`, the mirrored half also swings
   toward the rock without a second keyframe. */

.gm-goblins {
  /* --gm-gr is the horizontal reach of the inner ring: the rock's radius, minus
     a hair so the pick tip overlaps the boulder's edge, plus half a goblin
     because the offsets below position centres. */
  /* Two radii, one per rank. --gm-gr puts a goblin's centre where its tool tip
     lands just inside the boulder's outline; --gm-gr2 is a goblin's width and a
     bit further out, so the second rank stands directly behind the first and
     they overlap by a few pixels rather than floating apart. */
  --gm-gr: calc(var(--gm-rock-size) / 2 - 4px + var(--gm-goblin-size) / 2);
  /* v1.14: 0.84 -> 1.06 of a goblin width. The owner called the crew *"superpuestos"*
     and they were: at 0.84 the second rank sat inside the first by a sixth of a
     sprite, so at eight or more goblins the ring read as one crowded smear rather
     than as two ranks. A full width plus a hair clears the sprite box at every angle
     and leaves the ranks legible as ranks. */
  --gm-gr2: calc(var(--gm-gr) + var(--gm-goblin-size) * 1.06);

  /* The grid area is centred on the ROCK BUTTON, and the button is taller than
     the boulder because the HP bar and its label hang underneath. So:

       button height       = 8 + H + 4 + 10 + 4 + 16 + 8  = H + 50
       button centre       = (H + 50) / 2
       sprite centre       = 8 + H / 2
       sprite - button     = -17px,  for every H

     --gm-rock-readout is the 34px under the sprite: two 4px grid gaps, the 8px
     HP bar plus its 1px borders, and the 11px label at the inherited 1.45 line
     height. Change any of those three and change this. The offset is constant
     in H, which is why one number covers all four rock sizes.

     This is a ROW-axis relationship: it depends only on .gm-rock being
     align-self: center and .gm-goblins being align-self: stretch in the same
     row. Both were already true while the two were in different columns, so the
     grid-column fix did not move it — the bug was purely horizontal. */
  --gm-rock-readout: 34px;
  --gm-crew-lift: calc(var(--gm-rock-readout) / -2);
}

.gm-goblin {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--gm-goblin-size);
  height: var(--gm-goblin-size);
  /* -50% -50% centres the box on the station; --gm-gx/--gm-gy then displace it. */
  translate:
    calc(-50% + var(--gm-gx, 0px))
    calc(-50% + var(--gm-gy, 0px) + var(--gm-crew-lift));
}

.gm-goblin svg { width: 100%; height: 100%; }

/* THE STATIONS ARE POLAR, AND THEY HAVE TO BE.

   The first version varied x by a tenth of a goblin width and y by two and a
   half goblin heights, which is not a ring — it is two vertical files, and it
   would have read as two files even in the correct box. A ring means the
   horizontal distance falls as the station climbs or drops. So each station is
   now (cos, sin) of a chosen angle off the horizontal, times its rank's radius.

     rank 1 (--gm-gr)    +8deg   +30deg   -40deg     magnitude .98 / .93 / .91
     rank 2 (--gm-gr2)   -2deg   +30deg   -22deg     magnitude 1.0 / .95 / .95

   The magnitudes are under 1 on purpose. A goblin is an axis-aligned box, so at
   a diagonal station its nearest corner is closer to the centre than its radius
   suggests; pulling the radius in by roughly a third of a goblin width times
   |sin| puts the tool tip back on the outline at every angle. That correction is
   baked into the numbers below rather than computed, because it is a constant of
   the artwork and not of the layout.

   Angles are deliberately asymmetric (+8/+30/-40, not -30/0/+30). Mirror-image
   angles give mirror-image cosines, and two stations at the same |x| are a file
   again. As drawn, the six per side land on six distinct distances spanning
   about one and a half goblin widths.

   Order matters: the pool fills 1..12, so rank 1 takes the face first — a pair
   at the boulder's waist, then low, then high — and rank 2 only appears once
   there are more than six goblins. */

/* Rank 1 — on the face. The tool tip crosses the outline by 6 to 14px. */
.gm-goblin:nth-child(1) { --gm-gx: calc(0px - var(--gm-gr) * 0.971); --gm-gy: calc(var(--gm-gr) * 0.136); }
.gm-goblin:nth-child(2) { --gm-gx: calc(var(--gm-gr) * 0.971);       --gm-gy: calc(var(--gm-gr) * 0.136); }
.gm-goblin:nth-child(3) { --gm-gx: calc(0px - var(--gm-gr) * 0.803); --gm-gy: calc(var(--gm-gr) * 0.464); }
.gm-goblin:nth-child(4) { --gm-gx: calc(var(--gm-gr) * 0.803);       --gm-gy: calc(var(--gm-gr) * 0.464); }
.gm-goblin:nth-child(5) { --gm-gx: calc(0px - var(--gm-gr) * 0.695); --gm-gy: calc(0px - var(--gm-gr) * 0.583); }
.gm-goblin:nth-child(6) { --gm-gx: calc(var(--gm-gr) * 0.695);       --gm-gy: calc(0px - var(--gm-gr) * 0.583); }

/* Rank 2 — standing behind rank 1, not reaching the rock. */
.gm-goblin:nth-child(7)  { --gm-gx: calc(0px - var(--gm-gr2) * 0.996); --gm-gy: calc(0px - var(--gm-gr2) * 0.035); }
.gm-goblin:nth-child(8)  { --gm-gx: calc(var(--gm-gr2) * 0.996);       --gm-gy: calc(0px - var(--gm-gr2) * 0.035); }
.gm-goblin:nth-child(9)  { --gm-gx: calc(0px - var(--gm-gr2) * 0.820); --gm-gy: calc(var(--gm-gr2) * 0.473); }
.gm-goblin:nth-child(10) { --gm-gx: calc(var(--gm-gr2) * 0.820);       --gm-gy: calc(var(--gm-gr2) * 0.473); }
.gm-goblin:nth-child(11) { --gm-gx: calc(0px - var(--gm-gr2) * 0.884); --gm-gy: calc(0px - var(--gm-gr2) * 0.357); }
.gm-goblin:nth-child(12) { --gm-gx: calc(var(--gm-gr2) * 0.884);       --gm-gy: calc(0px - var(--gm-gr2) * 0.357); }

/* ---- THE CENTRE STATIONS, and why exactly two of them exist (v1.15) ----

   Every station above is one half of a mirrored pair, which is what makes the ring
   read as a crew rather than as sprites dropped at random - the v1.14 item. The
   consequence was that an odd crowd was lopsided, so util.crowdSize rounded the count
   down to an even number, and the owner then found what THAT costs: *"el tercer goblin
   no ha aparecido alrededor de la roca hasta que no he comprado el cuarto"*.

   Both complaints are right and they only conflict while every station is half of a
   pair. So a crowd of three or of five moves its LAST goblin onto the vertical axis -
   dead centre, below the boulder at three and above it at five - and the ring is
   balanced with an odd number of heads in it. crowdSize can then follow the Diggers
   one for one up to six, which is the whole of the fix.

   Only 3 and 5, and that is not laziness. Above six the count stops tracking purchases
   (it goes logarithmic, so nobody is counting heads against Diggers any more) and the
   even rounding takes over again, so no other odd crowd is ever drawn. 1 needs nothing:
   a single goblin is not lopsided, it is a goblin.

   `data-crowd` is written by ui.render() from the same crowdSize() call that decides
   how many are visible, so the count the layout keys off and the count on screen are
   the same number by construction. */
.gm-goblins[data-crowd="3"] .gm-goblin:nth-child(3) {
  --gm-gx: 0px;
  --gm-gy: calc(var(--gm-gr) * 0.72);
}
.gm-goblins[data-crowd="5"] .gm-goblin:nth-child(5) {
  --gm-gx: 0px;
  --gm-gy: calc(0px - var(--gm-gr) * 0.76);
}

/* The two ranks overlap by a few pixels where they meet, and the one at the
   face has to be in front. Without this, rank 2 wins on DOM order and a goblin
   standing further from the rock paints over the goblin working it. Both values
   are scoped to .gm-goblins' own stacking context, so neither can escape past
   the rock or the effects layer. */
.gm-goblin { z-index: 1; }
.gm-goblin:nth-child(n + 7) { z-index: 0; }

/* Right-hand stations face the rock. Mirroring is only about which way a goblin
   points — it neither makes nor breaks the ring, which is entirely the work of
   the offsets above. */
.gm-goblin:nth-child(2n) { scale: -1 1; }

/* ---- floating numbers and ore bits ---- */

/* Anchoring lives here, motion lives in animations.css. The `translate`
   longhand centres the element on whatever point the game puts it at with
   left/top, and composes with — rather than competes with — the `transform`
   the keyframes animate. That separation is what keeps every keyframe free of
   any assumption about where the effect started. left/top below are only a
   fallback for an effect spawned without a position. */
.gm-fx__num {
  position: absolute;
  left: 50%;
  top: 55%;
  translate: -50% -50%;
  font-family: var(--gm-font-num);
  font-weight: 700;
  font-size: 14px;
  white-space: nowrap;
  color: var(--gm-ink);
  text-shadow: 0 1px 0 var(--gm-bg), 0 -1px 0 var(--gm-bg),
               1px 0 0 var(--gm-bg), -1px 0 0 var(--gm-bg);
  will-change: transform, opacity;
}

.gm-fx__num[data-kind="hit"]  { color: var(--gm-ink); }
.gm-fx__num[data-kind="crit"] { color: var(--gm-amber); font-size: 20px; }
.gm-fx__num[data-kind="gold"] { color: var(--gm-gold); }
/* A tap that landed nothing (v1.18). Grey and small, because it is the absence of a
   number rather than a number - it says the pick bounced and gets out of the way. */
.gm-fx__num[data-kind="miss"] { color: var(--gm-ink-mute); font-size: 11px; }

.gm-fx__bit {
  position: absolute;
  left: 50%;
  top: 55%;
  translate: -50% -50%;
  width: var(--gm-px);
  height: var(--gm-px);
  background: var(--gm-ore-coal);
  will-change: transform, opacity;
}

.gm-fx__bit[data-ore="coal"]       { background: var(--gm-ore-coal); }
.gm-fx__bit[data-ore="copper"]     { background: var(--gm-ore-copper); }
.gm-fx__bit[data-ore="iron"]       { background: var(--gm-ore-iron); }
.gm-fx__bit[data-ore="silver"]     { background: var(--gm-ore-silver); }
.gm-fx__bit[data-ore="gold"]       { background: var(--gm-ore-gold); }
.gm-fx__bit[data-ore="mithril"]    { background: var(--gm-ore-mithril); }
.gm-fx__bit[data-ore="adamantite"] { background: var(--gm-ore-adamantite); }
.gm-fx__bit[data-ore="voidstone"]  { background: var(--gm-ore-voidstone); }
.gm-fx__bit[data-ore="starmetal"]  { background: var(--gm-ore-starmetal); }

/* Particle density. "off" must cost nothing: the bits leave the render tree
   entirely rather than being painted transparent. */
.gm-app[data-particles="low"] .gm-fx__bit:nth-child(2n) { display: none; }
.gm-app[data-particles="off"] .gm-fx__bit { display: none; }

/* ---- golden goblin ---- */

/* v1.12: it FLIES now, so the element is the size of the goblin rather than a
   full-width track (the old shape existed because a percentage translateX
   resolves against the element's own box, so a 32px button could only travel
   32px — engine.tickGolden() hands over pixels, which has no such problem).

   The whole button is tappable now and that is a deliberate reversal: the old
   strip was inert except for the sprite, because the strip was the width of the
   cave and tapping empty rock would have claimed the goblin. An element the size
   of the goblin has no empty rock in it. A thing moving erratically at half a
   screen per second is hard enough to hit without shrinking the target to the
   pixels that happen to be opaque.

   MOTION IS SPLIT THE WAY THE CONTRACT SPLITS IT. JS owns where it is — one
   transform per frame, written from normalised coordinates. CSS owns what it
   looks like while it is there: the wing beat and the shimmer, neither of which
   the engine has any business knowing about. */
.gm-golden {
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  padding: 0;
  background: none;
  left: 0;
  top: 0;
  width: calc(var(--gm-px) * 9);
  height: calc(var(--gm-px) * 9);
  pointer-events: auto;
  cursor: pointer;
  touch-action: manipulation;
  z-index: 5;
  --gm-gx: 0px;
  --gm-gy: 0px;
  /* Stated on the base rule as well as in the keyframes, so the goblin is in the
     right place whether or not the animation is running. Reduced-motion and
     animation-off both strip the keyframes, and a fly parked in the top-left
     corner of the cave would be the result of leaving position to them. */
  transform: translate(var(--gm-gx), var(--gm-gy));
  filter: drop-shadow(0 0 calc(var(--gm-px) * 2) rgb(255 200 90 / 55%));
  animation: gm-golden-flap 0.16s steps(2, end) infinite;
}

.gm-golden svg {
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* On its way out it dims, so the last second and a half reads as a deadline
   rather than as the goblin simply happening to be near an edge. */
.gm-golden[data-leaving="true"] { opacity: 0.55; }

@keyframes gm-golden-flap {
  from { transform: translate(var(--gm-gx), var(--gm-gy)) scaleY(1); }
  to   { transform: translate(var(--gm-gx), var(--gm-gy)) scaleY(0.82); }
}

/* ========================================================================== */
/* CORE COLUMN                                                                */
/* ========================================================================== */

/* ==========================================================================
   THE CHEST

   Ten gem cells above the gold. There is no markup for the chest itself — the
   box is drawn here, around the container, so this must read as a small banded
   strongbox standing open and not as another resource strip.

   Four things build it, all on one element and its two pseudo-elements:

   1. The body is boards, not a panel: hard-stopped vertical planks in the wood
      pair, with the seams a shade darker.
   2. `::before` is the open lid seen edge-on — an iron band across the top with
      a gold hasp notched into the middle of it.
   3. `::after` is the shadow the lid throws into the box, so the cells sit
      *inside* something rather than on top of it.
   4. Iron corner straps come from the border: heavy on the left and right,
      light top and bottom.

   A found gem is a lit stone on a dark cell. An unfound one is a black
   silhouette on a lighter cell with `???` beneath it (the game writes the
   text). That is three channels — value, shape and words — because lock state
   is never colour alone (ARTSTYLE §2). The silhouette is the whole reason the
   ten shapes were measured against each other before any of them was shaded.
   ========================================================================== */

.gm-chest {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(58px, 1fr));
  gap: 3px;
  padding: 18px 6px 8px;
  position: relative;
  isolation: isolate;
  border: 1px solid var(--gm-line);
  border-left-width: 4px;
  border-right-width: 4px;
  border-radius: var(--gm-radius);
  background-color: var(--gm-wood-dk);
  background-image:
    /* plank seams — hard stops only, this is pixel art */
    repeating-linear-gradient(90deg,
      transparent 0 15px,
      rgb(0 0 0 / 42%) 15px 17px),
    linear-gradient(180deg,
      var(--gm-wood) 0 60%,
      var(--gm-wood-dk) 60% 100%);
}

/* The lid, standing open: an iron band with a gold hasp let into it. */
.gm-chest::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 13px;
  z-index: 1;
  pointer-events: none;
  background-image:
    /* the hasp */
    linear-gradient(90deg,
      transparent 0 calc(50% - 7px),
      var(--gm-gold-dk) calc(50% - 7px) calc(50% - 6px),
      var(--gm-gold)    calc(50% - 6px) calc(50% + 6px),
      var(--gm-gold-dk) calc(50% + 6px) calc(50% + 7px),
      transparent calc(50% + 7px) 100%),
    /* the band itself */
    linear-gradient(180deg,
      var(--gm-ink-dim)  0    3px,
      var(--gm-ink-mute) 3px  10px,
      var(--gm-bg)       10px 13px);
}

/* The dark the open lid throws down into the box. */
.gm-chest::after {
  content: "";
  position: absolute;
  inset: 13px 0 auto;
  height: 10px;
  z-index: 1;
  pointer-events: none;
  background-image: linear-gradient(180deg,
    rgb(0 0 0 / 55%) 0 4px,
    rgb(0 0 0 / 28%) 4px 8px,
    transparent 8px 100%);
}

.gm-chest__item {
  position: relative;
  z-index: 2;
  display: grid;
  justify-items: center;
  align-content: start;
  gap: 2px;
  padding: 4px 2px 3px;
  min-width: 0;
  background: var(--gm-bg);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
}

/* 2x the 16px source grid. */
/* The chest is the one symbol drawn on 24x24 among the 16x16 gems, so x1 is 24 and
   32 was x1.33. */
.gm-chest__icon { width: 24px; height: 24px; }

.gm-chest__name {
  font-size: 9px;
  line-height: 1.15;
  text-align: center;
  color: var(--gm-ink-dim);
  overflow-wrap: anywhere;
}

.gm-chest__item[data-found="true"] { border-color: var(--gm-ink-mute); }
.gm-chest__item[data-found="true"] .gm-chest__name { color: var(--gm-ink); }

/* Not yet found: the stone goes flat black and the cell goes pale behind it, so
   the shape is legible and the colour is not. `brightness(0)` collapses every
   fill in the sprite — including its 1px --gm-bg outline — to a single black
   silhouette, which is exactly the "see that there is something, do not see
   what it is" state a locked building uses. */
.gm-chest__item[data-found="false"] {
  background: var(--gm-bg-3);
  border-style: dashed;
  border-color: var(--gm-line);
}

.gm-chest__item[data-found="false"] .gm-chest__icon { filter: brightness(0); }

.gm-chest__item[data-found="false"] .gm-chest__name {
  font-family: var(--gm-font-num);
  letter-spacing: .1em;
  color: var(--gm-ink-mute);
}

/* Resource tiles, not a cramped strip. Gold is the currency every decision is
   measured in, so it takes a full-width tile and the largest number on screen;
   bars and runestones pair up beneath it. This is also the top third of the
   centre column doing real work instead of being 56px of header. */
.gm-res {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--gm-gap-sm);
  padding: var(--gm-gap-sm);
}

.gm-res__item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto auto;
  column-gap: var(--gm-gap);
  align-items: center;
  align-content: center;
  min-width: 0;
  padding: var(--gm-gap) var(--gm-gap);
  background: var(--gm-bg-3);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
}

.gm-res__item[data-res="gold"] { grid-column: 1 / -1; }

/* 16px = x1 on a 16x16 canvas (v1.16). It was 24, an x1.5 that crispEdges renders with
   uneven pixel widths. Gold overrides to 32 (x2) just below, which is the step that keeps
   the primary resource the biggest thing in the bar. */
.gm-res__icon {
  width: 16px;
  height: 16px;
  grid-row: 1 / 3;
}

.gm-res__value {
  font-family: var(--gm-font-num);
  font-size: 20px;
  font-weight: 700;
  line-height: 1.1;
  color: var(--gm-ink);
  overflow-wrap: anywhere;
}

.gm-res__item[data-res="gold"] .gm-res__icon { width: 32px; height: 32px; }
.gm-res__item[data-res="gold"] .gm-res__value { font-size: 28px; }

.gm-res__rate {
  font-family: var(--gm-font-num);
  font-size: 12px;
  color: var(--gm-ink-dim);
}

.gm-res__item[data-res="gold"]       .gm-res__value { color: var(--gm-gold); }
.gm-res__item[data-res="bars"]       .gm-res__value { color: var(--gm-blue); }
.gm-res__item[data-res="runestones"] .gm-res__value { color: var(--gm-violet); }

/* ---- morale (v1.17) ----
   A label, its multiplier, the bar, and the cap it is drifting toward. Two rows so
   the bar spans the full width: a 3px bar squeezed into a column beside text reads as
   a divider rather than a meter.

   It is the same --gm-fill / scaleX mechanism as the rock, the event timer and the
   stall price (CONTRACT section 8): the stylesheet owns the motion, the game owns the
   value, and never both. The transition is slower than the stall's because morale
   drifts at 0.01 a second - a fast transition on a slow number just looks like jitter. */
/* ---------------------------------------------------------------- the rail
   v1.18. One icon button per window in the core column, at the far left of the app.
   Square, so the column can be narrow; the icon carries the meaning and the accessible
   name carries the word. `aria-pressed` is the open state and the stylesheet reads it,
   which means the control is honest to a screen reader for free. */
.gm-rail__btn {
  appearance: none;
  -webkit-appearance: none;
  display: grid;
  place-items: center;
  aspect-ratio: 1;
  width: 100%;
  padding: 0;
  font: inherit;
  color: var(--gm-ink-dim);
  background: var(--gm-bg-2);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
  cursor: pointer;
}

.gm-rail__btn svg { width: 16px; height: 16px; }

.gm-rail__btn[aria-pressed="true"] {
  color: var(--gm-gold);
  border-color: var(--gm-gold);
  background: var(--gm-bg-3, var(--gm-bg-2));
}

.gm-rail__btn:hover { color: var(--gm-ink); }

/* A window the rail has folded away. `display: none` rather than visibility, so it
   costs no layout in the column - which is the whole point of the control. */
.gm-core > [data-open="false"] { display: none; }

/* MORALE LIVES WITH THE ROCK SINCE v1.18 (the owner: *"yo pondria la moral en la misma
   ventana en la que esta la roca"*), so it is placed against .gm-mine's grid rather than
   stacked in the core column. Row 1 is the depth readout's row and it sits beside it.

   `grid-column: 1` is not optional - see .gm-rock in layout.css. And it is translucent
   over the strata rather than a solid panel, because it is now floating over the cave
   art instead of sitting in a column of cards. */
.gm-morale {
  grid-row: 1;
  grid-column: 1;
  position: relative;
  z-index: 3;
  justify-self: end;
  align-self: start;
  margin: var(--gm-gap-sm) var(--gm-gap) 0 0;
  /* Same figure .gm-mine__depth reserves against (layout.css, .gm-mine) -
     one writer, so the two can never drift apart and overlap again. */
  max-width: var(--gm-morale-w);
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 2px var(--gm-gap-sm);
  align-items: center;
  padding: var(--gm-gap-sm) var(--gm-gap);
  background: color-mix(in srgb, var(--gm-bg-2) 82%, transparent);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
}

.gm-morale__label {
  font-size: 12px;
  color: var(--gm-ink-dim);
  text-transform: uppercase;
  letter-spacing: .06em;
}

.gm-morale__mult {
  font-family: var(--gm-font-num);
  font-size: 14px;
  font-weight: 700;
  color: var(--gm-green);
  justify-self: end;
}

.gm-morale__bar {
  grid-column: 1 / -1;
  display: block;
  height: 4px;
  background: var(--gm-bg);
  border: 1px solid var(--gm-line);
  overflow: hidden;
}

.gm-morale__bar > i {
  display: block;
  height: 100%;
  background: var(--gm-green);
  transform: scaleX(var(--gm-fill, 1));
  transform-origin: left center;
  transition: transform .4s linear;
}

.gm-morale__cap {
  grid-column: 1 / -1;
  font-family: var(--gm-font-num);
  font-size: 11px;
  color: var(--gm-ink-dim);
}

/* ---- event banner ---- */

/* GDD v1.5: the event floats at the top centre of the cave, over the rock face
   rather than above the ore strip. It is the only moment-to-moment decision the
   game offers and it was living below the fold.

   `left/right` keep it centred with a margin at any width; `z-index` puts it over
   the strata but under nothing that matters. The card itself takes pointer events
   so its two buttons work, but it stops short of the rock: the whole point is that
   answering an event must never mean losing taps. */
.gm-event {
  position: absolute;
  top: var(--gm-gap);
  left: var(--gm-gap);
  right: var(--gm-gap);
  max-width: 420px;
  margin: 0 auto;
  z-index: 4;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--gm-gap);
  padding: var(--gm-gap);
  background: var(--gm-bg-2);
  border: 1px solid var(--gm-line);
  border-left: 3px solid var(--gm-ink-mute);
  border-radius: var(--gm-radius);
  box-shadow: 0 6px 18px rgb(0 0 0 / 45%);
}

/* Two full-width buttons, because each one now carries a sentence. */
.gm-event__actions .gm-btn { width: 100%; text-align: left; }

.gm-event__icon { width: 16px; height: 16px; grid-row: 1 / 3; align-self: start; }  /* x1 on 16 */
.gm-event__body { min-width: 0; }

.gm-event__title {
  margin: 0 0 2px;
  font-size: 14px;
  font-weight: 700;
  color: var(--gm-ink);
}

.gm-event__text {
  margin: 0;
  font-size: 12px;
  color: var(--gm-ink-dim);
}

/* Stacked, never side by side, and since v1.8 that is load-bearing rather than a
   preference: the second choice is the one a timeout takes and it must be the one
   BELOW ("la peor siempre va abajo"). `flex-direction` used to be declared in a
   separate one-line rule further up, which is one careless deletion away from
   putting the worst option on the right. */
.gm-event__actions {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  gap: var(--gm-gap-sm);
  margin-top: var(--gm-gap-sm);
}

/* The bottom choice is what a timeout takes, and since GDD v1.8 the table
   guarantees it is also the worse one. Saying so on the button is the difference
   between a rule the player can use and one they meet by losing a cart: the
   timer bar underneath already says "something happens when this runs out", and
   this says which thing. Generated text rather than a decoration, so it is read
   aloud with the label it belongs to. */
.gm-event__actions .gm-btn[data-default="true"]::after {
  content: "  \2014  if you do nothing";
  font-family: var(--gm-font-num);
  font-size: 10px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--gm-ink-mute);
  white-space: nowrap;
}

/* A choice you cannot pay for (v1.9). Dimmed and struck through the affordance,
   not hidden: seeing the option you cannot take is what tells you the fine exists
   and that you are about to eat the other side of the card. */
.gm-event__actions .gm-btn[data-afford="false"] {
  opacity: .45;
  cursor: not-allowed;
  border-style: dashed;
}

/* The seconds left, in words, bottom-right of the card and clear of both buttons.
   It reads the same field the bar below it does (v1.18). */
.gm-event__clock {
  position: absolute;
  right: var(--gm-gap);
  bottom: 6px;
  font-size: 10px;
  line-height: 1;
  color: var(--gm-ink-mute);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

.gm-event__timer {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 3px;
  background: var(--gm-bg);
  overflow: hidden;
}

.gm-event__timer > i {
  display: block;
  width: 100%;
  height: 100%;
  transform-origin: left center;
  transform: scaleX(var(--gm-fill, 1));
  background: var(--gm-ink-mute);
}

/* Tone is carried by the rail, the title colour AND an icon tint, so it never
   depends on hue alone. */
.gm-event[data-tone="good"]    { border-left-color: var(--gm-green); }
.gm-event[data-tone="bad"]     { border-left-color: var(--gm-red); }
.gm-event[data-tone="neutral"] { border-left-color: var(--gm-blue); }

.gm-event[data-tone="good"]    .gm-event__icon { color: var(--gm-green); }
.gm-event[data-tone="bad"]     .gm-event__icon { color: var(--gm-red); }
.gm-event[data-tone="neutral"] .gm-event__icon { color: var(--gm-blue); }

.gm-event[data-tone="good"]    .gm-event__timer > i { background: var(--gm-green); }
.gm-event[data-tone="bad"]     .gm-event__timer > i { background: var(--gm-red); }
.gm-event[data-tone="neutral"] .gm-event__timer > i { background: var(--gm-blue); }

/* ---- the dig button ---- */

/* The word DIG is gone. The owner called it redundant next to a pickaxe the
   size of a fist, and they are right: nothing else in the mine is a button with
   a pick on it. What is left is a pick and one line of type, so the pick has to
   carry the whole control — it is bigger than it was, and the gap under it is
   tighter, so the two read as one object rather than as an icon with a caption
   floating beneath it.

   The DIG label's rules were deleted along with the span. If that label ever
   comes back it needs a new rule written for it; it will otherwise render at
   body size. (The class name is deliberately not spelled here as a selector —
   a dead name in a comment reads as dead CSS to the contract checker.) */
.gm-dig {
  appearance: none;
  -webkit-appearance: none;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: var(--gm-gap-sm);
  padding: var(--gm-gap) var(--gm-gap-lg) var(--gm-gap-lg);
  cursor: pointer;
  color: var(--gm-ink);
  font: inherit;
  background: var(--gm-bg-3);
  border: 1px solid var(--gm-green-dk);
  border-bottom-width: 4px;
  border-radius: var(--gm-radius);
  touch-action: manipulation;
}

.gm-dig:hover { background: var(--gm-line); }

.gm-dig__icon {
  width: calc(var(--gm-px) * 28);
  height: calc(var(--gm-px) * 28);
  color: var(--gm-amber);
}

/* Not a caption. This line is the only number on the control and it also
   carries the buff countdown and the current hint, so it gets room for two
   lines and a colour it can be read at from across the room. */
.gm-dig__sub {
  max-width: 26ch;
  font-family: var(--gm-font-num);
  font-size: 15px;
  font-weight: 700;
  line-height: 1.25;
  text-align: center;
  text-wrap: balance;
  color: var(--gm-ink);
}

/* ============================================================================
   THE ORE CART  (v1.8 item 5)
   The owner: *"El cart deberia tener un dibujo de un carro minero como el cofre
   de las gemas"*. Same construction as .gm-chest, one object along: no sprite,
   no image, four layers of hard-stop gradient on one element and its two
   pseudo-elements, because a wagon that is 220px wide in the core column and
   600px wide on a desktop cannot be a bitmap.

     1. The body is planked wood, seams every 15px, dark at the bottom where the
        ore sits — the same two --gm-wood tokens the chest is built from, so the
        two boxes read as one workshop.
     2. `::before` is the iron rim, an edge-on band across the top with the
        shadow it throws into the box hanging under it.
     3. `::after` is the undercarriage: two iron wheels with a pale hub each, an
        axle bar between them, and a length of rail under the lot.
     4. The heavy left and right borders are the corner straps, exactly as on
        the chest.

   The wheels sit BELOW the box (`bottom: -9px`) and are 9px tall, which is why
   .gm-cart carries that much bottom margin in layout.css. Nothing inside the
   cart knows any of this: .gm-ores is untouched.
   ============================================================================ */

.gm-cart {
  position: relative;
  isolation: isolate;
  padding: 15px var(--gm-gap-sm) var(--gm-gap-sm);
  border: 1px solid var(--gm-line);
  border-left-width: 4px;
  border-right-width: 4px;
  border-radius: var(--gm-radius);
  background-color: var(--gm-wood-dk);
  background-image:
    /* plank seams — hard stops only, this is pixel art */
    repeating-linear-gradient(90deg,
      transparent 0 15px,
      rgb(0 0 0 / 42%) 15px 17px),
    linear-gradient(180deg,
      var(--gm-wood) 0 55%,
      var(--gm-wood-dk) 55% 100%);
}

/* The iron rim, and the dark it throws down into the box. */
.gm-cart::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 15px;
  z-index: 1;
  pointer-events: none;
  background-image: linear-gradient(180deg,
    var(--gm-ink-mute) 0   3px,
    var(--gm-ink-dim)  3px 7px,
    rgb(0 0 0 / 55%)   7px 11px,
    rgb(0 0 0 / 25%)   11px 15px);
}

/* Wheels, axle and rail. Two radial hard stops for the tyres, one linear bar for
   the axle, one for the rail they run on. */
.gm-cart::after {
  content: "";
  position: absolute;
  inset: auto 0 -9px;
  height: 11px;
  z-index: 0;
  pointer-events: none;
  background-image:
    /* left wheel */
    radial-gradient(circle 5px at 22px 5px,
      var(--gm-ink-mute) 0 2px,
      var(--gm-ink-dim)  2px 5px,
      transparent 5px 100%),
    /* right wheel */
    radial-gradient(circle 5px at calc(100% - 22px) 5px,
      var(--gm-ink-mute) 0 2px,
      var(--gm-ink-dim)  2px 5px,
      transparent 5px 100%),
    /* the axle between them */
    linear-gradient(180deg,
      transparent 0 4px,
      var(--gm-ink-dim) 4px 6px,
      transparent 6px 100%),
    /* the rail they run on */
    linear-gradient(180deg,
      transparent 0 9px,
      var(--gm-line) 9px 11px,
      transparent 11px 100%);
  background-repeat: no-repeat;
}

/* ---- ore inventory strip ---- */

/* What the cart is carrying. `auto-fill` at a small minimum keeps at least two
   columns even in the 220px core column, so the grid never becomes one chip
   adrift in a wide empty box — which is what a 124px minimum did at 1900px, and
   what the player photographed. Row stretching is disabled in layout.css for the
   same reason. */
/* ---- v1.12: a display case, not a stock list ----

   The owner retired the quantity — *"sin poner cantidades, solo el ore, centrado en
   la casilla, mismo tamaño que las gemas"* — so the cell changed shape with it.
   It was a wide row with a sprite on the left and a number on the right, sized to
   fit the number; it is now a square the size of a chest cell with the sprite in
   the middle of it, and the eight of them read as one case rather than as a list.

   `auto-fill` with a 44px track rather than a fixed count, for the same reason the
   chest uses one: the column is narrow on a phone and wide on a desktop, and a
   fixed eight-across is unreadable on the first and lost on the second. */
/* A FIXED 3x3 (v1.14, owner: *"deben ocupar una cuadricula de 3x3"*), not the
   auto-fit the chest uses. There are nine ores and the case is a display of nine
   collectibles, so the grid is the shape of the collection rather than the shape of
   whatever width the column happens to have: at auto-fit it reflowed to 4+4+1 on a
   desktop and 2+2+2+2+1 on a phone, and a case with an orphan row does not read as a
   set. The chest keeps auto-fit because twelve gems have no square to be.

   tools/simulate.js fails if D.ORES stops being nine, because a tenth ore would make
   this three-column grid an orphan row again and the fix is a layout decision rather
   than a number. */
.gm-ores {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--gm-gap-sm);
  align-content: start;
}

.gm-ores__item {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1;
  padding: 3px;
  background: var(--gm-bg-2);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
  min-width: 0;
}

/* 48px = 3x on a 16x16 canvas (ARTSTYLE, canvas sizes table), and BOTH halves of that
   matter (v1.15).

   The owner: *"el sprite del ore en el cart queda demasiado pequeño, necesita ser más
   grande para que se vea bien dentro del cuadro"*. The case is a 3-column grid of
   square cells, so in the core column a cell is around 85px and a 24px sprite sat in
   the middle of it looking like a mistake.

   And 24 on a 16px canvas is x1.5 - HALF A PIXEL. With `image-rendering: pixelated`
   that does not blur, it does something worse: it renders some source pixels one
   screen pixel wide and some two, so a sprite drawn on an even grid comes out with
   uneven strokes. ARTSTYLE is explicit that pixel art only survives integer scaling,
   and the gem chest next to this already runs at x2. The cell here is the bigger of
   the two, so it takes x3. */
.gm-ores__icon { width: 48px; height: 48px; flex: none; image-rendering: pixelated; }

/* NO DIMMING FOR "FOUND BUT HOLDING NONE" (v1.14). The owner: *"no deben tener
   transparencias, se supone que funcionan como las gemas, son coleccionables"*.

   There used to be a rule here putting `opacity: .38; filter: grayscale(1)` on
   `[data-owned="false"][data-found="true"]`, and it was answering the wrong question.
   The case stopped being a warehouse readout in v1.12 - what you HOLD is the market's
   business, one row per ore beside the price it fetches - and a collectible that fades
   when the cart empties is a stock level wearing a collection's clothes. A gem you
   have found does not go grey when you spend the gold it paid for.

   So there are two states here and only two, exactly as in the chest: never dug (the
   locked silhouette below), and found (drawn, full strength, forever). `data-owned` is
   no longer emitted at all - see the note in ui.js. */

/* Ore you have never dug: the chest's locked state, applied to the cart (v1.9).
   Flat black silhouette on a pale cell with `???` beside it — value, shape and
   words, because lock state is never colour alone (ARTSTYLE section 2). The
   border colour below is suppressed too, or the outline would name the ore the
   silhouette exists to hide. */
.gm-ores__item[data-found="false"] {
  background: var(--gm-bg-3);
  border-style: dashed;
  border-color: var(--gm-line) !important;
}

.gm-ores__item[data-found="false"] .gm-ores__icon { filter: brightness(0); }

.gm-ores__item[data-ore="coal"]       { border-color: var(--gm-ore-coal); }
.gm-ores__item[data-ore="copper"]     { border-color: var(--gm-ore-copper); }
.gm-ores__item[data-ore="iron"]       { border-color: var(--gm-ore-iron); }
.gm-ores__item[data-ore="silver"]     { border-color: var(--gm-ore-silver); }
.gm-ores__item[data-ore="gold"]       { border-color: var(--gm-ore-gold); }
.gm-ores__item[data-ore="mithril"]    { border-color: var(--gm-ore-mithril); }
.gm-ores__item[data-ore="adamantite"] { border-color: var(--gm-ore-adamantite); }
.gm-ores__item[data-ore="voidstone"]  { border-color: var(--gm-ore-voidstone); }
.gm-ores__item[data-ore="starmetal"]  { border-color: var(--gm-ore-starmetal); }

/* ---- the figures band (v1.12 item 13) ---- */

/* This is where the Sell ore button used to be, and the swap is the point: that
   button was the least useful thing on the screen once every ore had its own
   market, and the space at the foot of the column was the only space left. The
   owner: *"El boton de abajo antiguo para vender el ore ya no se necesita, en su
   lugar podrias cambiarlo por dinero/segundo... al menos se usa esa zona"*.

   Six totals on an auto-fitting grid, each one a label over a number. Numbers in
   the numeric face and one size larger than their labels, so the eye runs down
   the values and not the words — the same relationship .gm-res uses, at a smaller
   scale, because this is reference and that is the headline. */
.gm-figures {
  display: flex;
  flex-direction: column;
  gap: var(--gm-gap-sm);
  padding: var(--gm-gap);
  background: var(--gm-bg-2);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
}

/* 84 px was a floor chosen for a 300 px column. The strip is the full width of the
   mine since v1.18 - which is the whole reason it moved - so the cells get room for
   the numbers the owner could not read, and auto-fit spreads them across it. */
.gm-figures__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  gap: var(--gm-gap-sm) var(--gm-gap);
}

.gm-figures__cell {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.gm-figures__label {
  font-size: 10px;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--gm-ink-mute);
}

.gm-figures__value {
  font-family: var(--gm-font-num);
  font-size: 13px;
  font-weight: 700;
  color: var(--gm-ink);
  overflow-wrap: anywhere;
}

/* The automation switches. Hidden entirely until a perk grants one, so the strip
   does not advertise four things the player cannot have — the same rule locked
   buildings follow. Off is stated by the pill going flat and muted, never by
   colour alone. */
.gm-figures__autos {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gm-gap-sm);
  padding-top: var(--gm-gap-sm);
  border-top: 1px solid var(--gm-line);
}

.gm-figures__auto {
  appearance: none;
  -webkit-appearance: none;
  padding: 2px 7px;
  font: inherit;
  font-size: 10px;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--gm-bg);
  background: var(--gm-green);
  border: 1px solid var(--gm-green-dk);
  border-radius: 999px;
  cursor: pointer;
  touch-action: manipulation;
}

.gm-figures__auto[aria-pressed="false"] {
  color: var(--gm-ink-mute);
  background: var(--gm-bg-3);
  border-color: var(--gm-line);
  text-decoration: line-through;
}

/* ---- the stall (GDD v1.10) ---- */

/* Where SELL dumps the whole cart at whatever the town is paying, the stall is
   where you sell ONE ore in a lot you chose and watch the price move. It sits
   directly under the cart it draws from, and it is deliberately plainer than the
   cart above it: this is the working half. The awning, the counter and the dwarf
   who keeps it are in propuestas/estetica.html awaiting the owner's pick, and the
   frame here is sized so that the scenery can be dropped in around it without the
   grid inside changing at all.

   The one thing it does draw is the PRICE BAR on each slot, because the price
   walk is the whole mechanic and a number alone does not show a thing moving. */
/* The stall is now DRESSED (v1.10, approved from propuestas/estetica.html): a
   striped awning across the top, a counter with a lit lip and boards under it, and
   the dwarf who keeps it. The division is the same one the cart already uses - the
   furniture is drawn here in hard-stop gradients, the CHARACTER is a sprite - and
   the grid inside is untouched, which is what let the scenery be added without the
   working half moving a pixel. */
.gm-stall {
  position: relative;
  isolation: isolate;
  display: grid;
  gap: var(--gm-gap-sm);
  /* room at the top for the awning, and at the bottom for the counter boards */
  padding: 20px var(--gm-gap) 13px;
  background-color: var(--gm-bg-2);
  border: 1px solid var(--gm-line);
  border-left-width: 3px;
  border-right-width: 3px;
  border-left-color: var(--gm-wood-dk);
  border-right-color: var(--gm-wood-dk);
  border-radius: var(--gm-radius);
}

/* The awning: a dark rail, four rows of stripe, and a scalloped hem. Hard stops
   only - this is pixel art and a soft gradient would be the one blurred edge on
   the screen. The scallop is a repeating radial with a hard cut, which is the
   cheapest way to get a row of half-circles that stay on the pixel grid. */
.gm-stall::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 20px;
  z-index: 1;
  pointer-events: none;
  background-image:
    /* hem: half-circles hanging off the bottom edge of the canopy */
    repeating-radial-gradient(circle 4px at 6px 15px,
      var(--gm-wood-lt) 0 4px,
      transparent 4px 100%),
    linear-gradient(180deg,
      var(--gm-wood-dk) 0    3px,
      var(--gm-wood-lt) 3px  7px,
      var(--gm-wood)    7px  11px,
      var(--gm-wood-lt) 11px 15px,
      var(--gm-wood-dk) 15px 16px,
      transparent       16px 20px);
  background-size: 24px 20px, 100% 20px;
}

/* The counter: a lit lip, the top surface, a shadow under it, then boards with
   plank seams. Four planes front to back, which is the whole of "more depth". */
.gm-stall::after {
  content: "";
  position: absolute;
  inset: auto 0 0;
  height: 13px;
  z-index: 0;
  pointer-events: none;
  background-image:
    repeating-linear-gradient(90deg,
      transparent 0 16px,
      rgb(0 0 0 / 40%) 16px 17px,
      var(--gm-wood-lt) 17px 18px),
    linear-gradient(180deg,
      var(--gm-wood-lt) 0   1px,
      var(--gm-wood)    1px 4px,
      rgb(0 0 0 / 55%)  4px 5px,
      var(--gm-wood-dk) 5px 12px,
      rgb(0 0 0 / 45%)  12px 13px);
}

.gm-stall__head {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gm-gap-sm);
}

/* The keeper leans in from the left of the header. He is a DWARF: ruddy skin, a
   ginger beard to the counter and an iron helmet, and not one green pixel on him -
   at this size the beard alone is what tells him apart from the goblins. */
.gm-stall__keeper {
  width: 56px;
  height: 44px;
  flex: none;
  margin: -14px 2px -4px 0;
  image-rendering: pixelated;
}

.gm-stall__title {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--gm-ink-dim);
}

.gm-stall__reserve {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  color: var(--gm-ink-mute);
}

.gm-stall__reserve-label { white-space: nowrap; }

.gm-stall__reserve-btn {
  appearance: none;
  -webkit-appearance: none;
  padding: 2px 7px;
  font: inherit;
  font-family: var(--gm-font-num);
  font-weight: 700;
  color: var(--gm-gold);
  background: var(--gm-bg-3);
  border: 1px solid var(--gm-gold-dk);
  border-radius: var(--gm-radius);
  cursor: pointer;
}

.gm-stall__grid {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(76px, 1fr));
  gap: 4px;
}

.gm-stall__slot {
  appearance: none;
  -webkit-appearance: none;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto auto;
  gap: 1px 5px;
  align-items: center;
  padding: 4px 5px;
  font: inherit;
  text-align: left;
  color: var(--gm-ink);
  background: var(--gm-bg-3);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
  cursor: pointer;
  touch-action: manipulation;
}

.gm-stall__icon { grid-row: 1 / 3; width: 16px; height: 16px; flex: none; }  /* was 20 = x1.25 */

.gm-stall__held {
  font-family: var(--gm-font-num);
  font-size: 12px;
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The price, as a bar rather than a percentage. Full is top price, empty is the
   floor, and it is the same --gm-fill transform every other bar in the game uses
   (CONTRACT section 4: the stylesheet owns the motion, the game owns the value). */
.gm-stall__bar {
  display: block;
  height: 3px;
  background: var(--gm-bg);
  border: 1px solid var(--gm-line);
  overflow: hidden;
}

.gm-stall__bar > i {
  display: block;
  height: 100%;
  background: var(--gm-gold);
  transform: scaleX(var(--gm-fill, 1));
  transform-origin: left center;
  transition: transform .18s linear;
}

.gm-stall__slot[aria-selected="true"] {
  border-color: var(--gm-gold);
  background: var(--gm-line);
}

/* Locked exactly as the cart and the chest lock: flat black silhouette, dashed
   border, `???`, and no cursor - a slot you cannot sell from must not look like a
   slot you merely have not clicked. */
.gm-stall__slot[data-found="false"] {
  cursor: not-allowed;
  border-style: dashed;
}

.gm-stall__slot[data-found="false"] .gm-stall__icon { filter: brightness(0); }
.gm-stall__slot[data-found="false"] .gm-stall__held { color: var(--gm-ink-mute); letter-spacing: .1em; }
.gm-stall__slot[data-found="false"] .gm-stall__bar { visibility: hidden; }

.gm-stall__foot {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 4px;
}

.gm-stall__amounts { display: flex; gap: 3px; }

.gm-stall__amt {
  appearance: none;
  -webkit-appearance: none;
  flex: 1 1 0;
  min-width: 0;
  padding: 3px 0;
  font: inherit;
  font-family: var(--gm-font-num);
  font-size: 11px;
  font-weight: 700;
  color: var(--gm-ink-dim);
  background: var(--gm-bg-3);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
  cursor: pointer;
  touch-action: manipulation;
}

.gm-stall__amt[aria-selected="true"] {
  color: var(--gm-ink);
  background: var(--gm-line);
  border-color: var(--gm-gold-dk);
}

.gm-stall__sell {
  appearance: none;
  -webkit-appearance: none;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gm-gap-sm);
  padding: 5px var(--gm-gap-sm);
  font: inherit;
  color: var(--gm-ink);
  background: var(--gm-bg-3);
  border: 1px solid var(--gm-gold-dk);
  border-left-width: 3px;
  border-radius: var(--gm-radius);
  cursor: pointer;
  touch-action: manipulation;
}

.gm-stall__sell-label { font-size: 12px; font-weight: 700; }

.gm-stall__sell-value {
  font-family: var(--gm-font-num);
  font-size: 14px;
  font-weight: 700;
  color: var(--gm-gold);
}

.gm-stall__sell[data-ready="true"]:hover { background: var(--gm-line); }

.gm-stall__sell[data-ready="false"] {
  cursor: not-allowed;
  opacity: .55;
  border-left-color: var(--gm-line);
}

/* ---- Sell all, inside the market (v1.12 item 10) ----

   Deliberately QUIETER than the Sell button beside it. Selling one ore in a lot
   you chose is the considered move the market exists for; dumping the lot is the
   blunt one, and it walks every price in the game to the floor at once. So it is
   a full-width secondary bar under the pair rather than a third thing competing
   with them: gold text on the panel ground, no gold rail.

   The third line carries the number the owner suspected was missing — what the
   same ore would fetch into untouched markets — so the volume discount is on the
   button rather than only in the tooltip. */
.gm-stall__all {
  appearance: none;
  -webkit-appearance: none;
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--gm-gap-sm);
  width: 100%;
  padding: 5px var(--gm-gap);
  font: inherit;
  text-align: left;
  color: var(--gm-ink-dim);
  background: var(--gm-bg-3);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
  cursor: pointer;
  touch-action: manipulation;
}

.gm-stall__all-label {
  font-size: 11px;
  letter-spacing: .06em;
  text-transform: uppercase;
}

.gm-stall__all-value {
  font-family: var(--gm-font-num);
  font-size: 14px;
  font-weight: 700;
  color: var(--gm-gold);
}

.gm-stall__all-auto {
  flex-basis: 100%;
  font-family: var(--gm-font-num);
  font-size: 10px;
  color: var(--gm-ink-mute);
}

.gm-stall__all[data-ready="true"]:hover { background: var(--gm-line); }

.gm-stall__all[data-ready="false"] {
  cursor: not-allowed;
  opacity: .55;
}

.gm-stall__all[data-ready="false"] .gm-stall__all-value { color: var(--gm-ink-mute); }

/* ========================================================================== */
/* BUTTONS                                                                    */
/* ========================================================================== */

.gm-btn {
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  font-weight: 600;
  padding: 6px 12px;
  cursor: pointer;
  color: var(--gm-ink);
  background: var(--gm-bg-3);
  border: 1px solid var(--gm-line);
  border-bottom-width: 3px;
  border-radius: var(--gm-radius);
  touch-action: manipulation;
}

.gm-btn:hover { background: var(--gm-line); }
.gm-btn:active { border-bottom-width: 1px; transform: translateY(2px); }

.gm-btn--sm { padding: 3px 8px; font-size: 12px; }

.gm-btn--ghost {
  background: transparent;
  color: var(--gm-ink-dim);
}

.gm-btn--danger {
  color: var(--gm-red);
  border-color: var(--gm-red-dk);
}

.gm-btn--danger:hover { background: var(--gm-red-dk); color: var(--gm-ink); }

.gm-btn:disabled {
  cursor: not-allowed;
  opacity: .5;
  color: var(--gm-ink-mute);
}

/* ========================================================================== */
/* PANEL — tabs                                                               */
/* ========================================================================== */

/* The FOURTH look at the tab-bar label bug, and the first with real evidence instead
 * of a theory: the owner's own DevTools Computed panel, on `.gm-tabs__label` itself,
 * with a long list open below it. Everything the first three fixes put in place was
 * already correct - `display: block`, `justify-self: stretch`, a real `width` - and
 * still `height: 0px` with `overflow: hidden` clipping it. That is not a width bug at
 * all, which is the only thing the first three attempts ever touched.
 *
 * `.gm-tabs` is a flex ITEM of `.gm-panel` (flex-direction: column), sharing the
 * column's height with `.gm-tabpanes` below it. Nothing here said `flex-shrink: 0`,
 * so it carried the default of 1 - shrinkable - and when `.gm-tabpanes`'s long content
 * pushes the column's demanded height past what is available, flexbox compresses BOTH
 * items instead of only the one built to absorb it (`.gm-tabpanes` already has
 * `min-height: 0` and `overflow-y: auto` for exactly that job). Inside each
 * `.gm-tabs__tab` grid, the label's row has nothing forcing a minimum height the way
 * the icon's fixed-size SVG does, so it is what gives first - collapsing to the 0px
 * the owner measured, everywhere, the moment the tab bar itself is asked to be even
 * one pixel shorter than its own content wants. `flex-shrink: 0` says the tab bar is
 * never that pixel - only `.gm-tabpanes` may ever give ground. */
.gm-tabs {
  display: flex;
  flex-shrink: 0;
  gap: 1px;
  padding: 2px;
  overflow-x: auto;
  scrollbar-width: none;
  background: var(--gm-bg-2);
}

.gm-tabs::-webkit-scrollbar { display: none; }

/* v1.18.3, the owner: *"cuando hay una cantidad grande de elementos de cada categoria,
 * esta desborda y oculta los nombres"*. `min-width: 0` let flex-shrink crush every tab
 * to fit whatever the panel happened to be, and the label's own text-overflow:ellipsis
 * has no room to draw even one character below a certain width - which is why the tab
 * bar was going icon-only by ACCIDENT rather than by design the moment a seventh tab
 * (Settings) joined the six the panel's floor width was measured against below.
 *
 * FIXED WITHOUT RE-MEASURING FOR SEVEN, because that would only stay correct until an
 * eighth. `min-width: 52px` is the floor a label needs to draw in full at this font (see
 * .gm-tabs__label below) - so a tab can shrink to fit the row only until every label is
 * exactly legible, and not one pixel further. Past that, `.gm-tabs` already declares
 * `overflow-x: auto` - it existed for this and was never reachable while shrinkage had no
 * floor. Adding a tab now costs a wider scrollable row, never a blank one. */
.gm-tabs__tab {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  flex: 1 1 0;
  min-width: 52px;
  display: grid;
  justify-items: center;
  gap: 2px;
  padding: 6px 1px;
  cursor: pointer;
  font: inherit;
  color: var(--gm-ink-mute);
  background: transparent;
  border: 0;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  touch-action: manipulation;
}

.gm-tabs__tab:hover { color: var(--gm-ink-dim); background: var(--gm-bg-3); }

.gm-tabs__icon { width: 16px; height: 16px; }

/* v1.18.3: THIS is what min-width: 52px on .gm-tabs__tab is measured against, and
 * nothing else needs to be - UPGRADES/PRESTIGE/SETTINGS, the longest labels the game
 * ships, need 45.7px at this font, and 52px clears that with room to spare. Whichever
 * tab count the panel is asked to hold, this is the one number a wider bar or a
 * horizontal scroll falls back to; it does not need to change with the count, which is
 * the bug the last version of this comment was measured for six and silently wrong for
 * seven. The ellipsis stays as a backstop for a label longer than any the game ships.
 *
 * v1.18.4: min-width alone did not bring the text back - the owner still saw bare icons
 * after a forced reload AND in a private window, which rules out caching. `justify-self`
 * was unset here, so it inherited .gm-tabs__tab's `justify-items: center` - which means
 * a GRID ITEM SIZES TO ITS OWN CONTENT rather than to the column, and `max-width: 100%`
 * on a box that is already exactly as wide as its content resolves against ITS OWN
 * width, which is a box with nothing to overflow. That is a real, separately-known grid
 * quirk (`overflow`/`text-overflow` need a box with a size that comes from OUTSIDE the
 * content, not from it) and it would produce exactly this: no crash, no console error,
 * simply a label with no legible content, present in the DOM the whole time - which is
 * why it survived a hard reload and a private window with no cache at all to blame.
 * `justify-self: stretch` makes the label's box the full tab width instead, and
 * `text-align: center` keeps the icon and the label visually lined up the way
 * justify-items: center used to promise for both of them.
 *
 * v1.18.5: STILL bare icons, confirmed in a fresh private window - and the owner's own
 * DevTools screenshot is why this one is not a guess. Computed showed `display: inline`
 * and `width: auto` on the label, meaning the browser never blockified it into a grid
 * item at all. Per spec a grid item's box IS blockified automatically and `justify-self`
 * should have been moot - but `.gm-tabs__tab` is a `<button>`, and a `<button>` carries
 * its own internal UA formatting context that `display: grid` sits on top of rather than
 * replaces cleanly in every engine. The icon (an SVG) never showed the bug because a
 * replaced element respects width/height regardless of inline/block; a bare `<span>`
 * does not - `width` simply does not apply to an inline non-replaced box, which is
 * exactly the "auto x auto" the box model diagram had nothing else to show.
 *
 * Fixed by not depending on blockification happening at all: `display: block` says so
 * outright, so width, text-overflow and justify-self all have a box that unambiguously
 * takes them, in every engine, button quirk or not. */
.gm-tabs__label {
  display: block;
  justify-self: stretch;
  text-align: center;
  font-size: 9px;
  letter-spacing: .02em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Selection is an underline plus full-contrast ink, not a colour swap alone. */
.gm-tabs__tab[aria-selected="true"] {
  color: var(--gm-ink);
  background: var(--gm-bg-3);
  border-bottom-color: var(--gm-amber);
}

.gm-tabs__dot {
  position: absolute;
  top: 4px;
  right: calc(50% - 16px);
  width: 6px;
  height: 6px;
  background: var(--gm-green);
  border: 1px solid var(--gm-bg);
  border-radius: 50%;
}

/* THE MOBILE HUD's tab, v1.18.6. Hidden outside the phone breakpoint (below) -
 * desktop and tablet already show the mine and the panel side by side and never
 * need a switch between them, so an eighth tab there would only widen a bar that
 * seven already fill exactly (v1.18.3's min-width: 52px, above). */
.gm-tabs__tab--mine { display: none; }

/* ---- panes ---- */

.gm-pane {
  display: none;
  padding: var(--gm-gap);
}

.gm-pane[data-active="true"] {
  display: grid;
  gap: var(--gm-gap-sm);
  align-content: start;
}

.gm-empty {
  margin: 0;
  padding: var(--gm-gap-lg);
  text-align: center;
  font-size: 13px;
  color: var(--gm-ink-mute);
  border: 1px dashed var(--gm-line);
  border-radius: var(--gm-radius);
}

/* ---- buy amount bar ---- */

.gm-buybar {
  display: flex;
  align-items: center;
  gap: var(--gm-gap-sm);
  padding-bottom: var(--gm-gap-sm);
}

.gm-buybar__label {
  font-size: 11px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--gm-ink-mute);
}

.gm-buybar__opt {
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  font-family: var(--gm-font-num);
  font-size: 12px;
  min-width: 36px;
  padding: 3px 8px;
  cursor: pointer;
  color: var(--gm-ink-dim);
  background: var(--gm-bg-3);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
  touch-action: manipulation;
}

.gm-buybar__opt:hover { color: var(--gm-ink); }

.gm-buybar__opt[aria-selected="true"] {
  color: var(--gm-bg);
  background: var(--gm-amber);
  border-color: var(--gm-amber);
  font-weight: 700;
}

/* ---- the Trader's leash, per mineral (v1.17) ----
   Chips again, one per discovered ore, wrapping. They sit under the market's foot
   because that is where the decision they modify is taken. */
.gm-keep {
  display: grid;
  gap: 4px;
  padding: var(--gm-gap-sm) var(--gm-gap) var(--gm-gap);
}

.gm-keep__label {
  font-size: 11px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--gm-ink-mute);
}

.gm-keep__row { display: flex; flex-wrap: wrap; gap: 4px; }

.gm-keep__opt {
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  font-size: 10px;
  display: flex;
  align-items: center;
  gap: 3px;
  padding: 2px 5px;
  cursor: pointer;
  color: var(--gm-ink-dim);
  background: var(--gm-bg-3);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
  touch-action: manipulation;
}

.gm-keep__opt:hover { color: var(--gm-ink); }
.gm-keep__icon { width: 16px; height: 16px; }
/* Amber for "holding some back", red for "holding it all": two different promises,
   and the player needs to tell them apart at a glance on a row of nine. */
.gm-keep__opt[data-mode="keep"] { color: var(--gm-amber); border-color: var(--gm-amber); }
.gm-keep__opt[data-mode="hold"] { color: var(--gm-red); border-color: var(--gm-red); }

/* ---- the smelter (v1.17) ---- */
.gm-forge {
  display: grid;
  gap: var(--gm-gap-sm);
  padding: var(--gm-gap-sm) var(--gm-gap) var(--gm-gap);
  background: var(--gm-bg-2);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
}

.gm-forge__head {
  display: flex;
  align-items: center;
  gap: var(--gm-gap-sm);
}

.gm-forge__icon { width: 24px; height: 24px; }

.gm-forge__title {
  font-size: 12px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--gm-ink-dim);
}

.gm-forge__note {
  margin-left: auto;
  font-family: var(--gm-font-num);
  font-size: 11px;
  color: var(--gm-ink-mute);
}

/* The lot row on the forge (v1.18). It reuses .gm-stall__amt for the buttons, because
   it is literally the same control as the market's - the owner asked for "el mismo
   menu" - so only the row itself needs a rule. */
.gm-forge__amounts {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  margin-top: var(--gm-gap-sm);
}

.gm-forge__grid { display: grid; gap: 3px; }

.gm-forge__row {
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  display: grid;
  grid-template-columns: 16px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--gm-gap-sm);
  padding: 3px var(--gm-gap-sm);
  text-align: left;
  cursor: pointer;
  color: var(--gm-ink-dim);
  background: var(--gm-bg-3);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
  touch-action: manipulation;
}

.gm-forge__row[data-ready="true"] { color: var(--gm-ink); border-color: var(--gm-green-dk); }
.gm-forge__row:disabled { cursor: default; opacity: .55; }
.gm-forge__row-icon { width: 16px; height: 16px; }
.gm-forge__row-name { font-size: 11px; }
.gm-forge__row-count {
  font-family: var(--gm-font-num);
  font-size: 11px;
  color: var(--gm-amber);
}

/* ---- climbing back to a seam (v1.17) ----
   Sits under the depth readout, inside the mine, because that is where the player is
   already looking when they decide they want to be somewhere else. Chips again - the
   game has one shape for "pick one of these". */
.gm-climb {
  /* A child of the flex .gm-mine__depth. Full basis so it takes a line of its own
     under the depth figure instead of squeezing in beside the seam name. */
  flex: 1 0 100%;
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.gm-climb__opt {
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  font-size: 10px;
  padding: 2px 6px;
  cursor: pointer;
  color: var(--gm-ink-dim);
  background: var(--gm-bg-2);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
  touch-action: manipulation;
}

.gm-climb__opt:hover { color: var(--gm-ink); }

.gm-climb__opt[aria-selected="true"] {
  color: var(--gm-bg);
  background: var(--gm-amber);
  border-color: var(--gm-amber);
  font-weight: 700;
}

/* ---- the build shelves (v1.17) ----
   Same chip as the buy bar, deliberately: this game has one shape for "pick one of
   these" and a second one would read as a different kind of control. They WRAP rather
   than scroll sideways, because there will be a dozen of them once every ore has its
   pair and a row you have to drag is a row whose far end nobody finds. */
.gm-shelves {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  padding-bottom: var(--gm-gap-sm);
}

.gm-shelves__opt {
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  font-size: 11px;
  padding: 3px 7px;
  cursor: pointer;
  color: var(--gm-ink-dim);
  background: var(--gm-bg-3);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
  touch-action: manipulation;
}

.gm-shelves__opt:hover { color: var(--gm-ink); }

.gm-shelves__opt[aria-selected="true"] {
  color: var(--gm-bg);
  background: var(--gm-amber);
  border-color: var(--gm-amber);
  font-weight: 700;
}

/* ========================================================================== */
/* THE ITEM ROW — buildings and upgrades                                      */
/* The most-repeated element in the game. Everything here is built to survive  */
/* a 24-character name, a 7-digit count and a 320px-wide panel at once.        */
/* ========================================================================== */

.gm-item {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  width: 100%;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--gm-gap);
  align-items: center;
  padding: var(--gm-gap) var(--gm-gap) var(--gm-gap) calc(var(--gm-gap) + 4px);
  text-align: left;
  cursor: pointer;
  font: inherit;
  color: var(--gm-ink);
  background: var(--gm-bg-3);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
  touch-action: manipulation;
}

/* The affordability rail. Colour is only half the signal — the pattern of the
   rail is the other half, so it still reads without colour vision. */
.gm-item::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 4px;
  background: var(--gm-line);
}

/* SIZED BY THE CANVAS IT IS SHOWING, not by the row it sits in (v1.16).

   This one box receives three different canvases at runtime: a 24×24 building sprite,
   a 16×16 upgrade sprite, and the 16×16 padlock on a locked row. ARTSTYLE only survives
   whole-number scaling — `shape-rendering="crispEdges"` snaps rect edges to the device
   grid, so at ×1.5 some art pixels are one device pixel wide and their neighbours two —
   and no single width can be a whole multiple of both 16 and 24 at a sensible size.

   So util.svgIcon()/util.setIcon() stamp `data-canvas` with whatever symbol is in there
   and this sizes off that. The grid COLUMN stays 24px wide either way, so rows still
   line up; only the <svg> inside it changes. The old rule keyed off `.gm-item--upgrade`,
   which got the upgrade rows right and still left the padlock on a locked BUILDING row
   at ×1.5. */
.gm-item__icon {
  width: 24px;
  height: 24px;
  flex: none;
}

.gm-item__icon[data-canvas="16"] { width: 16px; height: 16px; }

/* Building rows carry a count and an effect line; upgrades carry neither.
   Reserving the right column keeps counts and costs on a shared vertical rule
   all the way down a 12-row list instead of ragging with the number width. */
.gm-item--building .gm-item__right { min-width: 5.5ch; }

.gm-item__main { min-width: 0; }

.gm-item__name {
  margin: 0;
  font-size: 14px;
  font-weight: 700;
  color: var(--gm-ink);
  overflow-wrap: anywhere;
}

.gm-item__desc {
  margin: 2px 0 0;
  font-size: 12px;
  line-height: 1.35;
  color: var(--gm-ink-dim);
  overflow-wrap: anywhere;
}

.gm-item__effect {
  margin: 2px 0 0;
  font-family: var(--gm-font-num);
  font-size: 11px;
  color: var(--gm-green);
}

.gm-item__right {
  display: grid;
  justify-items: end;
  gap: 2px;
  text-align: right;
}

.gm-item__count {
  font-family: var(--gm-font-num);
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  color: var(--gm-ink);
}

.gm-item__cost {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-family: var(--gm-font-num);
  font-size: 13px;
  font-weight: 700;
  white-space: nowrap;
  color: var(--gm-gold);
}

.gm-item__cost-icon { width: 16px; height: 16px; flex: none; }  /* was 12 = x0.75 */

.gm-item__cost[data-res="bars"]       { color: var(--gm-blue); }
.gm-item__cost[data-res="runestones"] { color: var(--gm-violet); }

/* ---- affordable ---- */

.gm-item[data-affordable="true"]::before { background: var(--gm-green); }

.gm-item[data-affordable="true"]:hover {
  background: var(--gm-line);
  border-color: var(--gm-green-dk);
}

/* ---- unaffordable: dimmed + hatched rail + underlined cost ---- */

.gm-item[data-affordable="false"] {
  cursor: not-allowed;
  opacity: .62;
}

.gm-item[data-affordable="false"]::before {
  background: repeating-linear-gradient(
    135deg,
    var(--gm-red-dk) 0 3px,
    transparent 3px 6px
  );
}

.gm-item[data-affordable="false"] .gm-item__cost {
  color: var(--gm-red);
  text-decoration: underline dotted currentColor 1px;
  text-underline-offset: 2px;
}

.gm-item[data-affordable="false"] .gm-item__effect { color: var(--gm-ink-mute); }

/* ---- locked: visible but inert ---- */

.gm-item[data-locked="true"] {
  cursor: default;
  opacity: .45;
  pointer-events: none;
}

.gm-item[data-locked="true"]::before {
  background: repeating-linear-gradient(
    135deg,
    var(--gm-line) 0 3px,
    transparent 3px 6px
  );
}

.gm-item[data-locked="true"] .gm-item__icon { filter: grayscale(1); }

.gm-item[data-locked="true"] .gm-item__name,
.gm-item[data-locked="true"] .gm-item__desc,
.gm-item[data-locked="true"] .gm-item__cost,
.gm-item[data-locked="true"] .gm-item__count {
  color: var(--gm-ink-mute);
}

/* ========================================================================== */
/* PRESTIGE                                                                   */
/* ========================================================================== */

.gm-prestige {
  display: grid;
  justify-items: center;
  gap: var(--gm-gap);
  padding: var(--gm-gap-lg);
  text-align: center;
  background: var(--gm-bg-3);
  border: 1px solid var(--gm-violet-dk);
  border-radius: var(--gm-radius);
}

.gm-prestige__sigil {
  width: calc(var(--gm-px) * 10);
  height: calc(var(--gm-px) * 10);
}

.gm-prestige__gain {
  margin: 0;
  font-size: 13px;
  color: var(--gm-ink-dim);
}

.gm-prestige__gain b {
  font-family: var(--gm-font-num);
  font-size: 18px;
  color: var(--gm-violet);
}

/* ---- progress to the next runestone (v1.14) ----

   The owner asked for a bar that moves proportionally to what is left before the
   next rune. Three parts on one row: what it is, how far along, and the two numbers
   behind it, because a bare bar is a shape and this one is counting gold.

   Violet throughout, like every other runestone surface in the game - the count in
   this pane, the sigil above it, the constellation - so the bar reads as belonging
   to the prestige currency and not as another resource meter.

   It uses --gm-fill and scaleX like the rock, the event timer and the stall price
   bar. One name, one writer, one reader (CONTRACT section 8): the game never writes
   a transform here, so the artist can compose one freely. */
.gm-prestige__next {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--gm-gap-sm);
  width: 100%;
}

.gm-prestige__next-label {
  font-size: 11px;
  color: var(--gm-ink-mute);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.gm-prestige__next-value {
  font-family: var(--gm-font-num);
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  color: var(--gm-ink-dim);
}

.gm-prestige__bar {
  display: block;
  position: relative;
  height: 8px;
  min-width: 40px;
  background: var(--gm-bg);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
  overflow: hidden;
}

.gm-prestige__bar > i {
  display: block;
  position: absolute;
  inset: 0;
  background: var(--gm-violet);
  transform-origin: left center;
  transform: scaleX(var(--gm-fill, 0));
}

/* ---- perk tree ---- */

.gm-perks {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: var(--gm-gap);
  padding-top: var(--gm-gap);
}

.gm-perks__branch {
  display: grid;
  gap: var(--gm-gap-sm);
  align-content: start;
  min-width: 0;
}

.gm-perks__title {
  margin: 0;
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--gm-ink-dim);
  border-bottom: 1px solid var(--gm-line);
  padding-bottom: 4px;
}

.gm-perks__branch[data-branch="pick"]  .gm-perks__title { color: var(--gm-amber); }
.gm-perks__branch[data-branch="horde"] .gm-perks__title { color: var(--gm-green); }
.gm-perks__branch[data-branch="greed"] .gm-perks__title { color: var(--gm-gold); }
.gm-perks__branch[data-branch="delve"] .gm-perks__title { color: var(--gm-blue); }
/* v1.18, the Forge branch. Topaz, so it reads as hot metal beside the four above. */
.gm-perks__branch[data-branch="forge"] .gm-perks__title { color: var(--gm-gold); }

.gm-perk {
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  font-size: 12px;
  text-align: left;
  padding: 6px 8px;
  cursor: pointer;
  color: var(--gm-ink-dim);
  background: var(--gm-bg-3);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
  min-width: 0;
  overflow-wrap: anywhere;
  touch-action: manipulation;
}

/* Owned / available / locked differ in fill, border AND weight. */
.gm-perk[data-state="owned"] {
  color: var(--gm-ink);
  font-weight: 700;
  background: var(--gm-violet-dk);
  border-color: var(--gm-violet);
}

/* The trophy case takes the same five branch colours as the constellation, off the
   same data-gem written by ui.js — on the branch here rather than on the node,
   because in this pane the whole column is one branch. Border only: these rows are
   text and a coloured fill behind 12px type is the one thing ARTSTYLE section 2
   will not have. */
.gm-perks__branch[data-gem="garnet"]    .gm-perk[data-state="owned"] { border-color: var(--gm-red); }
.gm-perks__branch[data-gem="emerald"]   .gm-perk[data-state="owned"] { border-color: var(--gm-green); }
.gm-perks__branch[data-gem="amber"]     .gm-perk[data-state="owned"] { border-color: var(--gm-amber); }
.gm-perks__branch[data-gem="sapphire"]  .gm-perk[data-state="owned"] { border-color: var(--gm-blue); }
.gm-perks__branch[data-gem="voidheart"] .gm-perk[data-state="owned"] { border-color: var(--gm-violet); }
.gm-perks__branch[data-gem="topaz"]     .gm-perk[data-state="owned"] { border-color: var(--gm-gold); }

.gm-perk[data-state="available"] {
  color: var(--gm-ink);
  border-color: var(--gm-amber);
  border-style: dashed;
}

.gm-perk[data-state="available"]:hover { background: var(--gm-line); }

.gm-perk[data-state="locked"] {
  color: var(--gm-ink-mute);
  cursor: not-allowed;
  opacity: .55;
  background: var(--gm-bg-2);
}

/* ========================================================================== */
/* ACHIEVEMENTS                                                               */
/* ========================================================================== */

/* 72px, not 84 (v1.15). The owner: *"la rejilla de logros... es demasiado grande"*.
   Forty-seven badges at 84px is a pane taller than any viewport it is shown in, and a
   tile that size is mostly padding - the icon is 24px and the two labels wrap anyway.
   At 72 an extra column fits at every breakpoint and the pane loses about a fifth of
   its height, which is the half of the item that a bigger bottom margin cannot fix.
   (The other half was the tab bar's own height being a stale literal - see measure()
   in ui.js.) */
.gm-achs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
  gap: var(--gm-gap-sm);
}

.gm-ach {
  display: grid;
  justify-items: center;
  gap: 4px;
  padding: var(--gm-gap-sm);
  text-align: center;
  background: var(--gm-bg-3);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
  cursor: help;
  min-width: 0;
}

/* 16px = x1 on a 16x16 canvas (v1.16). It was calc(--gm-px * 6) = 24, an x1.5 that
   crispEdges renders with uneven pixel widths. Smaller tiles also agree with the
   owner's v1.15 item about the Awards grid covering the tab labels. */
.gm-ach__icon {
  width: 16px;
  height: 16px;
}

.gm-ach__name {
  font-size: 10px;
  line-height: 1.25;
  color: var(--gm-ink-dim);
  overflow-wrap: anywhere;
}

/* What the badge pays (v1.9). Smaller and dimmer than the name — it is the second
   thing read, not the first — and it stays legible on a locked tile, because
   "what would this give me" is exactly the question a locked one has to answer. */
.gm-ach__bonus {
  font-size: 9px;
  line-height: 1.2;
  color: var(--gm-ink-mute);
  overflow-wrap: anywhere;
}

.gm-ach[data-unlocked="true"] .gm-ach__bonus { color: var(--gm-gold-dk); }

.gm-ach[data-unlocked="true"] {
  border-color: var(--gm-gold-dk);
}

.gm-ach[data-unlocked="true"] .gm-ach__name { color: var(--gm-ink); }

.gm-ach[data-unlocked="false"] {
  opacity: .4;
}

.gm-ach[data-unlocked="false"] .gm-ach__icon { filter: grayscale(1); }

/* ========================================================================== */
/* STATS                                                                      */
/* ========================================================================== */

.gm-stats { margin: 0; display: grid; gap: 1px; }

.gm-stats__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--gm-gap);
  align-items: baseline;
  padding: 6px var(--gm-gap);
  background: var(--gm-bg-3);
}

.gm-stats__row:nth-child(2n) { background: var(--gm-bg-2); }

.gm-stats__row dt {
  font-size: 12px;
  color: var(--gm-ink-dim);
  overflow-wrap: anywhere;
}

.gm-stats__row dd {
  margin: 0;
  font-family: var(--gm-font-num);
  font-size: 13px;
  font-weight: 700;
  color: var(--gm-ink);
  white-space: nowrap;
}

/* ========================================================================== */
/* SETTINGS                                                                   */
/* ========================================================================== */

.gm-set { display: grid; gap: var(--gm-gap); }

.gm-set__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--gm-gap);
  align-items: center;
}

.gm-set__label {
  font-size: 13px;
  color: var(--gm-ink-dim);
  overflow-wrap: anywhere;
}

.gm-set__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gm-gap-sm);
  justify-content: flex-end;
}

/* ---- range ---- */

.gm-set__range {
  appearance: none;
  -webkit-appearance: none;
  width: 140px;
  max-width: 100%;
  height: 16px;
  background: transparent;
  cursor: pointer;
  margin: 0;
}

.gm-set__range::-webkit-slider-runnable-track {
  height: 6px;
  background: var(--gm-bg);
  border: 1px solid var(--gm-line);
}

.gm-set__range::-moz-range-track {
  height: 6px;
  background: var(--gm-bg);
  border: 1px solid var(--gm-line);
}

.gm-set__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 12px;
  height: 16px;
  margin-top: -6px;
  background: var(--gm-amber);
  border: 1px solid var(--gm-bg);
}

.gm-set__range::-moz-range-thumb {
  width: 12px;
  height: 16px;
  border-radius: 0;
  background: var(--gm-amber);
  border: 1px solid var(--gm-bg);
}

/* ---- checkbox ---- */

.gm-set__check {
  appearance: auto;
  width: 18px;
  height: 18px;
  accent-color: var(--gm-green);
  cursor: pointer;
  margin: 0;
}

/* ---- save import/export ---- */

.gm-set__io {
  grid-column: 1 / -1;
  width: 100%;
  min-height: 88px;
  resize: vertical;
  padding: var(--gm-gap-sm);
  font-family: var(--gm-font-num);
  font-size: 11px;
  color: var(--gm-ink);
  background: var(--gm-bg);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
  -webkit-user-select: text;
  user-select: text;
}

.gm-set__note {
  grid-column: 1 / -1;
  margin: 0;
  padding: var(--gm-gap-sm) var(--gm-gap);
  font-size: 12px;
  color: var(--gm-amber);
  background: var(--gm-bg-3);
  border-left: 3px solid var(--gm-amber);
  border-radius: var(--gm-radius);
}

/* ========================================================================== */
/* OVERLAYS                                                                   */
/* ========================================================================== */

/* Bottom-left, not top-right. The tab bar owns the top-right corner on desktop
   and tablet, and a burst of achievement toasts after a Cave-In sat directly on
   top of PRESTIGE — covering navigation at exactly the moment the player wants
   it. Down here the stack can only ever overlap the mine view, which is a
   display rather than a control, and toasts are transient anyway.
   New toasts append at the bottom and push the older ones upward. */
.gm-toasts {
  position: fixed;
  z-index: 60;
  left: max(var(--gm-gap), env(safe-area-inset-left));
  bottom: max(var(--gm-gap), env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: flex-start;
  gap: var(--gm-gap-sm);
  pointer-events: none;
  max-width: min(320px, calc(100vw - 2 * var(--gm-gap)));

  /* Hard ceiling: roughly three cards, and it cannot grow past this however
     many toasts are alive. `justify-content: flex-end` keeps the newest pinned
     to the bottom and pushes the overflow out of the *top* of the box, where
     `overflow: hidden` clips it — so a burst drops the oldest rather than
     swallowing the play area. On a 390x844 phone six toasts previously covered
     the mine, the rock, the readouts and the DIG button entirely. */
  max-height: min(30vh, 210px);
  overflow: hidden;
}

.gm-toast { flex: none; }

.gm-toast {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--gm-gap);
  align-items: center;
  padding: var(--gm-gap);
  background: var(--gm-bg-2);
  border: 1px solid var(--gm-line);
  border-left: 3px solid var(--gm-gold);
  border-radius: var(--gm-radius);
  box-shadow: 0 2px 0 var(--gm-bg);
}

.gm-toast__icon { width: 16px; height: 16px; }   /* x1 on a 16x16 badge canvas */

.gm-toast__title {
  display: block;
  font-size: 13px;
  color: var(--gm-ink);
}

.gm-toast__text {
  display: block;
  font-size: 12px;
  color: var(--gm-ink-dim);
}

/* Only the two kinds the game actually emits. Amber separates a golden-goblin
   catch from an achievement, which would otherwise be the same gold rail. */
.gm-toast[data-kind="achievement"] { border-left-color: var(--gm-gold); }
.gm-toast[data-kind="golden"]      { border-left-color: var(--gm-amber); }

/* ---- modal ---- */

.gm-modal {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: grid;
  place-items: center;
  padding: var(--gm-gap-lg);
  background: rgba(16, 11, 8, .82);
}

.gm-modal__box {
  width: min(440px, 100%);
  max-height: 80dvh;
  overflow-y: auto;
  display: grid;
  gap: var(--gm-gap);
  padding: var(--gm-gap-lg);
  background: var(--gm-bg-2);
  border: 1px solid var(--gm-line);
  border-top: 3px solid var(--gm-amber);
  border-radius: var(--gm-radius);
}

.gm-modal__title {
  margin: 0;
  font-size: 18px;
  color: var(--gm-ink);
}

.gm-modal__body {
  font-size: 13px;
  line-height: 1.5;
  color: var(--gm-ink-dim);
}

/* Buttons sit in their own row, right-aligned, with Cancel to the LEFT of the
   action — the direction the eye leaves a dialog in, so the safe answer is the one
   it passes first. The row exists even when there is only one button, so a message
   and a question put their button in the same place (v1.9). */
.gm-modal__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--gm-gap);
}

.gm-modal__box .gm-btn { justify-self: end; }

/* A question is a red-topped box, whatever it is asking, because both of the two
   that exist — wipe the save, collapse the mine — destroy something. */
.gm-modal[data-modal="blocked"] .gm-modal__box { border-top-color: var(--gm-red); }

.gm-modal[data-modal="prestige"] .gm-modal__box { border-top-color: var(--gm-violet); }
/* "notice" was "offline" until GDD v1.8 deleted offline progress. The only thing
   still wearing it is the unreadable-save message, which is a notice and not a
   welcome-back. */
.gm-modal[data-modal="notice"]   .gm-modal__box { border-top-color: var(--gm-amber); }

/* ---- tooltip ---- */

.gm-tooltip {
  position: fixed;
  z-index: 80;
  max-width: 240px;
  padding: 6px 8px;
  font-size: 12px;
  line-height: 1.4;
  color: var(--gm-ink);
  background: var(--gm-bg);
  border: 1px solid var(--gm-line);
  border-radius: var(--gm-radius);
  pointer-events: none;
  box-shadow: 0 2px 0 rgba(0, 0, 0, .5);
}

/* v1.12: a text tooltip may be written as LINES. The owner called the market
   tooltip "engorroso" and the fix was three labelled figures instead of two
   sentences of prose — which only reads as three figures if the newlines survive.
   `pre-line` and not `pre`: the source is indented JavaScript, so runs of spaces
   still have to collapse. Tooltips that are one sentence are unaffected. */
.gm-tooltip[data-kind="text"] { white-space: pre-line; }

/* ---- the seam menu (v1.9) ----
   Hovering the band name opens a list of what is actually in this wall: sprite,
   name, chance. Three columns on a grid so the numbers line up in a column the eye
   can run down — a percentage that wanders left and right with the length of the
   ore name is a table pretending not to be one. It is wider than a text tooltip and
   says so, rather than letting `max-width` wrap "Adamantite" onto two lines. */
.gm-tooltip[data-kind="seam"] {
  max-width: 220px;
  min-width: 168px;
  padding: 7px 9px 8px;
}

.gm-tooltip__title {
  margin-bottom: 5px;
  padding-bottom: 4px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--gm-amber);
  border-bottom: 1px solid var(--gm-line);
}

.gm-tooltip__row {
  display: grid;
  grid-template-columns: 16px minmax(0, 1fr) auto;
  align-items: center;
  gap: 7px;
  padding: 1px 0;
}

.gm-tooltip__icon { width: 16px; height: 16px; }

.gm-tooltip__name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--gm-ink-dim);
}

/* Tabular figures, right-aligned: this is the column being compared. */
.gm-tooltip__pct {
  font-family: var(--gm-font-num);
  font-variant-numeric: tabular-nums;
  text-align: right;
  color: var(--gm-ink);
}

/* ========================================================================== */
/* Responsive scaling for the mine                                            */
/* Every rock size is a whole multiple of its 32px source grid and every       */
/* goblin a whole multiple of 24px, so the pixels stay square at every step.   */
/* ========================================================================== */

/* The rock's size and the goblin's size are the only two numbers here: the HP
   bar and the crew's two standing radii are all derived from them, so a
   breakpoint cannot move one and leave the others behind. */

@media (min-width: 1440px) {
  .gm-mine { --gm-rock-size: 224px; }                 /* 7x */
}

@media (max-width: 1023px) {
  .gm-mine { --gm-rock-size: 160px; }                 /* 5x */
}

@media (max-width: 639px) {
  .gm-mine {
    --gm-rock-size: 128px;                            /* 4x */
    --gm-goblin-size: 24px;                           /* 1x */
  }

  /* Desktop's --gm-morale-w (layout.css, min(240px, 55%)) plus .gm-mine__seam's own
     max-width (min(280px, 65%)) sum to 120% of the row - guaranteed overlap at every
     phone width, confirmed live at 390px (the seam badge and "Stay here" sat on top
     of the Morale card).

     v1.18.6, replacing the previous fix rather than adding to it. The first phone
     fix shrank Morale to one compact line and reserved a fixed 76px for row 1 to
     hold it under, with Morale itself `position: absolute` and a guessed `top`
     offset into the space that budget was supposed to leave free. That only ever
     held for a mine with no checkpoints yet - the moment a second seam is
     discovered, .gm-mine__depth grows a THIRD wrapped line (the scrollable
     .gm-climb chip row, below it) that 76px was never sized for, the depth block
     overflowed its own row into the rock's, and Morale's guessed `top` landed
     wherever that overflow happened to leave a gap. Confirmed live at 960m/copper:
     the seam badge sitting on top of the chip row, Morale rendering as a stray
     full-width bar wherever it landed.

     Fixed by not guessing a height at all. Morale gets its OWN grid row now
     (below), directly under the depth block instead of layered over it - so
     however tall .gm-mine__depth actually turns out (two lines with no
     checkpoints yet, three once .gm-climb has something to show), Morale starts
     exactly where it ends, the ordinary way one grid row follows another. It
     keeps the bar-less single-line style from the first fix - still short enough
     to be worth a whole row of its own rather than back to the full desktop card. */
  .gm-morale {
    grid-row: 2;
    grid-column: 1;
    position: relative;
    max-width: none;
    justify-self: stretch;
    margin: 0 0 var(--gm-gap-sm);
    grid-template-columns: auto auto;
    justify-content: center;
    padding: 2px var(--gm-gap);
  }

  .gm-morale__bar,
  .gm-morale__cap { display: none; }

  /* Five rows now, not desktop's four: the depth block, Morale (new, above), the
     rock/goblins/Mani (shifted from row 2 to row 3), DIG (row 3 to row 4), and an
     unused floor row. R7 in practice: .gm-mine declares rows and no columns, so
     inserting a row here means every other explicitly-placed child has to say
     which row it means now (below), or auto-placement invents a second COLUMN
     instead of shifting one down - see the note on .gm-rock in layout.css for
     exactly what that looks like when it goes wrong. auto/auto, not a fixed
     pixel guess: each row is exactly as tall as what is actually in it, which is
     the whole fix. */
  .gm-mine { grid-template-rows: auto auto minmax(0, 1fr) auto auto; }

  .gm-rock,
  .gm-goblins,
  .gm-mani { grid-row: 3; }

  .gm-mine > .gm-dig { grid-row: 4; }

  .gm-mine__depth { padding: var(--gm-gap-sm); align-content: start; }
  .gm-mine__depth-value { font-size: 18px; }

  /* .gm-climb (the checkpoint-seam chip row) wraps to as many lines as there are
     discovered ores on desktop, which is exactly the variable height that made a
     fixed `top` for the repositioned .gm-morale above unsafe - a 2-chip fresh mine
     and an 8-chip late-game one do not clear the same line. A single scrollable
     line removes the variable instead of chasing it: predictable height at every
     point in a run, and a swipeable chip strip is the normal mobile pattern for a
     row that cannot fit. */
  .gm-climb {
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
  }

  .gm-climb__opt { flex: none; }
}

/* ========================================================================== */
/* Phone tweaks that belong with the components rather than the grid          */
/* ========================================================================== */

@media (max-width: 639px) {
  .gm-tabs {
    border-radius: 0;
    border-left: 0;
    border-right: 0;
    border-bottom: 0;
    background: var(--gm-bg-2);
    box-shadow: 0 -2px 0 var(--gm-bg);
  }

  .gm-tabs__tab { padding: 8px 4px; }

  /* Bottom bar: the underline would sit under the thumb, so mark the active
     tab along its top edge instead. */
  .gm-tabs__tab {
    border-bottom: 0;
    border-top: 2px solid transparent;
  }

  .gm-tabs__tab[aria-selected="true"] { border-top-color: var(--gm-amber); }

  /* THE MOBILE HUD, v1.18.6 - see the long note in layout.css above the phone
   * .gm-app rule for why. Two pieces live here: showing the eighth tab (hidden
   * by default, above), and the actual screen switch, which is plain show/hide
   * keyed off `data-active-tab` (written by ui.renderTab()) rather than a grid
   * area, because the elements it switches between - .gm-mine, .gm-figures,
   * .gm-core's shop widgets, .gm-tabpanes - are four separate siblings of
   * #gm-app and a sibling of .gm-core, not one box a grid track could size. */
  .gm-tabs__tab--mine { display: grid; }

  /* Pinned to the ceiling the same way .gm-tabs is already pinned to the floor
   * (layout.css) - both position: fixed to the viewport regardless of how deep
   * in the tree they sit, and both independent of which screen #gm-app is
   * currently showing beneath them. Left nested inside .gm-core rather than
   * moved in the markup: the rule right below keeps the rest of .gm-core out of
   * the way whenever the mine is not the active screen, but #gm-res itself is
   * never part of that - it is meant to survive every tab, gold included. */
  #gm-res {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 40;
    padding-top: max(var(--gm-gap-sm), env(safe-area-inset-top));
    background: var(--gm-bg-2);
    /* Its base rule (above, shared with .gm-tabs and .gm-tabpanes) draws a
     * rounded border on all four sides, meant for a card sitting inside the
     * column - wrong for a strip pinned flush to the viewport edge. Cleared the
     * same way .gm-tabs already clears the sides IT doesn't need, just mirrored
     * for the opposite edge. */
    border-radius: 0;
    border-left: 0;
    border-right: 0;
    border-top: 0;
  }

  /* Default state: the mine screen. Chosen as the default (rather than the tab
   * panes) so the instant before boot() sets data-active-tab - and any state
   * where #gm-app has no such attribute at all - shows the rock rather than an
   * empty strip, the same "open on the mine, not on a menu" reasoning main.js
   * uses to pick "mine" as the phone's first tab. */
  .gm-tabpanes { display: none; }

  /* Every other tab: the reverse. .gm-core keeps rendering (it is what carries
   * the fixed #gm-res above), but everything inside it BESIDES #gm-res goes
   * away with the rest of the mine screen - the chest, the market and the
   * smelter belong to the "mine" tab, not to Build/Upgrades/Prestige/etc. */
  #gm-app[data-active-tab]:not([data-active-tab="mine"]) .gm-mine,
  #gm-app[data-active-tab]:not([data-active-tab="mine"]) .gm-figures,
  #gm-app[data-active-tab]:not([data-active-tab="mine"]) .gm-core > *:not(#gm-res) {
    display: none;
  }

  #gm-app[data-active-tab]:not([data-active-tab="mine"]) .gm-tabpanes {
    display: block;
  }

  /* Sit above the docked tab bar, and keep the ceiling tight — on a phone the
     play area is the whole screen, so the stack gets less of it, not more. */
  .gm-toasts {
    left: max(var(--gm-gap), env(safe-area-inset-left));
    right: max(var(--gm-gap), env(safe-area-inset-right));
    bottom: calc(60px + var(--gm-gap) + env(safe-area-inset-bottom));
    align-items: stretch;
    max-width: none;
    max-height: min(26vh, 176px);
  }

  .gm-item__count { font-size: 16px; }

  /* Vertical room is the scarce resource on a phone, so the column tightens:
     the tiles shrink, the ore inventory goes back to compact chips, and the
     pick stays the biggest thing on screen without eating the fold. */
  .gm-dig { padding: var(--gm-gap-sm) var(--gm-gap) var(--gm-gap); }
  .gm-dig__icon {
    width: calc(var(--gm-px) * 14);
    height: calc(var(--gm-px) * 14);
  }
  .gm-dig__sub { font-size: 13px; }

  .gm-res__item { padding: var(--gm-gap-sm); }
  .gm-res__value { font-size: 17px; }
  .gm-res__item[data-res="gold"] .gm-res__value { font-size: 22px; }
  /* The phone override is gone: 24 on a 16 canvas is x1.5. The desktop 32 (x2) is the
     nearest whole step and 16 would halve it, so the bar keeps the larger one. */

  .gm-ores { grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); }
  /* x2, down from the desktop x3 and up from the 16px (x1) that was here - the phone
     cell is the widest of the two, so the sprite was smallest exactly where there was
     most room for it. Integer scale, like every other sprite size in this file. */
  .gm-ores__icon { width: 32px; height: 32px; }

  /* A narrower wagon: shorter rim, smaller wheels pulled in towards the middle so
     they stay under the body rather than out past its corners. */
  .gm-cart { padding: 12px 4px 6px; margin-bottom: 7px; }
  .gm-cart::before { height: 12px; }
  .gm-cart::after {
    inset: auto 0 -7px;
    height: 9px;
    background-image:
      radial-gradient(circle 4px at 16px 4px,
        var(--gm-ink-mute) 0 2px, var(--gm-ink-dim) 2px 4px, transparent 4px 100%),
      radial-gradient(circle 4px at calc(100% - 16px) 4px,
        var(--gm-ink-mute) 0 2px, var(--gm-ink-dim) 2px 4px, transparent 4px 100%),
      linear-gradient(180deg,
        transparent 0 3px, var(--gm-ink-dim) 3px 5px, transparent 5px 100%),
      linear-gradient(180deg,
        transparent 0 7px, var(--gm-line) 7px 9px, transparent 9px 100%);
  }

  /* The cave is 26vh here, so the ceiling gets a proportionally shorter reach:
     18px to 28px against a depth readout about 30px tall. The three tooth sizes
     survive — only the band above them shrinks — because dropping to one size is
     exactly the flatness v1.9 was asked to fix, and it is the small screen that
     needs the silhouette to read. */
  .gm-mine__sky::before {
    --gm-roof: calc(6px + 10px * var(--gm-depth));
    height: calc(var(--gm-roof) + 20px);
  }

  /* Ten cells in a phone-width column: 1x gems, three or four across. */
  .gm-chest {
    grid-template-columns: repeat(auto-fill, minmax(46px, 1fr));
    padding: 15px 4px 6px;
  }
  .gm-chest::before { height: 11px; }
  .gm-chest::after { inset: 11px 0 auto; }
  /* x1 is the smallest whole step for a 24 canvas, so the phone keeps 24. */
  .gm-chest__icon { width: 24px; height: 24px; }
}


/* ============================================================ ascension =====
   GDD v1.5. The Cave-In opens a sky: one constellation per perk branch — five
   of them since Warren Management landed, and nothing here may assume a count.
   Gems joined tier by tier: owned ones lit, the next one dark with its price,
   and nothing past it drawn at all.

   Everything here is drawn SVG rather than the ore sprites, because a
   constellation needs the connecting lines to mean something - they light up as
   the branch is climbed, which is the only place in the game that shows the
   player the shape of what they are building.

   It should read as a night sky, not as a flowchart. That is four things: a
   star field that does not scale with the diagram, a dotted path where the
   climb has not happened yet, a lit gem that blooms, and branch names set like
   the layer name over the cave so the two screens are the same game. */

.gm-ascend {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: grid;
  place-items: center;
  padding: var(--gm-gap);
  /* Not a grey scrim over the game — the mine has just collapsed and this is
     what is above it. Violet is the prestige spend (ARTSTYLE section 1) and
     this is the one screen it owns outright. */
  background:
    radial-gradient(ellipse 90% 80% at 50% 42%,
      rgb(88 53 155 / 22%) 0 40%,
      rgb(88 53 155 / 10%) 40% 70%,
      transparent 70%),
    rgb(6 4 10 / 86%);
}

.gm-ascend[hidden] { display: none; }

/* WIDTH, and what could not be solved here.

   The sky's viewBox is 105 user units per branch: five branches is 525 wide,
   and at 16px padding inside a 560px box on a 360px phone that scaled to 0.59,
   which put the 9px perk labels at about 5.4px. Two things are done about it:

   1. The box is much wider, so anything from a small laptop up renders the sky
      ABOVE 1:1 and the labels above 9px. This is the real fix and it is free.
   2. Below that, the sky is floored at its natural size and the box scrolls
      sideways to reach it, rather than shrinking the type into illegibility.
      A phone therefore pans across the constellations at full size.

   What CSS cannot do is reflow five columns into two, because the geometry is
   drawn into the SVG at mount. If the owner finds the sideways pan awkward on a
   phone, that is the coder's fix: wrap the columns at a breakpoint, or emit one
   branch at a time. --gm-sky-min is the natural width, 105 per branch; the
   fallback is right for five and the coder can publish the real number from
   buildAscension(), where skyW is already in hand. */
.gm-ascend__box {
  width: min(880px, 100%);
  max-height: 100%;
  overflow: auto;
  overscroll-behavior: contain;
  padding: var(--gm-gap-lg);
  text-align: center;
  background: var(--gm-bg);
  border: 1px solid var(--gm-violet-dk);
  border-radius: var(--gm-radius);
  box-shadow: 0 18px 48px rgb(0 0 0 / 70%);
}

.gm-ascend__title {
  margin: 0 0 var(--gm-gap-sm);
  font-size: 20px;
  letter-spacing: .04em;
  color: var(--gm-ink);
}

.gm-ascend__sub {
  margin: 0 0 var(--gm-gap);
  font-size: 13px;
  color: var(--gm-ink-dim);
}

.gm-ascend__runes { color: var(--gm-gold); }

/* The night. Stars are a CSS background on the SVG box, which means they are
   painted in CSS pixels and do NOT scale with the viewBox — so they stay a
   crisp 2px whether the sky is rendering at 0.6x or 1.6x, and the constellation
   scales past them like something much further away. Three layers on coprime
   tiles (61x43, 89x67, 53x97), the same trick the cave wall uses so the eye
   never finds the repeat. */
.gm-ascend__sky {
  --gm-sky-min: 525px;                     /* 105 user units per branch x 5 */
  display: block;
  width: 100%;
  min-width: var(--gm-sky-min);
  height: auto;
  margin-bottom: var(--gm-gap);
  border-radius: var(--gm-radius);
  background-color: var(--gm-bg);
  background-image:
    radial-gradient(circle 1px at 14px 11px, var(--gm-ink-dim) 0 100%, transparent 100%),
    radial-gradient(circle 1px at 37px 26px, var(--gm-ink-mute) 0 100%, transparent 100%),
    radial-gradient(circle 1px at 8px 31px,  var(--gm-violet)   0 100%, transparent 100%),
    radial-gradient(ellipse 78% 72% at 50% 46%,
      transparent 0 46%,
      rgb(6 4 10 / 45%) 46% 72%,
      rgb(6 4 10 / 80%) 72% 100%);
  background-size: 61px 43px, 89px 67px, 53px 97px, 100% 100%;
  background-repeat: repeat, repeat, repeat, no-repeat;
}

/* A branch that has been climbed glows along its spine. One that has not is a
   dotted line — colour alone never carries a state here (ARTSTYLE section 2),
   so "the way on" is a different KIND of line, not just a dimmer one. */
.gm-ascend__link {
  stroke: var(--gm-violet-dk);
  stroke-width: 2;
  stroke-dasharray: 2 4;
  stroke-linecap: round;
  fill: none;
}

.gm-ascend__link[data-state="lit"] {
  stroke: var(--gm-gold);
  stroke-width: 3;
  stroke-dasharray: none;
  opacity: .8;
}

/* Gems. Owned ones are lit and carry a small bloom — the one deliberate soft
   edge in the project, because a constellation of hard-edged diamonds reads as
   a flowchart and the whole point of this screen is that it does not. It is
   confined to this one selector; nothing on the pixel-art side has a filter.
   The next gem is a dark stone with a gold rim; anything past it is drawn as a
   dotted outline so "not yet" is a texture as well as a colour. */
/* v1.12, the owner: *"El fondo de los prestigios escogidos no hace matching con el
   color de los nombres de sus categorias, es un morado y quedaria mejor con ese
   color"*.

   The purple they saw is this rule: every node was outlined in --gm-violet-dk
   whatever branch it belonged to, which is Voidheart's colour worn by all five —
   so an owned Garnet node was a red diamond in a purple socket. The base outline is
   neutral now and the branch colour is stated in ONE place per branch, on a
   variable that both the node and its constellation name read. That is the
   "matching" half: the name over a constellation and the stars inside it are the
   same hue by construction rather than by two lists agreeing. */
.gm-ascend__gem {
  cursor: pointer;
  stroke: var(--gm-line);
  stroke-width: 1.5;
  fill: var(--gm-bg-2);
  transition: fill 120ms linear, stroke 120ms linear, opacity 120ms linear;
}

.gm-ascend__gem[data-state="owned"] {
  fill: var(--gm-gold);
  stroke: var(--gm-ink);
  stroke-width: 1;
  filter: drop-shadow(0 0 3px var(--gm-gold));
}

/* ---- branch colours (v1.8 item 10) ----
   The owner: *"el color de las perks obtenidas deberia coincidir con el color de
   las gemas"*. Each branch is named after one of the ten collectibles in
   data.PERK_BRANCHES and ui.js writes that name onto the node as `data-gem`;
   these five rules are the only place the name meets a hue, and every hue is the
   token the gem's own sprite is drawn with.

   Only the OWNED state takes the colour. `ready` stays gold because gold is the
   affordance for "you can buy this" everywhere else in the game, and `locked`
   stays a dotted grey outline — a branch you have not climbed must not advertise
   its colour, or the constellation is fully lit from minute one. A branch with no
   gem keeps the gold above, which is visible rather than silent. */
/* One variable per branch, set on ANY element carrying the data-gem attribute -
   the node and the constellation name both do - and every rule below reads it. Five
   declarations instead of fifteen, and a branch cannot end up with a coloured star
   and an amber name again. */
[data-gem="garnet"]    { --gm-branch: var(--gm-red); }
[data-gem="emerald"]   { --gm-branch: var(--gm-green); }
[data-gem="amber"]     { --gm-branch: var(--gm-amber); }
[data-gem="sapphire"]  { --gm-branch: var(--gm-blue); }
[data-gem="voidheart"] { --gm-branch: var(--gm-violet); }
[data-gem="topaz"]     { --gm-branch: var(--gm-gold); }

.gm-ascend__gem[data-gem][data-state="owned"] {
  fill: var(--gm-branch);
  stroke: var(--gm-bg);
  filter: drop-shadow(0 0 4px var(--gm-branch));
}

.gm-ascend__gem[data-state="ready"] {
  fill: var(--gm-bg-3);
  stroke: var(--gm-gold);
  stroke-width: 2;
}

.gm-ascend__gem[data-state="locked"] {
  cursor: default;
  fill: none;
  stroke: var(--gm-ink-mute);
  stroke-dasharray: 2 3;
  opacity: .6;
}

.gm-ascend__gem:focus-visible { outline: 2px solid var(--gm-gold); outline-offset: 3px; }

/* The label is the gem's immediate next sibling in the sky (ui.buildAscension
   appends link, gem, label per node), which is what lets the label follow the
   gem's state without the game having to mark it twice. If that order ever
   changes, these three rules go quiet rather than wrong. */
/* v1.12, the owner: *"quiero que las letras tengan contornos negros"*. The outline
   was --gm-bg, which is the panel's own near-black and therefore invisible against
   it - it existed to punch the label out of the connector lines behind it, not to be
   seen. Against a constellation, a name can now land on a star, on a link or on
   empty sky, so it needs a real black halo that works over all three. Literal black
   rather than a token, because that is what was asked for and there is no token for
   pure black in the palette. */
.gm-ascend__label {
  font-size: 9px;
  fill: var(--gm-ink-dim);
  text-anchor: middle;
  pointer-events: none;
  paint-order: stroke fill;
  stroke: #000;
  stroke-width: 3;
  stroke-linejoin: round;
}

.gm-ascend__gem[data-state="owned"] + .gm-ascend__label { fill: var(--gm-ink); }
.gm-ascend__gem[data-state="ready"] + .gm-ascend__label { fill: var(--gm-gold); }
.gm-ascend__gem[data-state="locked"] + .gm-ascend__label { fill: var(--gm-ink-mute); }

/* The branch names are the constellation names, and since v1.12 each one is its
   own branch's colour rather than all five being amber - which is the other half of
   the owner's "matching": the name over a constellation and the stars in it are one
   hue, read from the same --gm-branch variable. Amber survives as the fallback for a
   branch with no gem, which is visible rather than silent. */
.gm-ascend__branchname {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  fill: var(--gm-branch, var(--gm-amber));
  text-anchor: middle;
  pointer-events: none;
  paint-order: stroke fill;
  stroke: #000;
  stroke-width: 3;
  stroke-linejoin: round;
}

.gm-ascend__detail {
  min-height: 2.6em;
  margin: 0 0 var(--gm-gap);
  font-size: 13px;
  color: var(--gm-ink-dim);
}

/* Phone. This block has to sit AFTER the ascension rules above, not up with the
   other phone tweaks: a media query adds no specificity, so an equal-specificity
   rule later in the file wins regardless of the query. Every pixel of padding
   here is a pixel of sky the player has to pan past, so the margins go first. */
@media (max-width: 639px) {
  .gm-ascend { padding: var(--gm-gap-sm); }
  .gm-ascend__box { padding: var(--gm-gap); }
  .gm-ascend__title { font-size: 17px; }
}
