/* THORNHOLD HUD — parchment & gold chrome (KR-grade).
   Spacing rides an 8px grid (4px half-steps for optics). */
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-user-select: none; user-select: none; }
:root {
  --gold: #ffcf4d;
  --gold-soft: #ffd98a;
  --gold-deep: #d99a24;
  --gold-edge: #8a5f14;
  --gold-line: #d8b25a;
  --parchment: #f3e6c8;
  --parchment-dim: #cdbfa3;
  --walnut-hi: rgba(48, 37, 23, 0.96);
  --walnut-lo: rgba(22, 16, 10, 0.96);
  --walnut-solid: #1d1610;
  --trim: rgba(255, 207, 77, 0.45);
  --red: #ff5a6e;
  /* r24: ONE type family. Measured with CDP CSS.getPlatformFontsForNode on
     every text node of the HUD, r23 rasterised THREE platform families —
     "Iowan Old Style" (the small-caps tier: the banner, the labels, the
     toolbar caption, the tower names), "Avenir Next" (the top-left chips'
     digits) and "Avenir Next Heavy" (the tray costs, the foes count, the
     sell price — weight 800 picks a different physical face). O1: "it mixes
     two type families: the top-left plates' digits are a sans-serif
     grotesque, the whole bottom bar a small-caps serif" — and the bottom bar
     was itself mixed, its LABELS serif and its NUMERALS sans. --ui-font is
     retired from the chrome; every glyph the HUD draws is now the display
     face, with lining tabular figures so the numerals stay column-aligned
     (Iowan's default figures are lining already; the declaration pins it). */
  --display-font: "Iowan Old Style", "Palatino Nova", Palatino, "Book Antiqua", Georgia, serif;
  --ui-font: var(--display-font);
  --hairline: inset 0 1px 0 rgba(255, 236, 190, 0.09);
  --drop: 0 4px 14px rgba(0, 0, 0, 0.45);
  /* r7: one framed-plate language for every piece of chrome, lifted from the
     wave banner (parchment scanlines on walnut, gold hairline rules). */
  --plate-lines: repeating-linear-gradient(0deg, rgba(255, 240, 200, .025) 0 1px, transparent 1px 3px);
  --plate: linear-gradient(180deg, rgba(46, 35, 22, .97), rgba(20, 14, 9, .97));
  --plate-ring: inset 0 0 0 1px rgba(0, 0, 0, .65), inset 0 1px 0 rgba(255, 236, 190, .1);
  --plate-shadow: 0 6px 16px rgba(0, 0, 0, .5), 0 0 0 1px rgba(0, 0, 0, .55);
  /* Metallic gold button face: a specular break at 40–55% instead of a flat web gradient. */
  --gold-face: linear-gradient(180deg, #ffeab0 0%, #f6c95a 40%, #d99a24 55%, #e9b13a 100%);
  --iron-face: linear-gradient(180deg, #4a3d2c, #221a10);
  --tint: #ffcf4d;
}
/* Corner rivets: four gold bosses sitting on the inner hairline's corners. */
.stat-chip::after, .ctl-cluster::after, .bm-card::after, #toast::after, #boss-bar::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
  --rv: radial-gradient(circle, #e8c86e 0 1.3px, rgba(0, 0, 0, 0) 2.1px);
  background:
    var(--rv) 1px 1px / 6px 6px no-repeat,
    var(--rv) calc(100% - 1px) 1px / 6px 6px no-repeat,
    var(--rv) 1px calc(100% - 1px) / 6px 6px no-repeat,
    var(--rv) calc(100% - 1px) calc(100% - 1px) / 6px 6px no-repeat;
}
/* Inner gold hairline rule, inset from the dark-gold edge. */
.stat-chip::before, .ctl-cluster::before, .bm-card::before, #tower-panel::before, #toast::before,
#boss-bar::before {
  content: ''; position: absolute; inset: 3px; pointer-events: none;
  border: 1px solid rgba(255, 207, 77, .3); border-radius: 2px;
}
/* Per-tower tint (medallions, build cards) — palette accents from ART_DIRECTION. */
[data-type="archer"] { --tint: #e8603e; }
[data-type="cannon"] { --tint: #ff9a3d; }
[data-type="arcane"] { --tint: #b45cff; }
[data-type="frost"]  { --tint: #7fd4ff; }
html, body {
  width: 100%; height: 100%; overflow: hidden; background: #0b1524;
  font-family: var(--display-font); font-variant-numeric: lining-nums tabular-nums;
  -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
}
#app { position: fixed; inset: 0; }
#scene { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.hidden { display: none !important; }
.svg-defs { position: absolute; width: 0; height: 0; overflow: hidden; }
svg { display: inline-block; }

/* ---------- Top bar ---------- */
#hud { position: absolute; inset: 0; pointer-events: none; }
#hud button { pointer-events: auto; }
#topbar {
  position: absolute; top: 16px; left: 16px; right: 16px;
  display: flex; align-items: flex-start; gap: 8px;
}
.hud-left { display: flex; flex-direction: column; align-items: flex-start; }
.chips-row { display: flex; gap: 8px; }
.spacer { flex: 1; }
.stat-chip {
  position: relative; display: flex; align-items: center; gap: 9px;
  height: 48px;
  background: var(--plate-lines), var(--plate);
  border: 1px solid var(--gold-edge);
  border-radius: 4px; padding: 0 20px 0 13px;
  color: var(--parchment); font-weight: 700; font-size: 17px;
  letter-spacing: .02em; font-variant-numeric: tabular-nums;
  box-shadow: var(--plate-shadow), var(--plate-ring);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .7);
}
.stat-chip .ic { width: 26px; height: 26px; filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .55)); }
/* r25: the wave chip names its own number ("Wave 2 / 15") so it and the
   toolbar's "Next * Wave 3" read as a sequence, not a contradiction (a drift
   scorer read them as one). The word is the toolbar's small-caps label tier,
   dim and subordinate, so the chip keeps its numeral as the loud thing. */
.stat-chip .chip-l {
  font-family: var(--display-font); font-variant-caps: small-caps; font-style: normal;
  font-weight: 700; font-size: 14px; letter-spacing: .09em;
  color: var(--parchment-dim); margin-right: 7px;
}

/* Wave timeline — one diamond per wave, gilded as waves fall. r22: it lives
   IN the bottom molding (#bottom-rail .rail-tl re-parents #wave-pips at
   construction), so it is sized for the 16 px rail: 10 px squares rotated
   (14.1 px point to point, 1 px inside the rail top and bottom), 10 px
   layout gap (~6 px tip to tip), a dark ring so each diamond separates from
   the bevel highlight behind it. Unlit pips are empty SOCKETS in the walnut;
   a cleared wave sets a gilded gem; the current wave's gem is lit and 12 %
   larger (scale, not size — the row's layout never shifts); boss waves are
   the red-bordered ones. */
/* r31 item 4 — THE RAIL BECOMES A TRACK, and it is the only thing that can
   move the future pips. obs 3 #8: an unplayed socket (7,5,3) on the bar's body
   (45,36,21) reads 1.33:1. SOLVED, not guessed: the bar body's relative
   luminance is 0.0180, so against it the DARKEST possible pip — pure black —
   tops out at (0.0180+0.05)/0.05 = 1.36:1, and the shipped socket at 1.318 is
   already 97 % of that physical ceiling. Going the other way, 1.6:1 needs a
   pip at relative luminance 0.0588, which IS the cleared gem's own 0.0598 —
   so "≥ 1.6:1 on the plate" and "≤ 0.6 of cleared" have an empty window on the
   pip, exactly as r30's boss-pip pair did. The only free variable left is the
   PLATE, and raising it is also the right drawing: a wave timeline wants to be
   a recessed TRACK with its own floor, which is what makes a socket read as a
   socket. The floor is ENM's r30 enemy-bar track #4a3a24 — one track colour in
   the game — under the molding's own groove-and-lip bevel. Layout is
   untouched: same 16 px row, same 10 px squares, same 10 px gaps. */
#wave-pips {
  display: flex; align-items: center; gap: 10px; height: 16px; padding: 0 10px;
  background: linear-gradient(180deg, #5a4529, #48371f);
  border-radius: 8px;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, .45), inset 0 -1px 0 rgba(255, 236, 190, .12), 0 1px 0 rgba(255, 236, 190, .07);
}
/* r30 item 3 — FIVE STATES IN HUE ONLY. obs 3 #6, measured at 1:1: final-boss
   red (159,52,50) against a plain upcoming socket (105,84,34) is CR 1.04 —
   luminance-identical; current (252,232,174) against cleared (240,204,120) is
   1.27; cleared-boss orange against cleared gold 1.435. "A deuteranope gets
   zero signal about where the last boss is." The MODEL is right (obs 3's own
   BEST #6: five true states in 300 px) — the ENCODING is the defect. So the
   ladder is rebuilt in LUMINANCE, with the boss waves carrying a different
   SILHOUETTE as well as a different hue:
     upcoming  a dark socket, its hairline dimmed .50 -> .30 so the socket's
               own MEAN drops instead of being set by its gold rim
     cleared   a deeper bronze gem (was a bright #ffe08a gold) — it moves DOWN
               the ladder so the current pip has somewhere to be brightest
     current   the cream gem, unchanged, still 12 % larger with its own glow
     boss      a five-point STAR, 1.3x, drawn by clip-path with the dark ring
               moved from `border`/`box-shadow` (both of which clip-path cuts)
               to a drop-shadow, so a boss wave is legible as a boss wave in
               greyscale at 12 px
     boss.done a BRIGHT ember, one rung ABOVE the cleared gold gem. Solved,
               not guessed: with cleared at relative luminance 0.0598 and the
               unplayed boss socket at 0.0158, a cleared-boss darker than the
               gold gem would have to sit at <= 0.0232 to clear 1.5:1 against
               it and at the same time >= 0.0487 to clear 1.5:1 against the
               socket — an EMPTY window. The bright side is the only one that
               exists, and it is also the right semantics: a boss wave you
               broke outranks a wave you broke. */
.pip {
  width: 10px; height: 10px; flex: none; transform: rotate(45deg); border-radius: 1.5px;
  border: 1px solid rgba(255, 207, 77, .30);
  /* r31 item 4: the socket goes to near-black. With the track under it now at
     relative luminance 0.031, the socket is the OTHER half of the 1.6:1 — and
     dropping it also lifts the one pair r30 could not close (upcoming vs
     upcoming-boss, 1.317). */
  background: linear-gradient(135deg, #150e07, #040201);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .72), inset 0 1px 1px rgba(0, 0, 0, .8);
}
.pip.boss {
  transform: scale(1.45); border: 0; border-radius: 0; box-shadow: none;
  /* a FAT five-point star (inner radius 0.62 of the outer, not the 0.38 of a
     thin star): it has to hold ~45 % of its own box at 13 px or the shape is
     legible and its VALUE is not — a thin star's box mean is dragged to the
     plate's by the plate showing through it. */
  clip-path: polygon(50% 0%, 68.2% 24.9%, 97.6% 34.5%, 79.5% 59.6%, 79.4% 90.5%, 50% 81%, 20.6% 90.5%, 20.5% 59.6%, 2.4% 34.5%, 31.8% 24.9%);
  /* r31 item 4: +12 % of value, which is what takes the upcoming-boss vs
     upcoming pair over 1.5:1 now that the socket is near-black. Its own pair
     against the cleared boss has 1.1 of headroom to pay it with. */
  background: linear-gradient(135deg, #c85a48, #5a241c);
  filter: drop-shadow(0 0 1.1px rgba(0, 0, 0, .95));
}
.pip.done {
  background: linear-gradient(135deg, #d8a64c, #8a5f18);
  border-color: var(--gold-edge);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .72), inset 0 0 2px rgba(255, 255, 255, .35);
}
.pip.boss.done {
  background: linear-gradient(135deg, #ffe2b4, #ff8e33);
  filter: drop-shadow(0 0 1.1px rgba(0, 0, 0, .95));
}
.pip.now {
  transform: rotate(45deg) scale(1.12);
  border-color: #fff1c4;
  background: radial-gradient(circle at 50% 40%, #fffbe9, #ffe6a4 55%, #f0c258);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .72), 0 0 8px 1px rgba(255, 207, 77, .7);
}
.pip.boss.now {
  transform: scale(1.58);
  background: radial-gradient(circle at 50% 40%, #fff4e6, #ffb684 55%, #e8703a);
  filter: drop-shadow(0 0 1.1px rgba(0, 0, 0, .95)) drop-shadow(0 0 5px rgba(255, 128, 80, .75));
}
/* The pending wave during a countdown: a LIT SOCKET (full-strength hairline,
   a faint glow), not a gem — the gem is set when the wave marches. */
.pip.next { border-color: rgba(255, 224, 138, .95); box-shadow: 0 0 0 1px rgba(0, 0, 0, .72), 0 0 6px rgba(255, 207, 77, .5); }
.pip.boss.next {
  background: linear-gradient(135deg, #c05a3c, #5e1d12);
  filter: drop-shadow(0 0 1.1px rgba(0, 0, 0, .95)) drop-shadow(0 0 4px rgba(255, 120, 70, .55));
}

/* Speed / pause / mute: one framed plate, cells split by gold hairlines */
.ctl-cluster {
  /* r25 — THE ONLY PLATE THAT EVER SITS ON THE WORLD'S SKYLINE. critic-B:
     "the play/pause cluster sits on the turret tops". MEASURED (hud-on minus
     hud=0 in the cluster's own columns x1760-1904, |dL| >= 1): the shared
     --plate-shadow's tail reaches 28 px past the plate's bottom edge and
     lands on 136 world px in early (the keep's spire tip at y83, 19 px below
     the plate) and 1056 in build, where the close camera puts the keep's
     turrets directly behind the corner (world in 144/144 columns, top y1).
     Boss is clear (the spire is 31 px down; 0 px on world) — the r24 reading
     of "7 px" was this soft tail on a gradient, not the plate.
     The token is not retuned for everyone: the VALUE is overridden inside
     this one corner, so every other plate keeps the shared shadow and this
     one stops 5 px past its own edge instead of smearing down the stonework.
     AFTER, same instrument, same-session hud/hud=0 pairs: tail depth 28 -> 5
     px; tail px landing on world beyond 4 px past the plate 109 -> 0 (early)
     and 888 -> 0 (build). What is left is the 4-5 px seat directly under the
     plate's own edge, which is what a shadow is for; on build the keep is
     DIRECTLY behind the corner (world at y1 in every column), so "0 px over
     the silhouette" and "a plate that is not pasted flat" cannot both hold —
     the shadow no longer reaches the spire (early: the smear ran to y91 and
     the spire's tip is y83; it now stops at y69).
     REFUTED, with numbers: "move the chip 24 px left" — in the moved band
     (x1736-1880) build still has world in 144/144 columns at y1-48 and early
     still bottoms out at y83; the move buys nothing and costs the 16 px
     corner inset the chips are aligned to.

     r26 — THE PLATE STOPS TOUCHING THE SKYLINE, AND THE 24 px TARGET IS
     REFUTED FOR THE OTHER TWO FRAMES. Three blind observers: "the chip sits
     2 px above the castle in early, build and upgrade". MEASURED (obs 1's own
     classifier — sky B-R > 18, cloud near-neutral and L > 140 — first world
     row below the plate, per column of the plate's rect): early 0, build 0,
     upgrade 0, mid 69, boss 30 px. An EXHAUSTIVE SEAT SWEEP (every left edge
     from x0 to x1920, widths 144/128/112/96/60/44, bottoms y33-y65, all five
     HUD frames) says the brief's "clears by >= 24 px in all five" is not
     reachable on the frozen camera: the best 144 px seat anywhere right of
     x1200 clears by 10 px, and the best in the WHOLE frame is 46 px at
     x0-143 — under the gold chip. On the `close` camera the keep fills
     x1692-1919 from y2 down.
     What IS reachable, and what the eye actually catches, is the TANGENCY.
     Classify each column of the plate's rect: world within 24 px BELOW and
     sky BEHIND = a tangency (a silhouette balanced on the plate's edge);
     world below AND behind = the plate simply overlaps the keep. Before:
     early 29 tangent columns (x1837-1904 — the turret crenellations at y83
     and the gold finial ball at y65-72, two px under the plate's own gold
     corner rivet, and NOTHING behind the plate: 5 world px in the whole
     rect); build/upgrade 5 tangent and 108 OVERLAP columns with 2 067 world
     px behind the plate — the keep runs up past the plate, so there is no
     silhouette balanced on anything.
     The fix is margin and height, no re-anchor. PART 1 took the plate's
     bottom 64 -> 56 (the crenellation band at y83 then 26 px clear) and its
     right edge 1904 -> 1892, which takes the finial's 8 columns out of the
     rect. PART 2 took another 4 px of height (padding 4 -> 2, so the 44x30
     BUTTONS and the 21 px icon are untouched) and 6 px more margin: bottom
     56 -> 52, right edge 1892 -> 1888, live DOM rect (1748,16)-(1885,52).
     The top inset stays 16 px throughout, so the plate still hangs from the
     same line as the three chips; the only thing spent is the plate's own
     inner breathing room, 4 px -> 2 px above and below the buttons.
     AFTER — all three states priced on the SAME current skylines with CAM's
     `?hudclear=1` (a far-plane render mask, not a colour classifier), min
     gap and tangent columns, r25 -> part 1 -> FINAL:
       early     1 px / 52t   ->  18 px / 14t  ->  25 px / 0t
       mid      71 px         ->  69 px        ->  71 px
       boss     31 px         ->  39 px        ->  43 px
       upgrade   0 / 142 ovl  ->   0 / 142 ovl ->   0 / 138 ovl (92.4% behind)
       build     0 / 142 ovl  ->   0 / 142 ovl ->   0 / 138 ovl (92.4% behind)
     Every frame with sky behind the plate now clears the skyline by >= 25 px
     and NO column of the rect has world within 24 px below it with sky
     behind. upgrade/build stay 0 because the keep's columns run to the top
     of the frame: the 2-D sweep says they are still 0 at a bottom of y28 and
     reach 1 px only at y22 — a third instrument agreeing the brief's 24 px
     is unreachable at this corner on the frozen camera.
     REFUTED, priced on the same skylines: more margin at y52 — mr20/24/28
     buy 1/3/5 px more in early and cost 2/5/10 px in mid, for a right inset
     of 37-44 px against the left chips' 16. mr18's 35 px is the knee.

     r27 — THE HEIGHT COMES BACK, THE INSET STAYS. r26 bought its sky by
     SHRINKING the plate (48 -> 40 -> 36 px) as well as insetting it, and four
     of the six drift scorers docked exactly that: "under-sized / shrunken,
     misaligned against the resource pills" (r26 drift, ui 7.00 -> 6.67, the
     round 6-0 against). The two levers were never the same thing. MEASURED
     with CAM's ?hudclear=1, priced on the same skylines: the 18 px MARGIN is
     what takes early's finial columns out of the rect (mr0 -> mr18 at any
     height: 26 -> 0 tangent columns), and the HEIGHT only buys ~1 px of gap
     per px of height. So the height goes back to the pills' exact box and the
     margin stays.
     The pills' box, read live from the DOM on the shipped tree:
     .stat-chip x16-117.3 / 125.3-216.2 / 224.2-397.3, all y16-64, HEIGHT 48.
     The plate is now the same 16..64 band: 48 px tall, 44x36 buttons, 5 px of
     padding (48 - 2 border - 2x5 = 36 = the button, exactly), right edge
     1886, live rect (1742,16)-(1886,64). Every horizontal rule of the top
     strip — top inset, bottom edge, cell height — is now shared with the
     three chips it faces across the frame. */
  --plate-shadow: 0 1px 3px rgba(0, 0, 0, .62), 0 0 0 1px rgba(0, 0, 0, .55);
  position: relative; display: flex; height: 48px; padding: 5px; margin-right: 18px;
  background: var(--plate-lines), var(--plate);
  border: 1px solid var(--gold-edge); border-radius: 4px;
  box-shadow: var(--plate-shadow), var(--plate-ring);
}
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  /* r26 took this 36 -> 30 px so the plate could be 36 px and clear the
     skyline; r27 puts it back to 36 with the plate (see .ctl-cluster — the
     sky was bought by the 18 px margin, not by the shrink, and the shrink is
     what the drift judges docked). The 44 px WIDTH — the pointer target that
     matters on a three-cell row — never changed, nor did the 21 px icon. */
  position: relative;
  width: 44px; height: 36px; border-radius: 2px;
  background: transparent; border: 0;
  border-left: 1px solid rgba(255, 207, 77, .22); color: var(--gold-soft);
  cursor: pointer;
  transition: background .12s ease, filter .12s ease;
}
.icon-btn:first-child { border-left: 0; }
.icon-btn svg { width: 21px; height: 21px; color: var(--gold-soft); filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .6)); }
.icon-btn:hover { background: rgba(255, 207, 77, .1); filter: brightness(1.2); }
.icon-btn:active { filter: brightness(.85); }
.icon-btn.off svg { opacity: .42; }
.icon-btn.on { background: rgba(255, 207, 77, .16); }
.icon-btn.on svg { color: var(--gold); filter: drop-shadow(0 0 5px rgba(255, 207, 77, .65)); }
/* r27 — THE STATE LAMP. All three r26 blind observers: "the three transport
   buttons have NO on/off/active state (identical glyph and cell colours)".
   They are right about the SHIPPED FRAMES and the existing classes cannot fix
   it: `.on` swaps the glyph from --gold-soft (#ffd98a, L 219) to --gold
   (#ffcf4d, L 208) — ELEVEN L of difference — and in every captured frame the
   speed is 1x, the game is running and the sound is on, so no class is set on
   any of the three and there is nothing to read at all.
   A lamp reads state in EVERY frame instead of only after a click: a 14x3 px
   socket under each glyph, dark walnut when the mode is off, gilded when it is
   engaged. The semantic is "what is true right now" — fast-forward engaged,
   the clock running, the sound on — so the shipped HUD prints one dark lamp
   (1x) and two lit (running, audible), which is a reading, not a decoration.
   MEASURED at 1:1: lit vs socket, see docs/fixlog-r27-UI.md §2. */
.icon-btn::after {
  content: ''; position: absolute; left: 50%; bottom: 4px; transform: translateX(-50%);
  width: 14px; height: 3px; border-radius: 2px;
  background: #35270f;
  box-shadow: inset 0 1px 0 rgba(0, 0, 0, .75), 0 1px 0 rgba(255, 236, 190, .07);
}
.icon-btn.lamp-on::after {
  background: linear-gradient(180deg, #ffe9ac, var(--gold));
  box-shadow: 0 0 5px rgba(255, 207, 77, .7), inset 0 1px 0 rgba(255, 255, 255, .5);
}

/* ---------- Wave call ---------- */
/* r23: the bottom toolbar is 52px; the call sits 24px above its groove (was
   96px over a 16px rail with the clusters standing between). */
#wave-cta { position: absolute; bottom: 76px; left: 50%; transform: translateX(-50%); text-align: center; }
#btn-callwave {
  display: inline-flex; align-items: center; gap: 9px;
  font-family: var(--display-font); font-variant-caps: small-caps; font-weight: 700;
  font-size: 20px; letter-spacing: .07em;
  color: #2a1a05; padding: 10px 26px; border-radius: 4px; cursor: pointer;
  background: var(--gold-face);
  border: 1px solid #6a4410;
  box-shadow:
    0 0 0 1px rgba(255, 224, 138, .5), 0 0 0 3px #1a120a, 0 0 0 4px rgba(216, 178, 90, .55),
    0 8px 20px rgba(0, 0, 0, .55),
    inset 0 2px 0 rgba(255, 255, 255, .6), inset 0 -3px 0 rgba(122, 76, 14, .45), inset 0 0 0 1px rgba(255, 240, 200, .28);
  text-shadow: 0 1px 0 rgba(255, 255, 255, .45);
  transition: transform .09s ease, filter .12s ease;
}
#btn-callwave .bic { width: 18px; height: 18px; color: #4a2f08; filter: drop-shadow(0 1px 0 rgba(255, 255, 255, .3)); }
#btn-callwave:hover { filter: brightness(1.07); transform: translateY(-1px) scale(1.02); }
#btn-callwave:active { transform: translateY(1px); }
#callwave-bonus { font-family: var(--display-font); font-variant-caps: normal; font-weight: 700; font-size: 13px; letter-spacing: .02em; opacity: .85; }
#wave-timer { margin-top: 8px; color: var(--parchment); font-weight: 600; font-size: 13px; letter-spacing: .04em; text-shadow: 0 1px 3px rgba(0, 0, 0, .8); }

/* ---------- Wave banner ---------- */
/* r30 item 2 retired this element with a `display: none !important` line here
   (the retirement had to be enforced from CSS because src/qa.js — CAM's file —
   force-shows it on the `mid` and `boss` scenarios via `sc.banner`).
   r30 part 2 DELETED that line: six blind drift scorers compared r30's frames
   against r29's and all six took a point off ui_hud naming, among other
   things, "the mid-wave title banner". The banner ships again, and because the
   one line is gone the harness staging works again too. */
#wave-banner {
  position: absolute; top: 88px; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 16px;
  padding: 12px 32px;
  background:
    repeating-linear-gradient(0deg, rgba(255, 240, 200, .025) 0 1px, transparent 1px 3px),
    linear-gradient(90deg, transparent, rgba(24, 17, 10, .9) 11%, rgba(24, 17, 10, .9) 89%, transparent);
  border-top: 1px solid transparent; border-bottom: 1px solid transparent;
  border-image: linear-gradient(90deg, transparent, var(--gold-line) 18%, var(--gold-line) 82%, transparent) 1;
  color: var(--parchment); font-family: var(--display-font);
  font-size: 26px; letter-spacing: .12em; text-align: center; white-space: nowrap;
  text-shadow: 0 -1px 0 rgba(0, 0, 0, .85), 0 1px 0 rgba(255, 236, 190, .1), 0 2px 8px rgba(0, 0, 0, .6);
  animation: bannerIn 3.2s ease forwards;
  pointer-events: none;
}
#wave-banner::before, #wave-banner::after {
  content: ''; position: absolute; left: 7%; right: 7%; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255, 207, 77, .38), transparent);
}
#wave-banner::before { top: 3px; }
#wave-banner::after { bottom: 3px; }
#wave-banner .wb-fl { width: 68px; height: 23px; color: var(--gold-line); opacity: .92; flex: none; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .6)); }
#wave-banner .wb-fl.r { transform: scaleX(-1); }
/* r30 item 2 — the wave-start flash on the chip that already carries the wave
   number, replacing the centre banner. Two beats of the chip's own gold, no
   layout change (box-shadow + border-colour only), 900 ms. Frozen under
   .qa-freeze with every other decorative animation. */
#chip-wave.flash {
  animation: chipFlash .9s cubic-bezier(.34, 1.1, .64, 1) 1;
}
@keyframes chipFlash {
  0%   { box-shadow: 0 0 0 0 rgba(255, 207, 77, .0), inset 0 1px 0 rgba(255, 236, 190, .10); }
  18%  { box-shadow: 0 0 18px 2px rgba(255, 207, 77, .55), inset 0 1px 0 rgba(255, 236, 190, .42); }
  100% { box-shadow: 0 0 0 0 rgba(255, 207, 77, .0), inset 0 1px 0 rgba(255, 236, 190, .10); }
}
@keyframes bannerIn {
  0% { opacity: 0; transform: translateX(-50%) translateY(-14px); }
  12% { opacity: 1; transform: translateX(-50%) translateY(0); }
  80% { opacity: 1; }
  100% { opacity: 0; }
}

/* ---------- Build menu (radial) ---------- */
#build-menu { position: absolute; pointer-events: none; }
/* r29 UI §2 — THE RING GROWS WITH R. obs 3 / r27 critic-C: the 206x225px
   card block hid 92% of the road under it — MEASURED (fixlog-r28-UI.md §4):
   at the old R=86 the cards' own inner corners sat 19px from the pad's
   centre, deep inside the pad's ~95px on-screen radius, so the occlusion was
   geometric, not a sizing accident. src/ui.js's openBuildMenu() moved R to
   150 (r28 §4's own proposal); this box scales with it, keeping the exact
   same ratio it always had — the solid ring (inset 40px) sits at radius
   (halfSize-40), which was engineered to land exactly ON the card centres
   at the old R (126-40=86); halfSize is now 190 so 190-40=150=R again, and
   the dashed outer ring (inset 22px, unchanged) lands at 190-22=168=R+18,
   the same R+18 relationship the old ring had (104=86+18). Only the box's
   own size changes; both insets are untouched. */
.bm-ring {
  position: absolute; width: 380px; height: 380px; margin-left: -190px; margin-top: -190px;
  border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle, rgba(14, 10, 6, 0) 0 84px, rgba(14, 10, 6, .42) 118px 154px, rgba(14, 10, 6, 0) 190px);
  animation: bmRing .22s ease both;
}
.bm-ring::before {
  content: ''; position: absolute; inset: 40px; border-radius: 50%;
  border: 1px solid rgba(255, 207, 77, .5);
  box-shadow: 0 0 14px rgba(255, 207, 77, .16), inset 0 0 14px rgba(255, 207, 77, .1);
}
.bm-ring::after {
  content: ''; position: absolute; inset: 22px; border-radius: 50%;
  border: 1px dashed rgba(255, 207, 77, .22);
}
@keyframes bmRing { from { opacity: 0; } to { opacity: 1; } }
/* 84x104 -> 82x102: the brief's own floor, trimmed the minimum amount so the
   move to R=150 buys clearance without the cards' content (icon/name/cost)
   needing to be re-laid-out. */
.bm-item {
  /* r31 item 2: 102 -> 110px of height for the two stat rows. The WIDTH stays
     at 82 (the brief's cap is 90x110 and a two-glyph stat row needs 34 px, so
     nothing is bought by widening) — which also keeps the menu's on-screen
     footprint and the pair clearance where r31 item 1 solved them.
     r31 item 1/2 PART 2 — 110 -> 116px. The pair observer: "on all four cards
     the range row is STRUCK THROUGH by the card's own bottom border and corner
     studs, 22 % of the digits on bare ground" and "the icon discs are squashed
     42x42 -> 43x27, which reads as a render error". ONE cause: the stack the
     stats joined is 25px taller than the 108px content box, so the flex line
     overflowed 5px past each end AND the only shrinkable item — the medallion —
     absorbed 15px of it. The card is given the height the content actually
     needs (MEASURED, not guessed) and the medallion is made unshrinkable.
     116 and not 120 (the cap) because every px of height costs road: the ring
     pair's own road population loses 0.11 points per px, and 116 keeps the
     worst-case card-to-card clearance over EVERY possible road bearing at
     12.1px — two digits, item 1's own bar (h 120 leaves 9.9). */
  position: absolute; width: 82px; height: 116px; margin-left: -41px; margin-top: -58px;
  cursor: pointer; pointer-events: auto;
}
.bm-card {
  position: relative; width: 100%; height: 100%; border-radius: 5px;
  /* r31 part 2: the stack is laid out against a declared content box instead of
     being centred and left to overflow. 3px of head room puts the medallion's
     rim on the ::before hairline (inset 3px) exactly where r30's 42px disc sat;
     7px of foot room puts the last stat row's box 3px clear of that hairline's
     bottom line and its DIGIT INK 5px clear (9px clear of the outer border,
     15.4-18.5px from the nearest ::after corner stud, which live at x<=7 and
     x>=75 while the digits span x 21-61) — all four cards MEASURED at 1:1. */
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  padding: 3px 0 7px; box-sizing: border-box;
  background: var(--plate-lines), var(--plate);
  border: 1px solid var(--gold-edge);
  box-shadow: 0 8px 22px rgba(0, 0, 0, .55), 0 0 0 1px rgba(0, 0, 0, .55), var(--plate-ring), inset 0 -12px 16px rgba(0, 0, 0, .28);
  animation: bmPop .18s cubic-bezier(.34, 1.56, .64, 1) both;
  transition: transform .1s ease, box-shadow .12s ease, border-color .12s ease;
}
.bm-item:hover .bm-card {
  transform: translateY(-3px) scale(1.06);
  border-color: var(--gold-line);
  box-shadow: 0 12px 26px rgba(0, 0, 0, .6), 0 0 18px rgba(255, 207, 77, .35), 0 0 0 1px rgba(0, 0, 0, .55), var(--plate-ring);
}
/* Portrait medallion: gold-ringed disc tinted per tower, same device as the tower panel */
.bm-icon {
  /* r31 part 2: `flex: none` + `aspect-ratio` are the fix for the squash — a
     flex item with a height and no flex-basis rule is shrinkable, and it was
     the ONLY shrinkable thing in the card, so it ate the whole overflow and
     rendered 42x27. It is now a circle by construction at any content height.
     38px (>= the 36px floor) is what the re-budgeted stack affords; the glyph's
     22x21 ink keeps 6.5-7px of rim clearance inside it. */
  width: 38px; height: 38px; flex: none; aspect-ratio: 1 / 1;
  border-radius: 50%; margin-bottom: 1px;
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(circle at 50% 36%, color-mix(in srgb, var(--tint) 58%, #1a120a), color-mix(in srgb, var(--tint) 22%, #120c07) 62%, #0e0905);
  border: 1.5px solid var(--gold-line);
  box-shadow: 0 0 0 1px #2a1c0b, 0 0 0 2px rgba(216, 178, 90, .38), 0 3px 6px rgba(0, 0, 0, .6), inset 0 0 8px rgba(0, 0, 0, .6), inset 0 1px 0 rgba(255, 236, 190, .18);
}
.bm-icon svg { width: 25px; height: 25px; color: var(--parchment); filter: drop-shadow(0 0 5px var(--tint)) drop-shadow(0 1px 1px rgba(0, 0, 0, .7)); }
.bm-item:hover .bm-icon svg { color: #fff8e6; }
.bm-name {
  font-family: var(--display-font); font-variant-caps: small-caps; font-weight: 700;
  color: var(--parchment); font-size: 14px; letter-spacing: .06em;
  /* r31 part 2: the FACE is untouched (14px, cap 10px — the pair's own number);
     only the half-leading the line box was adding is trimmed, which is where
     the height for the medallion comes from. Same for .bm-cost span below. */
  line-height: 13px;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .7);
}
.bm-cost { display: flex; align-items: center; gap: 4px; }
.bm-cost svg { width: 14px; height: 14px; }
.bm-cost span { color: var(--gold); font-size: 17px; line-height: 16px; font-weight: 800; font-variant-numeric: lining-nums tabular-nums; }
/* r31 item 2 — the two decisive numbers, under a gold rule so the card reads
   price-then-spec and not one list. The rule is the tower panel's own
   `.tp-stats` divider token; the rows are the panel's icon + value grammar at
   the build menu's scale (13px value, 11px glyph), tabular so the four cards'
   numbers line up when the eye crosses the ring. */
.bm-stats {
  margin-top: 1px; padding-top: 3px; width: 56px;
  border-top: 1px solid rgba(255, 207, 77, .26);
  display: flex; flex-direction: column; align-items: center; gap: 1px;
}
/* 14 -> 12px rows + a 1px gap: a 13px pitch on two 13px tabular numerals. The
   px came out of the leading, not the face — the digits are the same 9px cap
   the pair measured at 12.1:1. */
.bm-st { display: flex; align-items: center; justify-content: center; gap: 4px; height: 12px; }
.bm-st svg { width: 11px; height: 11px; flex: none; color: var(--gold-line); filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .6)); }
.bm-st span {
  color: var(--parchment); font-family: var(--display-font); font-weight: 700;
  font-size: 13px; line-height: 12px; font-variant-numeric: lining-nums tabular-nums;
  min-width: 22px; text-align: left; text-shadow: 0 1px 2px rgba(0, 0, 0, .8);
}
.bm-item.cant { cursor: not-allowed; }
.bm-item.cant .bm-card { filter: saturate(.25) brightness(.62); }
.bm-item.cant .bm-cost span { color: #ff6b5e; }
.bm-item.cant:hover .bm-card { transform: none; border-color: var(--gold-edge); box-shadow: 0 8px 22px rgba(0, 0, 0, .55), 0 0 0 1px rgba(0, 0, 0, .55), var(--plate-ring); }
@keyframes bmPop { from { opacity: 0; transform: scale(.5); } to { opacity: 1; transform: scale(1); } }

/* ---------- Tower panel ---------- */
#tower-panel {
  /* r19: the panel rises above the bottom dock instead of sharing an edge
     with it. r23: the armoury is INSIDE the 52px toolbar now, so the panel
     docks 16px (one grid step) above the toolbar's groove — it used to
     clear the free-standing armoury plate by 19px at bottom: 88px. */
  position: absolute; left: 16px; bottom: 68px; width: 304px;
  background: var(--plate-lines), var(--plate);
  border: 1px solid var(--gold-edge); border-radius: 6px; padding: 18px 18px 16px;
  color: var(--parchment);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .55), 0 0 0 1px rgba(0, 0, 0, .55), var(--plate-ring), inset 0 0 40px rgba(0, 0, 0, .3);
  pointer-events: auto;
}
#tower-panel::before { inset: 5px; border-radius: 3px; }
/* Same corner ornaments as the title card, scaled for the panel */
#tower-panel .corner { width: 22px; height: 22px; color: rgba(255, 207, 77, .75); }
#tower-panel .corner.tl { top: 4px; left: 4px; }
#tower-panel .corner.tr { top: 4px; right: 4px; }
#tower-panel .corner.bl { bottom: 4px; left: 4px; }
#tower-panel .corner.br { bottom: 4px; right: 4px; }
.tp-head {
  display: flex; align-items: center; gap: 13px;
  padding-bottom: 11px; margin-bottom: 6px;
  border-bottom: 1px solid transparent;
  border-image: linear-gradient(90deg, rgba(255, 207, 77, .6), rgba(255, 207, 77, .08)) 1;
}
/* Portrait medallion: the tower glyph inside a gold ring on a per-tower tinted disc */
.tp-medal {
  position: relative; flex: none; width: 54px; height: 54px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(circle at 50% 36%, color-mix(in srgb, var(--tint) 58%, #1a120a), color-mix(in srgb, var(--tint) 22%, #120c07) 62%, #0e0905);
  border: 2px solid var(--gold-line);
  box-shadow:
    0 0 0 1px #2a1c0b, 0 0 0 3px rgba(20, 14, 8, .95), 0 0 0 4px rgba(216, 178, 90, .5),
    0 4px 10px rgba(0, 0, 0, .6), inset 0 0 12px rgba(0, 0, 0, .65), inset 0 1px 0 rgba(255, 236, 190, .18);
}
.tp-medal svg { width: 30px; height: 30px; color: var(--parchment); filter: drop-shadow(0 0 6px var(--tint)) drop-shadow(0 2px 2px rgba(0, 0, 0, .7)); }
.tp-title { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
#tp-name {
  display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-family: var(--display-font); font-variant-caps: small-caps; font-weight: 700;
  font-size: 19px; letter-spacing: .02em;
  text-shadow: 0 -1px 0 rgba(0, 0, 0, .85), 0 1px 3px rgba(0, 0, 0, .7);
}
#tp-tier { display: flex; gap: 4px; }
#tp-tier svg { width: 16px; height: 16px; color: #57431f; filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .6)); }
#tp-tier svg.on { color: var(--gold); filter: drop-shadow(0 0 3px rgba(255, 207, 77, .55)); }
.tp-stats { margin-bottom: 12px; }
.tp-row {
  display: flex; align-items: center; gap: 10px; padding: 5px 0; font-size: 14.5px;
  border-bottom: 1px solid rgba(255, 207, 77, .1);
}
.tp-row svg { width: 18px; height: 18px; color: var(--gold-line); flex: none; filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .6)); }
.tp-row .lbl {
  font-family: var(--display-font); font-variant-caps: small-caps; font-size: 16px;
  letter-spacing: .04em; color: var(--parchment-dim);
}
.tp-row .val { margin-left: auto; font-weight: 800; color: var(--gold-soft); font-variant-numeric: tabular-nums; text-shadow: 0 1px 2px rgba(0, 0, 0, .6); }
.tp-foot {
  display: flex; align-items: center; gap: 7px; margin-top: 8px;
  font-size: 17.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: #b3a486;
}
/* r28 §2: the footer's wing icon was #8fb9d8 (hue ~205 deg) against the four
   stat-row icons above it at var(--gold-line) (hue ~42 deg) — obs 3 measured
   "GROUND & AIR hue 203 deg … 161 deg off". Same colour as the row icons now:
   one hue for the whole card, only the footer's SHAPE still sets it apart. */
.tp-foot svg { width: 14px; height: 14px; color: var(--gold-line); }
/* Chunky bevelled buttons: metallic face on a dark base ring with gold trim */
/* r31 item 4 — the MAX TIER badge, on the row that already encodes tier. Small,
   subordinate to the three stars beside it, and a STATE token (a flat plate,
   no bevel, no gold face) so it can never be mistaken for a control. */
.tp-max {
  font-family: var(--display-font); font-variant-caps: small-caps; font-style: normal;
  font-weight: 700; font-size: 13px; line-height: 16px; letter-spacing: .06em;
  margin-left: 6px; padding: 0 7px; border-radius: 3px;
  color: var(--gold-soft); background: rgba(255, 207, 77, .13);
  box-shadow: inset 0 0 0 1px rgba(255, 207, 77, .34);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .8);
}
.tp-actions { display: flex; gap: 10px; }
/* r31 item 4: obs 3 #11 measured both action buttons at 41 px, "under the 44 px
   target floor". 8 -> 10px of vertical padding puts every action control over
   it; the panel grows 6 px in a corner with 69 px of clear screen below it. */
.tp-actions button {
  position: relative; padding: 10px 4px; min-height: 44px; border-radius: 4px; cursor: pointer; white-space: nowrap;
  font-family: var(--display-font); font-variant-caps: small-caps; font-weight: 700; letter-spacing: .05em;
  border: 1px solid #6a4410;
  transition: filter .12s ease, transform .09s ease;
}
.tp-actions button:hover { filter: brightness(1.1); transform: translateY(-1px); }
.tp-actions button:active { transform: translateY(1px); }
#tp-upgrade {
  flex: 1.6; display: flex; flex-direction: column; align-items: center; gap: 1px;
  background: var(--gold-face); color: #2a1a05;
  box-shadow:
    0 0 0 1px rgba(255, 224, 138, .45), 0 0 0 2px #1a120a, 0 0 0 3px rgba(216, 178, 90, .5),
    0 4px 10px rgba(0, 0, 0, .5),
    inset 0 2px 0 rgba(255, 255, 255, .6), inset 0 -3px 0 rgba(122, 76, 14, .45), inset 0 0 0 1px rgba(255, 240, 200, .28);
  text-shadow: 0 1px 0 rgba(255, 255, 255, .45);
}
.tpu-main { display: inline-flex; align-items: center; gap: 6px; font-size: 17px; }
.tpu-main svg { width: 15px; height: 15px; filter: drop-shadow(0 1px 0 rgba(255, 255, 255, .3)); }
.tpu-main .cost { font-family: var(--display-font); font-variant-caps: normal; font-weight: 700; font-size: 14px; font-variant-numeric: tabular-nums; }
.tpu-delta { font-family: var(--display-font); font-variant-caps: normal; font-size: 10.5px; font-weight: 700; color: #6b4a09; letter-spacing: .01em; }
/* r27 — A DISABLED CONTROL MUST SIT UNDER A LIVE ONE. All three r26 blind
   observers measured MAX TIER (this state) as BRIGHTER than the live Sell
   beside it: L 66.1 vs 58.5 (obs 1), fill +16 % and border 1.8x (obs 2),
   top-decile 163 vs 138 (obs 3) — the panel was telling the player that the
   button they cannot press is the important one. The face is now darker AND
   desaturated against --iron-face (#4a3d2c -> #221a10, S 0.41): #302b24 ->
   #17140f, S 0.25, mean face L 32 vs iron's 45; the label drops from #d3c39b
   (L 192) to #9a9184 (L 146) against Sell's #cdbfa3; and the two gold rings
   come off the gilt ramp onto a dim pewter so the border stops out-ranking
   the live button's. Numbers on the shipped frame in docs/fixlog-r27-UI.md. */
#tp-upgrade:disabled {
  cursor: default; transform: none;
  background: linear-gradient(180deg, #302b24, #17140f); color: #9a9184;
  border-color: #33291a;
  text-shadow: 0 -1px 0 rgba(0, 0, 0, .7);
  box-shadow:
    0 0 0 1px rgba(150, 132, 96, .2), 0 0 0 2px #150e07, 0 0 0 3px rgba(150, 132, 96, .16),
    inset 0 1px 0 rgba(255, 236, 190, .06), inset 0 -3px 0 rgba(0, 0, 0, .5);
}
#tp-upgrade:disabled:hover { filter: none; transform: none; }
/* r31 item 4: at max tier the upgrade control leaves the row entirely, so the
   row holds exactly ONE control and Sell takes its full width. */
#tp-upgrade.gone { display: none; }
#tp-upgrade.cant { filter: saturate(.35) brightness(.78); }
#tp-upgrade.cant .cost { color: #8f1f1f; }
#tp-sell {
  flex: 1; background: var(--iron-face); color: var(--parchment-dim);
  border-color: #3a2a12; font-size: 15px; letter-spacing: .03em;
  text-shadow: 0 -1px 0 rgba(0, 0, 0, .7);
  box-shadow:
    0 0 0 1px rgba(216, 178, 90, .35), 0 0 0 2px #1a120a, 0 0 0 3px rgba(216, 178, 90, .3),
    0 4px 10px rgba(0, 0, 0, .5),
    inset 0 1px 0 rgba(255, 236, 190, .14), inset 0 -3px 0 rgba(0, 0, 0, .45);
}
#tp-sell .g { font-family: var(--display-font); font-variant-caps: normal; font-weight: 700; font-size: 13.5px; font-variant-numeric: tabular-nums; }

/* ---------- Toast ---------- */
#toast {
  position: absolute; bottom: 112px; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 8px;
  background: var(--plate-lines), var(--plate);
  border: 1px solid var(--gold-edge);
  color: var(--parchment); padding: 9px 22px; border-radius: 4px;
  font-size: 14px; font-weight: 700; letter-spacing: .02em; pointer-events: none;
  box-shadow: var(--plate-shadow), var(--plate-ring);
  animation: toastIn 1.8s cubic-bezier(.22, .9, .3, 1) forwards;
}
#toast .tic { width: 16px; height: 16px; flex: none; filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .5)); }
#toast.gold { border-color: var(--gold-edge); }
#toast.warn {
  border-color: #7a2c30;
  background: var(--plate-lines), linear-gradient(180deg, rgba(58, 16, 18, .96), rgba(30, 9, 11, .96));
  color: #ffd9dd;
}
#toast.warn::before { border-color: rgba(255, 90, 110, .35); }
#toast.warn .tic { color: #ff8a97; }
@keyframes toastIn {
  0% { opacity: 0; transform: translateX(-50%) translateY(26px); }
  14% { opacity: 1; transform: translateX(-50%) translateY(-2px); }
  22% { transform: translateX(-50%) translateY(0); }
  78% { opacity: 1; }
  100% { opacity: 0; transform: translateX(-50%) translateY(0); }
}

/* ---------- Screens ---------- */
/* r27 — THE START AND VICTORY CARDS STOP THROWING THE GAME AWAY.
   All three r26 blind observers, independently: "start/victory dim the world
   to 27 % luminance AND desaturate it (tree sat 0.75 -> 0.40)"; "start
   crushes the world L 144 -> 31.7"; "the world behind the card at L 29.4 vs
   119.4 in play". MEASURED here on the SAME frame and the same camera (one
   capture, then a second with `--pre` removing the scrim and hiding the card
   — not a comparison against `mid`, which is a different pose): world region
   = the frame minus the card's own rect plus a 16 px halo.
     r26   dimmed meanL 27.48  |  control 110.68  ->  DIM 75.2 %,
           mean HSV saturation 0.4589 vs 0.6683  ->  only 68.7 % RETAINED.
   The desaturation was never a filter — it is the scrim's own HUE. A blue
   overlay (rgba(16,26,44) -> rgba(5,9,18)) at 55-92 % pulls every warm
   surface toward its own chromaticity, which is exactly what "the tree's
   saturation falls 0.75 -> 0.40" is. A PURE BLACK scrim cannot do that: in
   sRGB codes it is out = (1-a)*c, a scale, and HSV saturation is
   scale-invariant, so the world keeps its hue and its chroma to the last
   decimal at any alpha.
   So the scrim is black now, and light: a shallow ellipse that is deepest
   directly behind the card (where the card is opaque anyway, and where the
   ring around it wants separation) and 0.30 out at the corners. Measured
   after: see docs/fixlog-r27-UI.md §3. */
.screen {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  background: radial-gradient(ellipse 62% 66% at 50% 46%,
    rgba(0, 0, 0, .54) 0%, rgba(0, 0, 0, .46) 42%, rgba(0, 0, 0, .22) 76%, rgba(0, 0, 0, .18) 100%);
  z-index: 10;
}
.screen-inner { position: relative; text-align: center; padding: 32px; }
.panel-frame {
  position: relative; padding: 48px 80px 40px; border-radius: 8px;
  /* r27: the world behind is 2.6x brighter than it was, so the card carries
     its own separation instead of borrowing the scrim's — the plate goes
     opaque and the drop shadow becomes the 20-24 px soft halo the upgrade
     panel is praised for ("a real 16-25 px soft halo", obs 1). */
  background: linear-gradient(180deg, rgba(34, 25, 15, .97), rgba(13, 9, 5, .985));
  border: 1px solid rgba(255, 207, 77, .45);
  box-shadow:
    0 10px 24px rgba(0, 0, 0, .62), 0 28px 72px rgba(0, 0, 0, .6),
    inset 0 0 0 1px rgba(0, 0, 0, .6),
    inset 0 0 90px rgba(0, 0, 0, .38),
    inset 0 1px 0 rgba(255, 236, 190, .07);
}
.corner { position: absolute; width: 34px; height: 34px; color: rgba(255, 207, 77, .6); }
.corner.tl { top: 7px; left: 7px; }
.corner.tr { top: 7px; right: 7px; transform: scaleX(-1); }
.corner.bl { bottom: 7px; left: 7px; transform: scaleY(-1); }
.corner.br { bottom: 7px; right: 7px; transform: scale(-1, -1); }
.game-title {
  font-family: var(--display-font); color: var(--parchment);
  font-size: clamp(52px, 9vw, 92px); letter-spacing: .13em; padding-left: .13em; font-weight: 700;
  text-shadow: 0 0 34px rgba(255, 207, 77, .28), 0 4px 0 #4a3410, 0 8px 24px rgba(0, 0, 0, .8);
}
.title-rule {
  display: flex; align-items: center; gap: 12px;
  margin: 4px auto 16px; max-width: 420px; color: rgba(255, 207, 77, .8);
}
.title-rule span { flex: 1; height: 1px; background: linear-gradient(90deg, transparent, rgba(255, 207, 77, .55)); }
.title-rule span:last-child { transform: scaleX(-1); }
.title-rule svg { width: 13px; height: 13px; filter: drop-shadow(0 0 4px rgba(255, 207, 77, .5)); }
.game-sub {
  color: var(--parchment-dim); font-size: 16px; letter-spacing: .22em; text-transform: uppercase;
  margin-bottom: 40px; font-weight: 600; text-shadow: 0 2px 6px rgba(0, 0, 0, .8);
}
.big-btn {
  display: inline-flex; align-items: center; gap: 16px;
  font-family: var(--display-font); font-variant-caps: small-caps; font-weight: 700;
  font-size: 27px; letter-spacing: .09em;
  color: #2a1a05; padding: 13px 40px; border-radius: 5px; cursor: pointer;
  background: var(--gold-face);
  border: 1px solid #6a4410;
  box-shadow:
    0 0 0 1px rgba(255, 224, 138, .55), 0 0 0 4px #1a120a, 0 0 0 5px rgba(216, 178, 90, .6),
    0 12px 30px rgba(0, 0, 0, .6), 0 0 40px rgba(255, 207, 77, .18),
    inset 0 2px 0 rgba(255, 255, 255, .62), inset 0 -4px 0 rgba(122, 76, 14, .45), inset 0 0 0 1px rgba(255, 240, 200, .3);
  text-shadow: 0 1px 0 rgba(255, 255, 255, .5);
  transition: transform .1s ease, filter .14s ease;
}
/* Gilt diamonds flanking the serif label, echoing the banner flourish */
.big-btn::before, .big-btn::after {
  content: ''; flex: none; width: 9px; height: 9px; transform: rotate(45deg);
  background: linear-gradient(135deg, #8a5f14, #4a2f08);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .35), 0 1px 0 rgba(255, 255, 255, .35);
}
.big-btn:hover { filter: brightness(1.08); transform: translateY(-2px) scale(1.03); }
.big-btn:active { transform: translateY(1px); }
.howto { display: flex; justify-content: center; gap: 40px; margin-top: 40px; }
.ht-item {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  color: var(--parchment-dim); font-size: 10.5px; letter-spacing: .13em; text-transform: uppercase; font-weight: 700;
}
.ht-item svg { width: 26px; height: 26px; color: var(--gold-line); filter: drop-shadow(0 2px 4px rgba(0, 0, 0, .6)); }
/* r27 — THE CONTROL HINT. All three observers: "the control hint 3.2-3.3:1 at
   7-11 px caps" — 13 px of #8d94a8 (a blue-grey that appears nowhere else in
   the chrome) floating on the world, and the world is now 2.6x brighter, so
   free-floating text cannot be guaranteed ANY ratio. It goes onto the HUD's
   own plate — the toast's recipe, gold edge and all — where the ratio is a
   constant of two known colours instead of a function of whatever is behind
   it: parchment-dim #cdbfa3 on the plate's #1e1610 measures 9.9:1. 17 px is
   the first Iowan size whose capital prints >= 12 px of ink (0.70 em cap). */
.hint {
  display: inline-flex; align-items: center; margin-top: 28px;
  padding: 8px 22px; border-radius: 4px;
  background: var(--plate-lines), var(--plate);
  border: 1px solid var(--gold-edge);
  box-shadow: var(--plate-shadow), var(--plate-ring);
  color: var(--parchment-dim); font-size: 17px; letter-spacing: .045em;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .8);
}
/* r27 — THE VICTORY PANEL AWARDS SOMETHING. All three observers: "victory
   offers no score, stars, gold, waves, celebration and is the IDENTICAL
   component to the title panel". The award block is UI-only — every number
   is read out of G at the moment the card opens (G.waves.waveIdx/total,
   G.gold, G.lives vs ECON.startLives); nothing in the sim is touched. */
.end-award { margin: 2px 0 34px; position: relative; }
.ea-stars { display: flex; justify-content: center; gap: 16px; margin-bottom: 22px; position: relative; z-index: 1; }
.ea-stars svg {
  width: 40px; height: 40px; color: #3a2f1b;
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .7));
}
/* r29 UI §1: the lit star's own path now carries the bevel (th-ic-star-lit,
   index.html — the button's 4-stop --gold-face ramp + its #6a4410 edge), so
   `color` no longer does the work here; this class only adds the glow and
   the staggered pop-in. Kept `color: var(--gold)` anyway as a harmless no-op
   fallback if a lit star is ever rendered with the plain th-ic-star symbol. */
.ea-stars svg.on {
  color: var(--gold);
  filter: drop-shadow(0 0 10px rgba(255, 207, 77, .6)) drop-shadow(0 2px 2px rgba(0, 0, 0, .7));
  animation: eaStarPop .22s cubic-bezier(.34, 1.56, .64, 1) both;
}
@keyframes eaStarPop {
  from { opacity: 0; transform: scale(.35) rotate(-16deg); }
  to { opacity: 1; transform: scale(1) rotate(0); }
}
/* r29 UI §1: a soft gold burst behind the star row (obs 2 r28: "no burst").
   Pure CSS, sized well under the 200px cap and centred behind .ea-stars;
   the star row itself (z-index 1 above) reads over it. */
.ea-burst {
  position: absolute; left: 50%; top: 20px; width: 190px; height: 190px;
  margin-left: -95px; margin-top: -95px; pointer-events: none; z-index: 0;
  background: radial-gradient(circle, rgba(255, 224, 138, .38) 0%, rgba(255, 207, 77, .18) 38%, rgba(255, 207, 77, 0) 72%);
}
.ea-stats {
  display: flex; justify-content: center; gap: 0;
  border-top: 1px solid rgba(255, 207, 77, .22);
  border-bottom: 1px solid rgba(255, 207, 77, .22);
  padding: 14px 0 12px;
}
.ea-cell {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  min-width: 168px; padding: 0 18px;
  border-left: 1px solid rgba(255, 207, 77, .16);
}
.ea-cell:first-child { border-left: 0; }
.ea-cell .ea-ic { width: 22px; height: 22px; color: var(--gold-line); filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .6)); }
.ea-cell .ea-v {
  font-family: var(--display-font); font-weight: 800; font-size: 26px; line-height: 1;
  color: var(--gold-soft); font-variant-numeric: lining-nums tabular-nums;
  text-shadow: 0 -1px 0 rgba(0, 0, 0, .85), 0 2px 5px rgba(0, 0, 0, .65);
}
.ea-cell .ea-l {
  font-family: var(--display-font); font-variant-caps: small-caps; font-weight: 700;
  font-size: 15px; letter-spacing: .09em; color: var(--parchment-dim);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .8);
}
#loading .game-sub { margin-bottom: 0; animation: pulse 1.4s ease infinite; }
@keyframes pulse { 50% { opacity: .45; } }

/* Golden shimmer motes behind the title (pure CSS, three drifting dot layers) */
.shimmer {
  position: absolute; inset: 0; pointer-events: none;
  background-image:
    radial-gradient(1.4px 1.4px at 22px 34px, rgba(255, 224, 138, .85) 50%, transparent 51%),
    radial-gradient(1px 1px at 64px 96px, rgba(255, 207, 77, .6) 50%, transparent 51%),
    radial-gradient(1.9px 1.9px at 112px 52px, rgba(255, 240, 190, .9) 50%, transparent 51%);
  background-size: 158px 190px, 214px 240px, 262px 300px;
  background-repeat: repeat;
  -webkit-mask-image: radial-gradient(46% 40% at 50% 42%, rgba(0, 0, 0, .95), transparent 78%);
  mask-image: radial-gradient(46% 40% at 50% 42%, rgba(0, 0, 0, .95), transparent 78%);
  animation: shimmerDrift 16s linear infinite, shimmerPulse 3.8s ease-in-out infinite;
}
@keyframes shimmerDrift {
  to { background-position: 0 -190px, 0 -480px, 0 -900px; }
}
@keyframes shimmerPulse { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }

/* r27: the motes are the CELEBRATION on the victory card — the observers'
   "no celebration" — and they must not appear over a defeat. UI.endScreen
   sets `.won` on #screen-end. */
#screen-end .shimmer { display: none; }
#screen-end.won .shimmer { display: block; }
/* QA determinism: freeze decorative motion so captures are reproducible */
.qa-freeze .shimmer { animation: none; opacity: .7; }

/* ---------- Bottom toolbar (r19 rail, r21 molding, r22 timeline, r23 ONE BAR)
   r19: the frame was top-heavy (every piece of chrome in the top ~80px, the
   bottom third bare). MEASURED across the five references (ui.js): kr_2
   closes its bottom edge with an ornamented RAIL carrying islands; the three
   BTD6 frames carry corner clusters and no bar. r19 shipped both.
   r21: critic-A read that rail as WORLD ("a uniform brown-black strip below
   the last row of grass"). MEASURED against kr_2's rail on a plain stretch
   (L by row from the bottom): 47 38 30 36 56 87 123 99 54 | 12 13 | grass —
   a MOLDING: a 2px contact groove, a bevel highlight two rows under it, a
   fall-off, a lip. The rail became that profile; the clusters stood on
   stepped plinths the rail rose into.
   r23: three blind observers, told nothing, independently: "the pips fill
   x 820-1110 of a 1920 px bar; 76-85 % of the strip is empty plate" and "a
   double-decker bottom edge: the palette and THE HOLD sit ABOVE it at
   y 922-965, two stacked bars where one would do"; kr_2 "fills its whole
   bottom bar". MEASURED on the r22 tree at 1920x993: the 16px rail carried
   the row and nothing else (filled columns x764-1156 = 20 % by deviation
   from the plain-plate column), and the two clusters stood 5px above it at
   y924-972 on their plinths — 71px of stacked pieces at each corner.
   So the rail is now THE TOOLBAR: ONE 52px plate. Its top 8 rows are the
   r21 molding, stop for stop (the world still ends at the groove); the body
   is r21's body stop stretched; the lower groove and lip close the bottom
   edge. The armoury, the timeline and the hold are cells INSIDE it — they
   are children of #bottom-rail, so hud=0 removes the whole thing — in the
   .ctl-cluster grammar the top-right corner already speaks: 36px cells
   split by gold hairlines, each group seated in a shallow WELL sunk into
   the plate (the .hg-track device), not a framed card standing on it. The
   plinths are gone; nothing floats above the groove. The timeline seat is
   two rows now — a small-caps wave caption over the r22 pips row — because
   a 16px row alone was lost in a 42px body. */
#bottom-rail {
  position: absolute; left: 0; right: 0; bottom: 0; height: 52px;
  background:
    var(--plate-lines),
    linear-gradient(180deg,
      #060402 0 2px,            /* contact groove: the world stops HERE */
      #4a3620 2px 3px,          /* bevel rising… */
      #8a6a36 3px 4px,
      #a3874a 4px 5px,          /* …to the highlight (L 136 vs kr_2's 123) */
      #7d6636 5px 6px,
      #5a4726 6px 7px,
      #3e3019 7px 8px,          /* …and falling off */
      #2b2112 8px, #241b0f calc(100% - 2px),      /* body: r21's stop, stretched */
      #1a130a calc(100% - 2px) calc(100% - 1px),  /* lower groove */
      #33281a calc(100% - 1px) 100%);             /* bottom lip */
}
/* The timeline seat (r22, two rows in r23): a wave caption over #wave-pips
   bracketed by the wave banner's own flourish pair, centred in the body.
   Flex, so the bracket follows the row's width with no layout read. */
#bottom-rail .rail-tl {
  position: absolute; left: 50%; top: 8px; height: 42px; transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
}
#bottom-rail .rail-row { display: flex; align-items: center; height: 16px; }
#bottom-rail .rail-cap {
  /* the .hg-label token: one small-caps tier across the toolbar.
     r25: 12.5 -> 14px. MEASURED at 1:1 on the r24 ship, ink height of the
     leading capital: 9 px here and on .hg-label, 8 px on .am-name, with the
     small capitals themselves 6-7 px — O2 "near the legibility floor at 1:1".
     Iowan's cap height is ~0.70 em, so 14px is the first size whose capital
     prints 10 px. Vertical cost: the seat's stack is cap 14 + gap 4 + row 16
     = 34 in a 42px seat, so the 52px bar does not grow.
     r28 §3 — obs 3: "ARCHER SPIRE" cap 11px / stems 1px; "FOES" 10/1 — SIZE
     was already settled (r26), WEIGHT was not: `font-weight:700` on a serif
     stack this small (Iowan Old Style is not resident on the capture host;
     the fallback chain lands on Palatino/Georgia at whatever synthetic-bold
     the renderer manages) still measured a 1px median stem. No heavier cut
     is bundled with the game, so the brief's OTHER option ships: a 0.5px
     ink-coloured stroke widens every stem by ~1 device px without moving a
     single glyph's advance width (stroke is painted, not laid out), so the
     tray's dead-run geometry (r24's own equal-run target) is untouched. */
  font-family: var(--display-font); font-variant-caps: small-caps; font-weight: 700;
  font-size: 17.5px; letter-spacing: .07em; line-height: 1; white-space: nowrap;
  /* r30 item 2: the caption goes silent while a wave marches; the row holds
     its box so the seat's stack (cap 17.5 + gap 4 + row 16) never twitches. */
  min-height: 17.5px;
  color: var(--parchment-dim); text-shadow: 0 1px 2px rgba(0, 0, 0, .85);
  -webkit-text-stroke: .5px currentColor;
}
#bottom-rail .rail-cap .n { color: var(--gold-soft); }
#bottom-rail .rail-fl {
  width: 48px; height: 16px; flex: none; color: var(--gold-line); opacity: .78;
  filter: drop-shadow(0 1px 0 rgba(0, 0, 0, .8));
}
#bottom-rail .rail-fl.r { transform: scaleX(-1); }

/* Both groups: 38px wells (36px cells inside a 1px ring) seated in the
   42px body with 2px of plate above and below; the top chips' left edge
   (16px) and the top cluster's right edge (16px) are the alignment. */
#armoury, #hold-gauge {
  position: absolute; top: 10px; height: 38px;
  display: flex; align-items: center; border-radius: 3px;
  background: linear-gradient(180deg, rgba(0, 0, 0, .34), rgba(0, 0, 0, .16));
  border: 1px solid rgba(0, 0, 0, .62);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, .7), inset 0 0 0 1px rgba(255, 207, 77, .13), 0 1px 0 rgba(255, 236, 190, .06);
}
#armoury { left: 16px; padding: 0 2px; }
#hold-gauge { right: 16px; padding: 0 0 0 5px; }

.am-cell {
  /* r30 part 2: the medallion-to-text gap 7 -> 5px. The full names at 14 px
     cap cost the tray 64 px of ink over r29's 12 px cap names, and r25/r26's
     own rule is that the tray pays for its own type out of its own furniture
     (r25 took the cell padding 14 -> 7, r26 7 -> 5, r30 2). This gap and the
     medallion below are the last two pieces of furniture that can pay. */
  display: flex; align-items: center; gap: 5px; height: 36px; padding: 0 10px;
  border-left: 1px solid rgba(255, 207, 77, .22); border-radius: 2px;
}
.am-cell:first-child { border-left: 0; }
.am-ic {
  /* r30 part 2 took this 26 -> 24px to pay for the full names. r31 item 0
     puts it at 28px — critic C's own number ("dark glyphs on dark discs at
     ~20 px … render them in the HUD gold on the disc at ~28 px").
     THE 16 px IS PAID INSIDE THE BAR, not out of the dead runs: the seat's
     #wave-pips padding goes 44 -> 28px in the same block, which shrinks the
     centred seat by exactly 32 and moves its left edge right by 16 — the
     tray's growth and the seat's shift cancel at EVERY viewport width, so
     r24/r25's equal-run rule survives untouched (measured in the r31 UI log).
     THE FIELD GOES DARK AND THE TINT MOVES TO THE RIM. MEASURED at 1:1 on
     the rendered mid frame (ink = a same-seed capture with the four svgs
     `visibility:hidden`): the shipped glyph's ink renders (193,154,128) /
     (185,153,115) / (171,141,164) / (157,170,165) against discs of
     (97,44,28) / (95,58,25) / (81,44,101) / (60,86,97) — WCAG 4.35 / 3.71 /
     3.73 / 3.27:1, which is critic C's "near-invisible rust-on-walnut" in
     numbers. The declared ink is --parchment (relative luminance 0.798), so
     at 6:1 the disc has to sit at or under 0.091 — the frost disc was 0.086
     and the archer's 0.044, i.e. the DISCS were already dark enough and the
     ink was never arriving: a 15px glyph inside a 24px well is ~60 % edge,
     and `drop-shadow(0 0 4px var(--tint))` washed the tint back into every
     one of those edge pixels. So: the field is mixed down (52 % tint -> 22 %,
     18 % -> 8 %), the tower's colour is carried by a 1.5px INSET RING in its
     own tint (the identity signal survives, on the rim where it belongs),
     and the glyph's glow becomes a plain dark drop shadow. */
  width: 28px; height: 28px; flex: none; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(circle at 50% 34%, color-mix(in srgb, var(--tint) 22%, #140d07), color-mix(in srgb, var(--tint) 8%, #0c0804) 62%, #070402);
  box-shadow:
    inset 0 0 0 1.5px color-mix(in srgb, var(--tint) 78%, #2a1c0b),
    inset 0 0 0 2.5px rgba(0, 0, 0, .75),
    0 1px 3px rgba(0, 0, 0, .6);
}
.am-ic svg { width: 19px; height: 19px; color: var(--parchment); filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .85)); }
/* r23: the tower's name over its price — a price board that names what it
   prices (BTD6's shop: icon, name, price). Shown from 1720px: MEASURED, the
   named tray is 604px (x16-620) and the centred 406px seat's left edge is
   (W - 406) / 2, so 24px of clearance needs W >= 1706 — at 1600 the two
   overlapped by 23px; at 1280 the unnamed tray (374px) clears it by 47px. */
/* r25: gap 2 -> 1px. At the 14px name the column stacks 14 + 1 + 20 = 35 in
   a 36px cell; at 2px it would be exactly 36 and flush with the well. */
/* r30 item 5 laid the name and the price side by side on one baseline row and
   shortened the names to their first word, so one 17.5px line had to fit a
   36px cell instead of two. r30 part 2 restores the FULL names on the drift
   panel's verdict, and a full name beside its price is 927 px of tray — 220 px
   past the seat. So the column comes back and carries the 14 px cap with it:
   the cell's width is now max(name, price), not name + gap + price, which is
   the ~200 px that pays for "Cannon Bastion". Vertically the stack is
   name 17.5 + gap 0 + price 17.5 = 35 in a 36 px cell (r25's recorded trap is
   a stack of exactly 36, flush with the well — this clears it by 1 px), which
   needs `line-height: 1` on the price and a 14 px coin beside it. */
.am-txt { display: flex; flex-direction: column; justify-content: center; gap: 0; }
/* r25: the tray's names carried the toolbar's SMALLEST small caps — capital
   ink 8 px, the small capitals 6 px. They go to 14px (capital 10) inside the
   >= 1860px block only, where the dead runs pay for the width; below it the
   r24 geometry is bit-for-bit unchanged, and below 1720 the name is not
   drawn at all. Tracking .06 -> .04em: at 14px .06em is 0.84 px per glyph,
   and the four names carry 51 characters, so the loosening the 11px size
   needed costs 14 px of tray width it no longer needs. */
.am-name {
  display: none; white-space: nowrap; line-height: 1;
  font-family: var(--display-font); font-variant-caps: small-caps; font-weight: 700;
  /* r30 part 2: tracking .01em -> 0. The brief's order for a string that will
     not fit is LETTERSPACING FIRST, cap height last — the four full names
     carry 51 characters, so this is the whole of what tracking has left to
     give here (8.9 px), and the cap height stays at 14. */
  font-size: 17.5px; letter-spacing: 0; color: var(--parchment-dim);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .8);
  -webkit-text-stroke: .5px currentColor;   /* r28 §3, see .rail-cap's note */
}
/* r30 part 2: the gate moves 1720 -> 1888px, to the width whose own block
   pays for the names (the 1px cell padding and the 391px track). MEASURED:
   the full names at 14 px cap make a 760 px tray under the 10px padding that
   rules the 1720-1888 band, against a left run of (W-474)/2 + 6 - 16 = 623 px
   of room at 1720 — a 147 px overlap. r29 (712.5 px tray) and r30 part 1
   (712.7) both overlapped that band by ~99 px already; this closes it rather
   than widening it. At 1888 itself the runs are 8.4 / 8.7 px, positive, which
   is r24's own gate test ("the first width where the widened set fits AT
   ALL"). Below the gate the tray prints medallion + price, as it does below
   1720 today. */
@media (min-width: 1888px) { .am-name { display: block; } }
.am-cost { display: flex; align-items: center; gap: 4px; }
.am-cost svg { width: 14px; height: 14px; filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .5)); }
.am-cost span {
  color: var(--gold); font-size: 17.5px; font-weight: 700; line-height: 1;
  font-variant-numeric: lining-nums tabular-nums; text-shadow: 0 1px 2px rgba(0, 0, 0, .7);
}
.am-cell.cant { filter: saturate(.28) brightness(.6); }
.am-cell.cant .am-cost span { color: #ff6b5e; }

/* The hold's integrity as a gauge — the boss marquee's track/fill recipe,
   in the lives red the top chip's heart already establishes — and, r23, the
   foes left in the wave beside it (kr_2's skull counter): the pending spawns
   plus the walkers, or the coming wave's size through a countdown. */
.hg-crest {
  width: 30px; height: 30px; flex: none; border-radius: 50%; margin: 0 9px 0 3px;
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(circle at 50% 34%, #3a2b16, #171009 68%, #0d0805);
  box-shadow: inset 0 0 0 1px rgba(255, 207, 77, .45), 0 1px 3px rgba(0, 0, 0, .6);
}
.hg-crest svg { width: 17px; height: 17px; color: var(--gold-soft); filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .6)); }
.hg-body { display: flex; flex-direction: column; gap: 3px; padding-right: 13px; }
.hg-body .hg-label { padding-left: 1px; }
/* r30 item 5 — SECONDARY TYPE AT 8-11 PX CAP HEIGHT (obs 1 #12, obs 3 #12).
   Calibration is r25's own, re-used: this display stack's capital is ~0.70 em,
   which is why 14 px printed a 10 px capital. A 12 px capital therefore needs
   17.1 px, so every secondary string below goes to 17.5 px (capital 12.25) and
   pays for the width out of its own LETTERSPACING, which is what obs 3 called
   out in the same breath ("letterspaced small-caps serif"). The bar stays 52 px
   and every stack is re-checked against its own 36 px cell in the numbers
   below. */
.hg-label {
  /* r25: 12.5 -> 14px with .rail-cap — one tier, one size. The hold's stack
     is label 14 + gap 5 + track 10 = 29, and the counters' is 14 + 4 + 15 =
     33, both inside the 36px cell; the bar is untouched.
     r30 item 5: 14 -> 17.5px (capital 10 -> 12.25). The two stacks are
     re-balanced to hold inside the same 36px cell: the hold's is 17.5 + gap 3
     + track 15 = 35.5, the counters' 17.5 + gap 2 + value 17.5 = 37 minus the
     value's own baseline overlap — both measured live in the r30 UI log. */
  font-family: var(--display-font); font-variant-caps: small-caps; font-weight: 700;
  font-size: 17.5px; letter-spacing: .07em; color: var(--parchment-dim);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .8); line-height: 1;
  -webkit-text-stroke: .5px currentColor;   /* r28 §3, see .rail-cap's note */
}
.hg-track {
  /* 146px base / 200px from 1400 / 381px from 1860 — the width the Next cell
     leaves. See the .hg-next and the r24 dead-run notes below. r25: the two
     lower tiers each give the track 24 px, which is exactly what the 14px
     labels and the Next cell's unit ("foes") add to the right cluster —
     MEASURED at 1280, hold 395 -> 418 and the gap to the centred seat 26 ->
     3 px before the track paid it back, 27 px after. */
  /* r30 item 2 — obs 3 #5: "a 380 x 6 px strip for 20 lives = 19 px per life
     on a 6 px bar; one death moves it 5 % and you will never see it". The
     brief's option: a >= 10 px BAR. 10 -> 15 px of box, so the FILL itself
     prints 13 px. The hold's stack is label 14 + gap 5 + track 15 = 34 in a
     36 px cell and the bar stays 52 px. */
  position: relative; width: 146px; height: 15px; border-radius: 7px; overflow: hidden;
  background: linear-gradient(180deg, rgba(10, 7, 4, .92), rgba(28, 20, 11, .92));
  border: 1px solid var(--gold-edge);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, .8), 0 1px 0 rgba(255, 236, 190, .08);
}
/* r27 — THE HOLD READS ITS OWN FRACTION. All three r26 blind observers wrote
   the same defect down: the gauge was CRIMSON at 100 % ("The Hold bar is
   crimson at 100 %", "a hot-pink (237,90,109) bar duplicating the heart in a
   hue nowhere else in the HUD"), so the one bar that says "you are fine" was
   painted in the colour of "you are dying" and never changed. Three stops by
   fraction now: SAFE > 60 %, WARN 30-60 %, CRITICAL <= 30 % (at ECON's 20
   lives: >= 13 / 7-12 / <= 6), set from _refreshDock on change only.
   The safe green is an EMERALD at hue ~146, not the KR bar green at ~100:
   obs 1 measured "the widest 40-deg hue window holds 38.8-48.8 % of every
   frame, forest 97.8 vs grass 99.9" — a 100-deg green in the chrome would
   land inside the frame's single most crowded hue bin. 146 is 46 deg clear of
   the grass and 49 deg clear of the frost accent. */
.hg-fill {
  position: absolute; inset: 1px auto 1px 1px; width: 100%;
  border-radius: 6px; transform-origin: left center;
  background: linear-gradient(180deg, #8fe8b0, #3fbf72 45%, #1c7a46);
  box-shadow: 0 0 7px rgba(63, 191, 114, .34);
}
.hg-fill.warn {
  background: linear-gradient(180deg, #ffdf9a, #e8a022 45%, #a4650a);
  box-shadow: 0 0 7px rgba(232, 160, 34, .38);
}
.hg-fill.low {
  background: linear-gradient(180deg, #ff9a86, #e0402c 45%, #7d1710);
  box-shadow: 0 0 7px rgba(224, 64, 44, .42);
}
.hg-notches {
  position: absolute; inset: 0;
  background: repeating-linear-gradient(90deg, transparent 0 calc(20% - 1px), rgba(0, 0, 0, .4) calc(20% - 1px) 20%);
}
.hg-foes {
  display: flex; align-items: center; gap: 8px; height: 36px; padding: 0 13px 0 12px;
  border-left: 1px solid rgba(255, 207, 77, .22);
}
.hg-foes svg { width: 17px; height: 17px; flex: none; color: var(--parchment-dim); filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .6)); }
.hg-fbody { display: flex; flex-direction: column; gap: 1px; }   /* the hold body's own stack: label over value. r30 part 2: gap 2 -> 1, so the stack is label 17.5 + 1 + value 17.5 = 36 inside the 36px cell (at 2 it stood 0.5px proud of the cell at both ends) */
.hg-foes span, .hg-next span {
  display: block; min-width: 2ch; line-height: 1; color: var(--parchment); font-size: 17.5px; font-weight: 700;
  font-variant-numeric: lining-nums tabular-nums; text-shadow: 0 1px 2px rgba(0, 0, 0, .7);
}
/* r24: the coming wave's SIZE, the third cell of the right cluster. r23 made
   one skull counter mean two things — foes on the field while a wave marched,
   the coming wave's size during the countdown — so the same glyph answered
   two questions and neither at the same time. Split: FOES is the field, NEXT
   is what follows, and the toolbar caption names that same wave. Same cell
   recipe as .hg-foes without an icon (one skull in the cluster, not two). */
/* r30 item 2 took .hg-next's own WELL away and moved its number into the Foes
   cell ("Foes 9 / 16 next"). r30 part 2 puts the cell back — six blind drift
   scorers, one point each off ui_hud against r29, all six naming the "split
   Foes/Next cells" among the pieces that read as a shipped game. Two labelled
   wells; the reader is not asked to parse a compound. */
.hg-next {
  /* MEASURED: the cell adds 72px to the right cluster, and at 1280 the r23
     cluster already cleared the centred seat by only 45px — showing it there
     at r23's track length overlapped the timeline by 28px. So below 1400 the
     TRACK pays for the cell (224 -> 170px) and the cell still ships; the
     display toggle here is the guard for anything narrower still. */
  display: none; align-items: center; height: 36px; padding: 0 15px 0 14px;
  border-left: 1px solid rgba(255, 207, 77, .22);
}
@media (min-width: 1280px) { .hg-next { display: flex; } }
@media (min-width: 1400px) { .hg-track { width: 200px; } }
.hg-next span { color: var(--parchment-dim); }
/* r25: the count's noun. "Next 16" read as cryptic to a drift scorer — a
   bare integer beside a Foes cell whose label is its own noun. The unit is
   the label tier at the counter's own scale, so the two cells read as one
   quantity twice: "Foes 9" on the field, "Next 16 foes" coming. */
.hg-val { display: flex; align-items: baseline; gap: 5px; }
.hg-u {
  font-family: var(--display-font); font-variant-caps: small-caps; font-style: normal;
  font-weight: 700; font-size: 15px; letter-spacing: .04em; line-height: 1;
  color: var(--parchment-dim); text-shadow: 0 1px 2px rgba(0, 0, 0, .8);
}
/* The last wave prints a bare em dash and the unit goes, but the CELL must
   not: display:none there would twitch the whole right cluster 13 px wider
   the moment the last wave starts (MEASURED: hold 670 -> 657, the gap 37 ->
   50). It keeps its box and loses its ink. */
.hg-u.off { visibility: hidden; }

/* r24 — THE TWO DEAD RUNS. Three blind observers measured the r23 toolbar as
   "25.6-31.0 % dead in two runs", 125-143 px between the tray and the seat
   and 367-371 px between the pips and THE HOLD — "the 371 px gap alone is
   19.3 % of the screen width". MEASURED here by the same instrument (a
   column carries a MARK when |L - the plain-plate median column| > 24 over
   the strip's rows; a dead run is a maximal run of columns with no mark):
   x620-763 = 143 px and x1157-1528 = 371 px, both reproduced to the pixel.

   The geometry, since the seat is transparent and only INK closes a run:
     left  run = (W - row)/2 + 6 - (16 + tray)
     right run = (W - 16 - hold) - ((W + row)/2 - 6)
   (the +-6 is the flourish's ink inset inside its 48px box). At W = 1920
   both fall to 77 px with row 474, tray 636 and hold 636 — so the three
   groups grow toward each other and the runs come down together, symmetric.
   The hold's share is real content, not padding: a 370px integrity bar (the
   boss marquee's track is 460px in a 520px plate — same recipe, correct
   hierarchy) and the Next cell above.

   Gated at 1860px, not the 1720px the tower names use: the widened set needs
   (W - 474)/2 >= 16 + 636 + a gap, so 1820 is the first width that fits at
   all (21px gaps, MEASURED) and 1860 the first that fits comfortably (41px).
   Below it the r23 toolbar ships unchanged, and its runs at 1280 are 63px —
   already inside the target, because the r23 groups nearly fill a 1280 bar. */
/* r25 keeps this block's JOB — the two runs equal and small — while the
   labels inside it grow. The runs are independent, not an average:
     left  run = (W - row)/2 + 6 - (16 + tray) = 713 - tray   at W 1920
     right run = (W - 16 - hold) - ((W + row)/2 - 6) = 713 - hold
   so each group's own width sets its own run. The 14px names cost the tray
   +104 px of ink; the cell padding gives 56 of it back (14 -> 7, MEASURED
   as still clear of the 26px medallion) and the tightened tracking 14, and
   the track then grows to put the hold back on the tray's width so the two
   runs stay equal. Both land well inside the r24 target (77 px).

   The gate moves 1860 -> 1888px, by r24's own reading of the ladder ("the
   first width where the widened set fits AT ALL", 21 px gaps): the r25 set
   is 34 px wider per side than r24's, so gap = W/2 - 923 and 21 px lands at
   1888. MEASURED at 1888: 21 / 21. At 1860 the r25 set would sit on 7 px
   gaps, so below 1888 the r24-and-below toolbar ships unchanged. */
/* r26 — THE TRAY'S NAMES GO ONE STEP MORE (obs 2: "the smallest text in the
   game, marginal at native scale"; obs 1 read the capital at 10 px). MEASURED
   at 1:1 with caps.py on the shipped frame: the four names' leading capital
   is 10 px of ink and the small capitals 7 px, the smallest tier in the bar.
   Iowan Old Style's capital is ~0.70 em, so 15px is the first size whose
   capital prints 11 — and 16px prints 11 as well (MEASURED, both captured
   with the override injected before the shot), so the step is 14 -> 15 and
   the extra pixel of 16px buys nothing but width.
   THE WIDTH IS PAID INSIDE THE TRAY, not out of the dead runs: the four
   names carry 51 characters, so 14 -> 15px costs +33.7 px of ink (473.1 ->
   506.8 by Range.getBoundingClientRect); tracking .04 -> .02em gives 15.3
   back and the cell padding 7 -> 5 px gives 16, landing the tray at 672.5
   (was 670.0). The hold's track takes the same 3 px (381 -> 384) so the two
   dead runs stay equal, as r24 and r25 kept them.
   .am-txt's gap goes to 0 in this block only: at 15px the stack is
   15 + 1 + 20 = 36 in a 36 px cell — exactly flush with the well, which is
   the trap r25 recorded when it set that gap to 1. */
@media (min-width: 1888px) {
  /* r30 part 2 — the full names come back and the equal-run rule is re-solved
     from scratch at 14 px cap. The order of levers is the brief's: the stack
     first (name OVER price, so a cell is max(name, price) wide, not their
     sum — worth ~200 px), then the tracking (.am-name .01em -> 0, 8.9 px),
     then the cell padding, and the cap height never. The hold's track is the
     r24-precedented counterweight that puts the two groups back on the same
     width, exactly as r25 and r26 used it. MEASURED numbers in the r30 UI
     log. */
  .am-cell { padding: 0 1px; }
  /* r31 item 0: 44 -> 28px. The 28px medallions cost the tray 16 px; this
     takes 32 px off the centred seat, so its left edge moves right by 16 and
     the two cancel — the dead runs are the SAME number at every width
     (MEASURED: 18.4 / 18.8 px at 1920 before and after). The seat's own
     stack is untouched: the pips keep their 10px squares and 10px gaps and
     the flourish pair just sits 16 px closer to them, and the widest caption
     in the game (`Next · Wave 3 — Skittering Vanguard`, 407.3 px) still
     fits inside the narrowed 442 px seat, so the row never sets its width. */
  #wave-pips { padding: 0 28px; }
  #hold-gauge { padding-left: 12px; }
  .hg-crest { margin: 0 13px 0 6px; }
  .hg-body { padding-right: 22px; }
  .hg-track { width: 407px; }   /* r30 part 2: 384 -> 391. r31 item 0: 391 -> 407, the same r25/r26 counterweight — the tray grew 16 px for the 28px medallions, so the hold takes the identical 16 and the two runs stay equal (MEASURED: tray 704.6 / hold 704.2) */
  .hg-foes { gap: 10px; padding: 0 17px 0 16px; }
  .hg-next { padding: 0 19px 0 18px; }
}

/* ======================================================================
   r32 UI items 0 + 2 — THE BAR'S PORTRAITS and THE CARDS' BAR
   ----------------------------------------------------------------------
   Behind `?r32bar=1` (a class on #bottom-rail, so nothing here can be
   reached with the flag off). critic B, sev 2: "replace the four text-label
   tower entries with chunky 64 px+ icon cards (tower portrait, cost badge,
   hotkey, selected/affordable states) anchored into a weighted left corner
   block"; critic A: "44-48 px painted tower portraits (roof + body
   silhouette on the dark brown card)"; the drift's named cost: "the bottom
   bar is more cramped — two-line tower names with no framed separation".

   THE SHAPE IS SOLVED BY THE BAR, NOT CHOSEN. The bar is 52 px and stays 52
   (r30's settled finish); its body is the 42 px between the r21 molding
   (top 8 rows) and the lower groove, so 40 px is the tallest portrait that
   fits inside it with a rim — which is the item's own "≥ 40 px tall" floor
   and 4 px under critic A's 44. Sideways the tray may grow by ~16 px in
   TOTAL: the centred seat can give up (442 − 407.3) / 2 = 17.35 px a side
   before the widest caption in the game sets its width (r30 part 2's
   invariant), and every px the tray takes past that comes straight off the
   dead runs r24/r25 keep equal. So the slot is a PORTRAIT — 34 wide by 40
   tall — and not a 44 px disc, which would have cost 64.

   The 16 px is paid the way r31 paid its own: #wave-pips 28 -> 12 px takes
   32 off the centred seat and moves its left edge right by 16; .hg-track
   407 -> 423 gives the hold the identical 16. The two dead runs come out at
   the SAME number as r31 (MEASURED: 18.4 / 18.7). */
#bottom-rail.r32bar #armoury {
  /* the 40 px portrait needs the body's whole 42 px: the well's top edge
     lands on the molding's last row and its bottom on the lower groove. */
  top: 8px; height: 42px;
}
#bottom-rail.r32bar .am-cell {
  height: 40px; gap: 3px; align-items: center;
  /* item 2 — A FRAMED SEPARATOR PER SLOT. The r31 bar's only separation was
     one 1 px gold hairline at 22 % alpha; this is a cut GROOVE (a dark left
     wall and a lit right one) plus a shallow recess for the slot's own
     plate. Both are inset shadows and a border that already existed, so the
     framing costs the tray ZERO px — the whole 16 px budget goes to the
     portraits. */
  border-left: 1px solid rgba(0, 0, 0, .55);
  box-shadow: inset 1px 0 0 rgba(255, 207, 77, .20), inset -1px 0 0 rgba(0, 0, 0, .34);
  background: linear-gradient(180deg, rgba(255, 236, 190, .035), rgba(0, 0, 0, .12) 62%, rgba(0, 0, 0, .22));
  border-radius: 2px;
}
#bottom-rail.r32bar .am-cell:first-child { border-left: 0; box-shadow: inset -1px 0 0 rgba(0, 0, 0, .34); }
#bottom-rail.r32bar .am-cell:last-child { box-shadow: inset 1px 0 0 rgba(255, 207, 77, .20); }
/* THE PORTRAIT. The r31 medallion's colour rule is kept exactly — the field
   mixed DOWN to 22 %/8 % tint over a near-black base with the tower's own
   colour carried on a 1.5 px inset ring, because r31 measured that the discs
   were never too light: the ink was not arriving (a 15 px glyph in a 24 px
   well is mostly edge, and the old tint glow washed the tint back into every
   edge pixel). Same rule, a bigger stage. */
#bottom-rail.r32bar .am-por {
  position: relative; width: 34px; height: 40px; flex: none; border-radius: 3px;
  display: flex; align-items: flex-start; justify-content: center;
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--tint) 22%, #140d07), color-mix(in srgb, var(--tint) 8%, #0c0804) 58%, #070402);
  box-shadow:
    inset 0 0 0 1.5px color-mix(in srgb, var(--tint) 78%, #2a1c0b),
    inset 0 0 0 2.5px rgba(0, 0, 0, .75),
    0 1px 3px rgba(0, 0, 0, .6);
  overflow: hidden;
}
#bottom-rail.r32bar .am-sil {
  width: 22px; height: 27px; margin-top: 1px;
  color: var(--parchment);
  /* r31's lesson on this exact element: a tinted glow is what ate the ink.
     A plain dark drop shadow, nothing else. */
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .85));
}
/* THE COST BADGE — a plate across the portrait's foot (KR's shop grammar:
   the price is part of the card, not a second line of text under it). */
#bottom-rail.r32bar .am-price {
  /* inset by the portrait's own 2.5 px rim so the tower's colour ring runs
     unbroken round all four sides — a full-bleed plate ate the bottom of it. */
  position: absolute; left: 2px; right: 2px; bottom: 2px; height: 13px;
  border-radius: 0 0 2px 2px;
  display: flex; align-items: center; justify-content: center;
  background: linear-gradient(180deg, rgba(10, 6, 3, .88), rgba(4, 2, 1, .95));
  box-shadow: inset 0 1px 0 rgba(255, 207, 77, .30);
}
#bottom-rail.r32bar .am-price span {
  color: var(--gold); font-size: 15px; font-weight: 700; line-height: 1;
  font-variant-numeric: lining-nums tabular-nums; letter-spacing: -.01em;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .8);
}
#bottom-rail.r32bar .am-cell.cant .am-price span { color: #ff6b5e; }
/* the name is the only thing left in the text column, so it is one line and
   it centres on the portrait — r30 part 2's type is untouched (same face,
   same 17.5 px size, same tracking, same stroke). */
#bottom-rail.r32bar .am-txt { justify-content: center; }

/* r32 item 0 (critic A) — "Next · 16 foes" on ONE line. `?r32next=1`. */
#bottom-rail.r32next .hg-next .hg-fbody { flex-direction: row; align-items: baseline; gap: 6px; }
#bottom-rail.r32next .hg-dot { font-style: normal; color: var(--gold-line); opacity: .75; font-size: 17.5px; line-height: 1; }
#bottom-rail.r32next .hg-next .hg-label { padding-left: 0; }

@media (min-width: 1888px) {
  /* the tray's own furniture pays 2 of the 4 px a slot costs: the
     portrait-to-name gap 5 -> 3 (the portrait has a rim of its own now, so
     it does not need the air a bare disc did). */
  #bottom-rail.r32bar .am-cell { gap: 3px; padding: 0 1px; }
  /* the counterweights, the same two r31 used, at the same 1:2:1 arithmetic */
  #bottom-rail.r32bar #wave-pips { padding: 0 12px; }
  #bottom-rail.r32bar .hg-track { width: 423px; }
  /* the one-line Next cell is 69.2 px wider than the stacked one (MEASURED:
     102.97 -> 172.17), so the hold's gauge pays that back on top of its own
     16 px counterweight — the same lever, twice. One rule per flag
     combination, because the two flags are independent for the coordinator. */
  #bottom-rail.r32bar.r32next .hg-track { width: 353.8px; }
  #bottom-rail.r32next .hg-track { width: 337.8px; }
}

/* ======================================================================
   r33 UI — THE THIRD BAR and the observers' additions
   ----------------------------------------------------------------------
   Every rule below is behind an r33 class (on #bottom-rail, or on <html> for
   the surfaces that live outside the rail), so nothing here can be reached
   with the flags off and the shipped HUD is byte-identical to HEAD.

   ITEM 0 — THE PORTRAITS WITHOUT THE CRAMPING (`?r33bar=1`).
   The r32 tray won its blind pair on the READ (8 vs 4 in both views) and lost
   the six-scorer drift on the FINISH: ui_hud 5.33 vs 6.17, the WHOLE gap of
   the round, four scorers with the same three words — "crams the cost inside
   a tiny pixel-glyph tile" (2), "the price digit sits cramped on top of the
   square tower glyph" (5), "lets the tower labels collide with their
   neighbours" (3), "runs Next · 6 foes inline" (1/4) — against r31's bar,
   "round emblem icons, name over cost, breathing room, stacked Foes/Next
   boxes reads as shippable" (3).

   So the third bar is r31's LAYOUT with r32's PORTRAIT in it, and the three
   complaints are answered one at a time:
     * the cost comes OUT of the portrait entirely and goes back to r31's own
       coin chip on its own line under the name (`.am-cost`, untouched type,
       untouched coin) — nothing is printed inside the portrait's plate;
     * the slots get a real GUTTER (a 4 px margin on top of the 3 px the cells
       already carried = 7 px of bar between one slot's ink box and the next
       slot's portrait) and each slot becomes a framed recessed PLATE, so the
       ink of one slot cannot sit closer to its neighbour's picture than to
       its own (obs 3 K6: "icon -> own name 5 / 5 / 4 / 2 px vs name -> next
       icon 4 / 4 / 7 px ... a player has to think for a beat about which name
       belongs to which icon"); and
     * the Foes / Next block is left STACKED — `.r33bar` adds no rule to it at
       all, and `r32next` is a different flag that this one never sets.

   THE WIDTH, and its named cost. The tray is r31's 704.58 px; the portrait
   costs +6 px a slot over the 28 px medallion and the gutter +4 px a
   boundary, and the tray pays 2 px back at each end (`#armoury`'s own
   padding) and 1 px at each outer slot edge. That lands the tray at 730.6 px
   (<= 760) and the bar at 52. r32's counterweights carry the first 16 px of
   it exactly as they did (#wave-pips 28 -> 12 px takes 32 off the centred
   seat, so the seat is 410 and the game's widest caption — 407.34 px at
   `build` — still sits inside it, r30 part 2's invariant intact), and
   `.hg-track` 407 -> 433 keeps the hold the same width as the tray so the two
   dead runs stay EQUAL, which is r24/r25's actual rule. What does NOT survive
   is r31's dead-run NUMBER: the seat cannot give more than 17.35 px in total
   before the caption starts setting its own width, so the gutter's last 10 px
   come out of the runs and they land at 8.4 / 8.8 px instead of 18.4 / 18.8.
   That is r24's own gate figure ("8.4 / 8.7 px, positive") and it is the
   price of the drift's "breathing room"; it is named here and measured in the
   log rather than discovered by the next round. */
#bottom-rail.r33bar #armoury {
  /* the 40 px portrait wants the rail body's whole 42 px (the r21 molding is
     the top 8 rows), and the tray's own 2 px of side padding is the cheapest
     px in the bar — it buys 4 of the gutter's 12. */
  top: 8px; height: 42px; padding: 0;
}
#bottom-rail.r33bar .am-cell {
  /* gap 5 -> 4: r31's medallion-to-text gap, one px tighter because the
     portrait carries its own rim. It is also the item's own floor — the cost
     chip's coin must clear the portrait's plate by >= 4 px, and this gap IS
     that distance. */
  height: 40px; gap: 4px; align-items: center; padding: 0 1px;
  /* the framed recessed plate: the r32 cut groove kept (a dark right wall and
     a lit left one) plus a shallow recess, all inset shadows on a box that
     already existed, so the FRAME costs the tray 0 px — only the gutter
     between the frames is paid for. */
  border-left: 1px solid rgba(0, 0, 0, .55);
  box-shadow: inset 1px 0 0 rgba(255, 207, 77, .20), inset -1px 0 0 rgba(0, 0, 0, .34);
  background: linear-gradient(180deg, rgba(255, 236, 190, .035), rgba(0, 0, 0, .12) 62%, rgba(0, 0, 0, .22));
  border-radius: 2px;
}
/* THE GUTTER. 3 px of cell padding + border became 7 px of bar between one
   slot's last glyph and the next slot's portrait — and, because the gap
   INSIDE a slot is 4 px, every label is now nearer its own picture than its
   neighbour's, which is the thing obs 3 measured backwards. */
#bottom-rail.r33bar .am-cell + .am-cell { margin-left: 4px; }
#bottom-rail.r33bar .am-cell:first-child { padding-left: 0; border-left: 0; box-shadow: inset -1px 0 0 rgba(0, 0, 0, .34); }
#bottom-rail.r33bar .am-cell:last-child { padding-right: 0; }
/* THE PORTRAIT — r32's, unchanged to the decimal (the pair's read stands).
   The r31 medallion's colour rule: the field mixed DOWN to 22 %/8 % tint over
   near-black with the tower's own colour on a 1.5 px inset ring, because r31
   measured that the discs were never too light — the ink was not arriving. */
#bottom-rail.r33bar .am-por {
  position: relative; width: 34px; height: 40px; flex: none; border-radius: 3px;
  display: flex; align-items: center; justify-content: center;
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--tint) 22%, #140d07), color-mix(in srgb, var(--tint) 8%, #0c0804) 58%, #070402);
  box-shadow:
    inset 0 0 0 1.5px color-mix(in srgb, var(--tint) 78%, #2a1c0b),
    inset 0 0 0 2.5px rgba(0, 0, 0, .75),
    0 1px 3px rgba(0, 0, 0, .6);
  overflow: hidden;
}
#bottom-rail.r33bar .am-sil {
  /* the cost is no longer sitting on the portrait's foot, so the silhouette
     gets the whole plate and centres in it instead of hanging from the top. */
  width: 24px; height: 30px;
  color: var(--parchment);
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .85));
}
/* the text column is r31's: the name on one line, the coin chip under it.
   Nothing about the type, the coin or the stack is changed. */
#bottom-rail.r33bar .am-txt { justify-content: center; gap: 0; }

@media (min-width: 1888px) {
  /* the counterweights. #wave-pips is r31's and r32's own lever (28 -> 12 px
     takes 32 off the centred seat and moves its left edge right by 16), and
     .hg-track is r24/r25/r26/r30/r31's: the hold takes exactly what the tray
     takes, so the two dead runs stay equal. 407 -> 433 = the tray's +26. */
  #bottom-rail.r33bar .am-cell { gap: 4px; padding: 0 1px; }
  #bottom-rail.r33bar .am-cell:first-child { padding-left: 0; }
  #bottom-rail.r33bar .am-cell:last-child { padding-right: 0; }
  #bottom-rail.r33bar #wave-pips { padding: 0 12px; }
  #bottom-rail.r33bar .hg-track { width: 433px; }
}

/* ----------------------------------------------------------------------
   OBSERVERS' ADDITION (4) — THE TOWER ROW HAS NO STATE (`?r33states=1`).
   obs 3 #5: `early` and `build` are pixel-identical across the whole row
   (0 px changed over 38 480 px) — opening a build pad changes nothing in the
   place that prices the towers. Two states, both classes written by
   `_refreshDock` on change only:
     * `.building` on the rail — a pad is open, so the row is answering a
       question: the affordable slots take a lit gold edge and a lifted plate,
       and the ones you cannot afford take the r19 `cant` treatment HARDER
       (the portrait itself desaturates, not only the price), so "can" and
       "cannot" are two pictures instead of one number in red;
     * `.sel` on a slot — the tower you have selected is ringed in the bar, so
       the panel on the left and the row at the bottom name the same object. */
#bottom-rail.r33states .am-cell { transition: none; }
#bottom-rail.r33states.building .am-cell:not(.cant) {
  border-color: rgba(255, 207, 77, .55);
  box-shadow:
    inset 1px 0 0 rgba(255, 207, 77, .20), inset -1px 0 0 rgba(0, 0, 0, .34),
    inset 0 0 0 1px rgba(255, 207, 77, .38), 0 0 8px rgba(255, 207, 77, .22);
  background: linear-gradient(180deg, rgba(255, 236, 190, .085), rgba(0, 0, 0, .08) 62%, rgba(0, 0, 0, .20));
}
#bottom-rail.r33states.building .am-cell.cant { filter: saturate(.18) brightness(.46); }
#bottom-rail.r33states .am-cell.sel {
  box-shadow:
    inset 0 0 0 2px var(--gold-line), inset 0 0 0 3px rgba(0, 0, 0, .6),
    0 0 10px rgba(255, 207, 77, .30);
  background: linear-gradient(180deg, rgba(255, 236, 190, .12), rgba(0, 0, 0, .06) 62%, rgba(0, 0, 0, .18));
}
#bottom-rail.r33states .am-cell:hover:not(.cant) { border-color: rgba(255, 224, 138, .8); }

/* ----------------------------------------------------------------------
   OBSERVERS' ADDITION (6) — FOES AND NEXT ARE THE SAME CELL TWICE
   (`?r33next=1`). The markup change is in ui.js (the noun slot carries the
   coming wave's NAME instead of the word "foes"); this is the other half of
   the ask, "one rule between them". The cells' 1 px gold hairline at 22 %
   alpha is the same separation r31's bar had and the drift called flat: it
   becomes a cut rule — a dark wall with a lit one beside it, the toolbar's
   own groove language — at 0 px of extra width. */
#bottom-rail.r33next .hg-foes,
#bottom-rail.r33next .hg-next {
  border-left: 1px solid rgba(0, 0, 0, .55);
  box-shadow: inset 1px 0 0 rgba(255, 207, 77, .26);
}
/* the name is the count's noun, so it keeps the unit tier exactly and only
   stops wrapping. */
#bottom-rail.r33next .hg-u { white-space: nowrap; }

/* ----------------------------------------------------------------------
   OBSERVERS' ADDITION (5) — THE WHOLE HUD IS SET AT 12 px (`?r33size=1`).
   obs 3 #3 measured every number in the chrome at a 12 px cap — 1.21 % of the
   frame's height — while "374" occupies 24 % of its own plate, i.e. the chips
   have the room and are not using it. The two numbers a player reads while
   playing are GOLD and LIVES; they go to a 23 px face, whose lining figures
   print a >= 16 px cap in Iowan (0.70 em). The wave chip is left alone (it is
   a label-plus-fraction, not a live number), the plates, icons, padding and
   the chip height are untouched, and the chips grow to the right into the
   topbar's own spacer, so nothing else in the HUD moves. */
html.r33size #chip-gold > span,
html.r33size #chip-lives > span { font-size: 23px; line-height: 1; }

/* ----------------------------------------------------------------------
   OBSERVERS' ADDITION (7) — THE PIPS, THE STARS AND THE RULES (`?r33pips=1`).
   obs 3 #12: the wave pips read 2.0 : 1 empty-against-rail and the onboarding
   rules cap at 7 px.
     * the PIPS. r31 item 4 already took the socket to near-black and put a
       track under it; the remaining half is the TRACK, and the arithmetic is
       forced: against a near-black socket (relative luminance ~0.008) a 3 : 1
       ratio needs the floor at (3 x 0.058) - 0.05 = 0.124, which is a mid
       walnut, not the dark one the rail carries. So the track's floor comes
       up to #7a5f38 / #66502d — ENM's r30 enemy-bar track, two stops lighter —
       and keeps its groove-and-lip bevel, which is also the right drawing: a
       recessed timeline wants a floor you can see the sockets against.
     * the STARS and the RULES are pure size: the start card's rule star and
       the tower panel's tier stars to a >= 10 px ink box, the three onboarding
       captions from a 7 px cap to >= 11 (10.5 -> 16 px, 0.70 em cap). The
       .howto row is centred in a 40 px-gapped flex, so the wider captions cost
       the card nothing but its own air. */
#bottom-rail.r33pips #wave-pips {   /* r33 VERDICT: the track's lift REFUSED (pips-mid) — the rail's own class only; html.r33pips (shipped) no longer reaches the track */
  background: linear-gradient(180deg, #86693e, #6e5631);
}
html.r33pips .title-rule svg { width: 17px; height: 17px; }
html.r33pips #tp-tier svg { width: 20px; height: 20px; }
html.r33pips .ht-item { font-size: 16px; letter-spacing: .10em; }
html.r33pips .ea-stars svg { width: 46px; height: 46px; }

/* ----------------------------------------------------------------------
   OBSERVERS' ADDITION (8) — THE STAT ICONS ARE TICK MARKS (`?r33dmg=1`).
   The glyph itself is the swap in ui.js (#th-ic-dmg, drawn on the diagonal so
   its ink fills 21.4/24 of the box on BOTH axes). This is the size half: the
   build card's stat row is set at 11 px, which cannot print a 12 px ink box
   whatever is drawn in it, so the DAMAGE row's icon goes to 14 px (ink 12.5)
   and the row's own box grows the 2 px with it. The panel's rows are already
   18 px (ink 16.0) and are untouched. */
/* The stat block's HEIGHT is not allowed to move — r31 part 2 records what
   happens when it does (the stack overflowed its 108 px content box and the
   only shrinkable item, the medallion, ate it and rendered 43 x 27). So the
   two 12 px rows become two 14 px rows and the block's own leading pays the
   4 px back exactly: margin-top 1 -> 0, padding-top 3 -> 1, gap 1 -> 0.
   Total block height 1+3+1+12+1+12 = 30 before, 0+1+1+14+0+14 = 30 after. */
html.r33dmg .bm-stats { margin-top: 0; padding-top: 1px; gap: 0; }
html.r33dmg .bm-st { height: 14px; }
html.r33dmg .bm-st svg { width: 14px; height: 14px; }

/* ----------------------------------------------------------------------
   ITEM 1 — THE BUILD RING'S CARDS (`?r33ring=1`). The uibar pair's second
   named cost: with the bar showing BUILDINGS the in-world ring still drew the
   round weapon marks (C vs D pixel-identical), so the two surfaces that name
   the same four towers spoke two icon languages. The card's 38 px medallion,
   its gold rim, the tint, the drop shadow and the hover rule are all r31's;
   only the drawing inside changes, and a 24 x 30 building in a 38 px disc
   takes the disc's own clearance (the weapon glyph ran 25 x 25 in it). */
/* `.bm-icon svg` already sets 25 x 25, and it out-specifies a bare class, so
   the building is selected THROUGH the disc's own rule. */
.bm-icon svg.bm-tw {
  width: 23px; height: 29px; color: var(--parchment);
  filter: drop-shadow(0 0 5px var(--tint)) drop-shadow(0 1px 1px rgba(0, 0, 0, .7));
}
.bm-item:hover .bm-icon svg.bm-tw { color: #fff8e6; }

/* ======================================================================
   r34 UI — THE FOURTH BAR and the round's HUD items
   ----------------------------------------------------------------------
   Every rule below is behind an r34 class (`#bottom-rail.r34ui*` or
   `html.r34ui*`), one class per flag per SURFACE, so with no flag set nothing
   here can be reached and the shipped HUD is byte-identical to HEAD.

   ITEM 0 — THE FOURTH BAR (`?r34uibar=1`). The mini-drift tied the third bar
   with r31's (32-32, first places 4-2 against) and wrote the recipe in both
   halves: all six credited the third bar's ANATOMY ("portrait chip, name on
   the top line, coin glyph + cost on its own line, no glyph clipped"; "the
   silhouettes read as buildings") and all six charged its SPACING (the
   tray-to-wave clearance 43 -> 14 px, "butted plates vs ruled slots", "Frost
   Totem ends 3 px inside the plate's edge", the wave title colliding with the
   tray at build) — and all six named the shared ceiling: no selected state on
   the bar, 'Next' twice, the wave header jumping 34 px between screens.

   So: the third bar's slot inside r31's GRID —
     * r31's flat well and hairline RULES between slots (no per-slot plates);
       the rule is a cut groove (the r31 gold hairline, a dark wall inside it),
       0 px wider than r31's;
     * the air is measured ink-to-ink: a name clears its own cell's rule by
       >= 4 px, and a slot's last ink clears the next portrait by >= 8 px
       with the rule between (obs 3 K6: "8-10 px of air between cards");
     * the portrait-to-own-name gap is 3 px, so every label is nearer its own
       picture than its neighbour's (K6's grouping failure, inverted);
     * the WAVE CAPTION IS PINNED: it no longer centres on its own text (the
       34 px jump between mid's "Grave Cohort" and build's "Skittering
       Vanguard"), it is left-aligned to the pip TRACK's left end, so every
       caption in the game starts at the same x and the clearance to the tray
       is the same number on every screen;
     * the pip track's padding goes back to its own r22 base (10 px), which
       narrows the centred row so the wider tray still has air before the
       left flourish; the Hold track pays the widest caption's overhang. */
#bottom-rail.r34uibar #armoury {
  /* the 40 px portrait needs the rail body's whole 42 px (r32/r33's own
     measure); the well's side padding goes to the cells, which carry it. */
  top: 8px; height: 42px; padding: 0;
}
#bottom-rail.r34uibar .am-cell {
  height: 40px; gap: 3px; align-items: center; padding: 0 4px 0 3px;
  /* THE RULE: r31's gold hairline, with a dark wall on its inner side — the
     toolbar's own cut-groove language, drawn as a border and an inset shadow
     on a box that already existed. */
  border-left: 1px solid rgba(255, 207, 77, .30);
  box-shadow: inset 1px 0 0 rgba(0, 0, 0, .50);
  border-radius: 0;
}
#bottom-rail.r34uibar .am-cell:first-child { border-left: 0; box-shadow: none; padding-left: 2px; }
#bottom-rail.r34uibar .am-cell:last-child { padding-right: 4px; }
/* THE PORTRAIT — the third bar's (= r32's) to the decimal: the r31 medallion
   colour rule (a dark field, the tower's tint on a 1.5 px inset ring). */
#bottom-rail.r34uibar .am-por {
  position: relative; width: 34px; height: 40px; flex: none; border-radius: 3px;
  display: flex; align-items: center; justify-content: center;
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--tint) 22%, #140d07), color-mix(in srgb, var(--tint) 8%, #0c0804) 58%, #070402);
  box-shadow:
    inset 0 0 0 1.5px color-mix(in srgb, var(--tint) 78%, #2a1c0b),
    inset 0 0 0 2.5px rgba(0, 0, 0, .75),
    0 1px 3px rgba(0, 0, 0, .6);
  overflow: hidden;
}
#bottom-rail.r34uibar .am-sil {
  width: 24px; height: 30px; color: var(--parchment);
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .85));
}
#bottom-rail.r34uibar .am-txt { justify-content: center; gap: 0; }

/* THE STATES. A selected / armed slot (the tower you have selected, or the
   ring card under the pointer while a pad is open) LIFTS ITS FILL — obs 3 #10
   measured r33's selected cell at CR 1.06 : 1 against its neighbours ("a
   1.5 px hairline"); this fill is >= 1.5 : 1 by construction — MEASURED on
   the rendered build frame the first fill (#5a4524 / #43331b) cleared its
   dimmed neighbour by only 1.52 : 1 (the plate lines take ~9 % off the
   token), so it sits one step lighter (#66512b / #4d3b1f, token relative
   luminance ~0.067, the parchment name still ~7.6 : 1 on it) — with a 1 px bright-gold ring on the
   cell and (a selected TOWER, no pad open) the portrait's rim in gold — while a
   pad is open the armed slot keeps its tower-colour rim (the `.building` rim
   rule out-specifies it; MEASURED on the build frame) and the cell's ring and
   fill carry the state. An affordable slot while a pad is open
   brightens its silhouette and rim (a quiet "available", not r33's gold
   frame on every slot — the states-build observer's own cost: "the currency
   colour as decoration, lit on every slot regardless of state"), and an
   unaffordable one keeps r19's tint (desaturated, dimmed, the price red),
   one step deeper while a pad is open. */
#bottom-rail.r34uibar.building .am-cell:not(.cant) {
  border-color: rgba(255, 207, 77, .30);
  box-shadow: inset 1px 0 0 rgba(0, 0, 0, .50);
  background: none;
}
#bottom-rail.r34uibar.building .am-cell:first-child:not(.cant) { box-shadow: none; }
#bottom-rail.r34uibar.building .am-cell:not(.cant) .am-sil { color: #fff6e0; }
#bottom-rail.r34uibar.building .am-cell:not(.cant) .am-por {
  box-shadow:
    inset 0 0 0 1.5px color-mix(in srgb, var(--tint) 100%, #2a1c0b),
    inset 0 0 0 2.5px rgba(0, 0, 0, .75),
    0 1px 3px rgba(0, 0, 0, .6);
}
#bottom-rail.r34uibar .am-cell.sel,
#bottom-rail.r34uibar.building .am-cell.sel:not(.cant) {
  background: linear-gradient(180deg, #66512b, #4d3b1f);
  box-shadow: inset 0 0 0 1px #ffe08a, inset 0 0 0 2px rgba(0, 0, 0, .55);
}
#bottom-rail.r34uibar .am-cell.sel .am-name { color: var(--parchment); }
#bottom-rail.r34uibar .am-cell.sel .am-sil { color: #fffaf0; }
#bottom-rail.r34uibar .am-cell.sel .am-por {
  box-shadow:
    inset 0 0 0 1.5px #ffe08a,
    inset 0 0 0 2.5px rgba(0, 0, 0, .75),
    0 0 6px rgba(255, 207, 77, .45);
}
#bottom-rail.r34uibar .am-cell:hover:not(.cant) { border-color: rgba(255, 207, 77, .30); }

@media (min-width: 1888px) {
  #bottom-rail.r34uibar .am-cell { gap: 3px; padding: 0 4px 0 3px; }
  #bottom-rail.r34uibar .am-cell:first-child { padding-left: 2px; }
  #bottom-rail.r34uibar .am-cell:last-child { padding-right: 4px; }
  /* the seat: the track's own r22 padding (r31's 28 px was the counterweight
     for r31's medallions; this bar pays its own way instead). */
  #bottom-rail.r34uibar #wave-pips { padding: 0 10px; }
  /* the Hold's gauge pays the widest caption's overhang past the row (see the
     r34 UI log for the arithmetic) — scorer 2's own charge on r31's bar: "the
     Hold bar stretches 430 px of near-empty green for one value". */
  #bottom-rail.r34uibar .hg-track { width: 340px; }
}
/* THE PIN. The caption stops centring on its own text: it is a zero-width box
   at the pip track's left end (after the 48 px flourish) and its text runs
   right from there, so it can never set the seat's width and every caption
   starts at the same x. */
#bottom-rail.r34uibar .rail-cap { align-self: flex-start; width: 0; margin-left: 48px; }

/* ----------------------------------------------------------------------
   r34 ITEM 2 — THE NEXT CELL'S NAME WITHOUT THE WIDTH (`?r34uinext=1`).
   next-mid read the wave's name in the cell 7-4 FOR and refused the flag 6-7
   on layout: the cell grew 106 px and drove the Hold plate 34 px through the
   centre headline, and the name was printed twice. Here the header drops its
   copy (ui.js: "Next · Wave 12"), and the CLUSTER keeps r31's footprint:
   #hold-gauge is pinned at r31's left edge and the Hold's track is the flex
   item that pays for the cell, per wave, so no wave's name can move the plate
   by a pixel. (The name cannot live at the CELL's r31 width at a readable
   face — measured in the r34 UI log.) */
#bottom-rail.r34uinext .hg-u { white-space: nowrap; }
@media (min-width: 1888px) {
  #bottom-rail.r34uinext #hold-gauge { left: calc(50% + 239.75px); }
  /* with the fourth bar too, the caption has already dropped the name, so the
     widest caption the bar's pin has to clear is "Next · Wave 12" (~157 px)
     and the Hold keeps r31's left edge (MEASURED: pinned at 1252 the combined
     track fell to 184 px at build; at r31's edge it is 236-346 px, the same
     range as `?r34uinext=1` alone). */
  #bottom-rail.r34uinext.r34uibar #hold-gauge { left: calc(50% + 239.75px); }
  #bottom-rail.r34uinext .hg-body { flex: 1 1 auto; min-width: 0; }
  #bottom-rail.r34uinext .hg-track,
  #bottom-rail.r34uinext.r34uibar .hg-track { width: auto; }
}

/* ----------------------------------------------------------------------
   r34 ITEM 3b — THE THREE EMPTY PIPS (`?r34uipips=1`), and nothing else.
   pips-mid refused the lighter TRACK (12 of 15 pips lost contrast). The
   empty socket cannot reach 3 : 1 by going darker — against the track's
   relative luminance ~0.046 even pure black tops out at 1.91 : 1 — and a
   lighter FILL at >= 3 : 1 (L* >= 56) would sit on the cleared gold gem's
   own L* 58 ("gone milky"). So the socket stays a near-black socket and its
   RIM is lifted: a parchment hairline (L* ~78) that reads >= 3 : 1 against
   the track while the core still says "empty". The cleared, current, next
   and boss pips are untouched. */
#bottom-rail.r34uipips .pip:not(.done):not(.now):not(.next):not(.boss) {
  border-color: #cdbfa3;
}

/* ----------------------------------------------------------------------
   r34 ITEM 5 — THE HUD'S SCALE, split by SURFACE from the start.
   size-mid shipped gold and lives at a 16 px cap and named the cost: the wave
   chip's digits and FOES stayed 12 — "the same class of datum at two sizes".
     * `?r34uisize=1` — the TOP STRIP: the wave chip's digits to the gold /
       lives face (23 px -> a >= 16 px lining cap); its "Wave" label keeps its
       own 14 px tier. The chip grows rightwards into the topbar's spacer.
     * `?r34uifoes=1` — the BOTTOM BAR: the Foes and Next counts to the same
       face. The label-over-value stack has to fit the 36 px cell, so the two
       line boxes tighten (label 16 + value 18 = 34), which moves no glyph
       into another (the log measures the ink gap). */
html.r34uisize #chip-wave > span { font-size: 23px; line-height: 1; }
#bottom-rail.r34uifoes .hg-fbody { gap: 0; }
#bottom-rail.r34uifoes .hg-fbody .hg-label { line-height: 16px; }
#bottom-rail.r34uifoes .hg-foes span,
#bottom-rail.r34uifoes .hg-next span { font-size: 23px; line-height: 18px; }

/* ----------------------------------------------------------------------
   OBSERVERS' ADDITION (UI, crop K6) — THE ARCHER RING IS 20.5 dE FROM THE
   CANNON'S (`?r34uihue=1`). Every other ring pair is 64.8-86.6 dE apart; the
   two warm ones are "one warm blob". The archer's identity tint moves to a
   verdant green: its ring (the tint mixed 78 % over #2a1c0b) lands at Lab
   h 131 — 67 deg from the Cannon's, 109 from the Frost's, 177 from the
   Arcane's, dE >= 64 to every other ring. It is the identity TOKEN, so it
   moves on every surface that carries it (the bar, the build ring's card,
   the tower panel's medal) — two archer colours in one screenshot would be
   r33's "two icon languages" again. The range ring keeps its own gold. */
html.r34uihue [data-type="archer"] { --tint: #6cc04a; }

/* ======================================================================
   r35 UI — THE BAR'S ICONS, ITS LAYOUT DEFECTS, THE POSTER FRAMES, THE
   ARCHER'S VALUE, THE START CARD'S TIERS
   ----------------------------------------------------------------------
   Every rule below is behind an r35 class (`#bottom-rail.r35ui*` or
   `html.r35ui*`), one class per flag per SURFACE, so with no flag set
   nothing here can be reached and the shipped frame is byte-identical to
   HEAD. The bar's rules are scoped to the fourth bar (`.r34uibar`, shipped)
   so a flag cannot land on r31's medallion markup under `?r34uibar=0`.

   ITEM 0 — THE TYPE EMBLEMS (`?r35uiglyph=1`). The fourth bar won the
   mini-drift 6/6 on its grid and selected state and lost the full-frame
   drift on its ICONS: "four near-identical white tower silhouettes" against
   r31's type glyphs that "say what each tower does"; all six mini-drift
   scorers: "flat cream one-colour silhouettes on ~30-33 px tiles ... chess
   pieces". The grid, the rules and the states are untouched; the tile is
   the KR / BTD6 SQUARE build tile (40 x 40, the rail body's full height)
   and carries r31's bow / cannon / gem / snowflake in the HUD's emblem kit
   (index.html #th-ty-*) on a field lit in the tower's own tint — the
   tower's identity is a FIELD now, not a 1.5 px rim, so the four slots
   part by colour at full frame (the drift's read) and by shape at 1:1.
   THE WIDTH: 40 px is the budget, not the art director's 48-56. Every px of
   tile width costs the bar 4 px (four slots), and the seat has to clear the
   tray by >= 40 px (r34's do-not-regress) with the widest caption in the
   game (`Next · Wave 3 — Skittering Vanguard`, 407 px), so the Hold's track
   pays it (the log has the arithmetic). The tray grows 24 px; the seat moves
   right by the same 24 so every r34 clearance holds to the pixel, and the
   Hold gives back 16 px of track so the widest caption still clears it. */
#bottom-rail.r34uibar.r35uiglyph { --r35uigrow: 24px; }
#bottom-rail.r34uibar.r35uiglyph .am-por {
  width: 40px; height: 40px; border-radius: 4px;
  background: radial-gradient(120% 95% at 50% 16%,
    color-mix(in srgb, var(--tint) 60%, #1a120a) 0%,
    color-mix(in srgb, var(--tint) 34%, #110b06) 55%,
    color-mix(in srgb, var(--tint) 14%, #060402) 100%);
}
#bottom-rail.r34uibar.r35uiglyph .am-sil {
  width: 32px; height: 32px; filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .8));
}
@media (min-width: 1888px) {
  #bottom-rail.r34uibar.r35uiglyph:not(.r35uiseat) .rail-tl { left: calc(50% + 24px); }
  #bottom-rail.r34uibar.r35uiglyph:not(.r35uiseat) .hg-track { width: 324px; }
}

/* ----------------------------------------------------------------------
   ITEM 1 — THE SEAT (`?r35uiseat=1`). Every mini-drift scorer: the build
   caption "left-anchored to the pip capsule ... runs ~95 px past the
   capsule's right end", and "~95 px of dead bar between the wave track's
   flourish (x~1157) and the Hold panel (x~1253) ... the Hold bar squeezed to
   ~337 px". So the caption goes back to CENTRED on its pill (r31's, the
   third bar's — "the wave title sits exactly centred on the pip track"), and
   what r33's third bar lost that way (the tray collision, 11 px) is paid by
   the SEAT, not the caption: the seat sits 40 px right of the frame's centre
   (+ the tray's own growth), so the widest caption clears the tray by 44.7 px
   on every screen, and the Hold's plate closes on the seat's right end at
   16 px (bar3's "tight to the wave block, ~17 px") — the dead band is gone
   and the track takes the rest (340 -> 373 px). */
#bottom-rail.r34uibar.r35uiseat .rail-cap { align-self: center; width: auto; margin-left: 0; }
@media (min-width: 1888px) {
  #bottom-rail.r34uibar.r35uiseat .rail-tl { left: calc(50% + 40px + var(--r35uigrow, 0px)); }
  #bottom-rail.r34uibar.r35uiseat .hg-track { width: calc(373px - var(--r35uigrow, 0px)); }
}

/* ----------------------------------------------------------------------
   ITEMS 1 + 4 — THE BAR'S FIGURES (`?r35uifigs=1`), the type details the
   scorers named, one surface (the bar's numerals and their labels):
     * the caption's number: "the old-style gold '3' sinks below the
       small-caps baseline" — the display face's small-caps feature swaps
       the digits for small-cap (old-style) figures; the number drops the
       caps variant and prints a lining numeral on the capitals' line;
     * "'6   FOES' has a gap about three letters wide": the count's 2ch
       reservation (a two-digit slot) goes; the cell's width is set by its
       "Coming" label, so no count can move the cluster;
     * "the Hold bar has no number at all": the count on the label row,
       right-aligned over the track's end (ui.js, the node exists only here);
     * "the 'FOES' label nearly touches its number (2 rows)" (uifoes' named
       cost): label up 1 px, value down 1 px — 5 bare rows, 2 to each rim. */
#bottom-rail.r35uifigs .rail-cap .n { font-variant-caps: normal; font-variant-numeric: lining-nums tabular-nums; }
#bottom-rail.r35uifigs .hg-next span { min-width: 0; }
#bottom-rail.r35uifigs .hg-foes .hg-label,
#bottom-rail.r35uifigs .hg-next .hg-label { transform: translateY(-1px); }
#bottom-rail.r35uifigs .hg-foes span,
#bottom-rail.r35uifigs .hg-next .hg-val { transform: translateY(1px); }
#bottom-rail.r35uifigs .hg-lrow { display: flex; align-items: baseline; justify-content: space-between; }
#bottom-rail.r35uifigs .hg-num {
  font-family: var(--display-font); font-weight: 700; font-size: 17.5px; line-height: 1;
  color: var(--parchment); font-variant-numeric: lining-nums tabular-nums; white-space: nowrap;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .8);
}
#bottom-rail.r35uifigs .hg-num b { font-weight: 700; }
#bottom-rail.r35uifigs .hg-num i { font-style: normal; font-size: 15px; color: var(--parchment-dim); margin-left: 4px; }

/* ----------------------------------------------------------------------
   ITEM 2 — THE POSTER FRAMES (`?r35uiposter=1`). The r34 probe pair
   (lightposter-start 7-4 for the scrim REMOVED) and CAM's `?r34camposter`
   frames: the black radial scrim cuts the start / victory world to sRGB gain
   0.56-0.82 (the sun 98.3 -> 74.2 L*, colourfulness 80.7 -> 58.8), and in the
   dimmed frame the card's dark edge MERGES into the dark world (34 % of the
   ring within 10 L* of the card vs 4 %). The card is a 0.97-opaque plate
   with its own gold keyline and its own 24 / 72 px shadow (r27) — it never
   leaned on the scrim — so the scrim goes on the two poster screens; the
   loading card keeps it. */
html.r35uiposter #screen-start,
html.r35uiposter #screen-end { background: none; }

/* ----------------------------------------------------------------------
   ITEM 3 — THE ARCHER'S GREEN AT A VALUE (`?r35uiarcher=1`). r34uihue's
   named cost: the green ring (L* 58.4) and the cannon's orange (60.4) share
   a value, so under protan simulation Archer vs Cannon falls 15.3 -> 3.4
   dE00 (deutan 10.9 -> 6.7). The hue DIRECTION is r34's (shipped, h 131);
   the value goes down a step — the ring (the tint mixed 78 % over #2a1c0b)
   lands at L* 42.7 / C* 49 / h 134, 17.8 L* under the cannon's ring, and the
   one token moves on every surface that carries it, as r34's did. */
html.r35uiarcher [data-type="archer"] { --tint: #3b8a2e; }

/* ----------------------------------------------------------------------
   ITEM 4 — THE START CARD'S TYPE TIERS (`?r35uistart=1`, the start card
   only). pips-start shipped the rules row at a 13 px cap and named the
   cost: "the rules' cap now EQUALS the subtitle's 13 px (two tiers, one
   size) and 68 % of the CTA's, the row reads as one dense band (gutters
   3.4x cap vs 4.8x)". The critics praise the card's "spaced serif small
   caps", so the STYLE stays and the tiers separate by SIZE and VALUE: the
   subtitle one step up and to full parchment, the rules one step down and
   dimmer (still >= 12 px of cap — r33's legibility floor for them was 9),
   and the rules' gutters opened so the row reads as three items. */
html.r35uistart #screen-start .game-sub { font-size: 19px; letter-spacing: .2em; color: var(--parchment); }
html.r35uistart #screen-start .howto { gap: 64px; }
html.r35uistart #screen-start .ht-item { font-size: 15px; letter-spacing: .12em; color: #b9aa8c; }

/* ======================================================================
   LAUNCH ROUND (gameplay/UX fixer, 2026-10-11) — docs/launch-fix-gameplay.md
   Small screens, keyboard hints, pause label, rotate card. Everything below
   is layout only; no art token is changed.
   ====================================================================== */

/* S4 — the keyboard number on each build card, and the start card's key line.
   Hidden on touch-first devices, where there is no keyboard to press. */
.bm-key {
  position: absolute; top: 4px; left: 5px; z-index: 2;
  min-width: 15px; height: 15px; padding: 0 3px; border-radius: 3px;
  font: 700 11px/15px var(--ui-font); text-align: center; color: #2a1a05;
  background: var(--gold-soft); box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
  pointer-events: none;
}
.keys-hint {
  /* launch: on the hint's own plate — bare text over the world read as dark-on-busy */
  display: table; margin: 8px auto 0; padding: 5px 16px; border-radius: 4px;
  background: var(--plate-lines), var(--plate);
  border: 1px solid var(--gold-edge);
  box-shadow: var(--plate-shadow), var(--plate-ring);
  color: var(--parchment); font-size: 14px; letter-spacing: .03em;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .9);
}
.keys-hint kbd {
  display: inline-block; min-width: 18px; padding: 1px 5px; margin: 0 1px; border-radius: 3px;
  font: 700 12px/16px var(--ui-font); color: var(--parchment);
  background: rgba(20, 14, 9, .85); border: 1px solid var(--gold-edge);
}
@media (hover: none), (pointer: coarse) {
  .bm-key, .keys-hint { display: none; }
}
/* the key line adds ~30 px to the start card: keep a 720 px laptop clear */
@media (max-height: 760px) {
  #screen-start .screen-inner { padding-top: 8px; padding-bottom: 8px; }
}

/* N4 — a readable paused state: a faint dim and a label under the chips. */
#pause-label { display: none; }
body.th-paused #pause-label {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  position: absolute; top: 96px; left: 50%; transform: translateX(-50%);
  padding: 10px 30px; border-radius: 4px; pointer-events: none;
  background: var(--plate-lines), var(--plate); border: 1px solid var(--gold-edge);
  box-shadow: var(--plate-shadow), var(--plate-ring), 0 0 0 100vmax rgba(0, 0, 0, .16);
  color: var(--parchment); font-family: var(--display-font); text-align: center;
}
#pause-label span { font-size: 24px; font-variant-caps: small-caps; letter-spacing: .14em; font-weight: 700; }
#pause-label small { font-size: 12.5px; color: var(--parchment-dim); letter-spacing: .03em; }

/* S2 — the Send Next Wave call no longer sits on the centre of the island,
   where the best pads are: it docks bottom-right, over the Hold gauge. Only
   the button takes the pointer. */
#wave-cta { left: auto; right: 16px; transform: none; text-align: right; pointer-events: none; }
#btn-callwave { pointer-events: auto; }

/* B1 — phone held upright. The island is wider than it is tall, so a narrow
   portrait viewport shows 4 of 14 pads and pushes the controls off-screen:
   ask for landscape (ui.js also pauses the clock while this is up). */
#rotate-overlay { display: none; }
@media (orientation: portrait) and (max-width: 700px) {
  #rotate-overlay {
    display: flex; align-items: center; justify-content: center;
    position: fixed; inset: 0; z-index: 1000; padding: 24px;
    background: radial-gradient(ellipse at 50% 45%, rgba(34, 25, 15, .97), rgba(8, 6, 3, .99));
    color: var(--parchment); font-family: var(--display-font); text-align: center;
  }
  .ro-card { max-width: 300px; }
  .ro-phone {
    width: 44px; height: 76px; margin: 0 auto 26px; border-radius: 8px;
    border: 3px solid var(--gold-line); box-shadow: 0 0 18px rgba(255, 207, 77, .25);
    animation: roTurn 2.4s ease-in-out infinite;
  }
  .ro-title { font-size: 28px; font-variant-caps: small-caps; letter-spacing: .12em; font-weight: 700; margin: 0 0 8px; }
  .ro-sub { font-size: 16px; color: var(--parchment-dim); margin: 0; }
  @keyframes roTurn { 0%, 25% { transform: rotate(0deg); } 55%, 100% { transform: rotate(-90deg); } }
}

/* S5 / B1 — narrow screens: the rail's centre caption and wave pips collide
   with the armoury prices and the Hold gauge below ~1200 px; the wave chip
   already says "Wave n / 15", so the seat steps aside. */
/* N9 — "THE HOLD20 / 20": the label and the count touched at 1280 px. */
#bottom-rail.r35uifigs .hg-lrow { gap: 8px; }

@media (max-width: 1199px) {
  #bottom-rail .rail-tl { display: none; }
}

/* S5 — phone landscape (844 x 390 and other short screens). */
@media (max-height: 450px) {
  .stat-chip { height: 40px; font-size: 15px; padding: 0 14px 0 10px; }
  .stat-chip .ic { width: 22px; height: 22px; }
  #wave-banner { top: 64px; font-size: 18px; padding: 6px 20px; }
  body.th-paused #pause-label { top: 60px; padding: 6px 20px; }
  #toast { bottom: 124px; }   /* clear of the docked Send Next Wave call */
  #wave-cta { bottom: 60px; }
  #btn-callwave { font-size: 16px; padding: 7px 16px; }
  #wave-timer { margin-top: 4px; }
  #tower-panel { bottom: 60px; transform: scale(.78); transform-origin: 0 100%; }
  .screen-inner { padding: 8px; }
  .panel-frame { padding: 16px 40px 14px; }
  .game-title { font-size: 44px; }
  .title-rule { margin: 0 auto 6px; }
  .game-sub { margin: 0 0 10px; font-size: 15px; }
  .big-btn { font-size: 21px; padding: 8px 28px; }
  .howto, html.r35uistart #screen-start .howto { margin-top: 12px; gap: 36px; }
  .ht-item { gap: 4px; }
  .hint { margin-top: 8px; padding: 5px 16px; font-size: 13px; }
  .end-award { margin: 0 0 10px; }
  .ea-stars { margin-bottom: 6px; }
}

/* Below 1400 px the rail's long captions ("Next · Wave 3 — Skittering
   Vanguard") ran up to 14 px into the Hold gauge at 1280 (measured, launch
   round), and the pips' flourish pair 14 px once the Hold's "Coming" cell
   appears at 1280: the caption steps down one size and the two flourishes
   step aside there. */
@media (max-width: 1399px) {
  #bottom-rail .rail-cap { font-size: 15px; letter-spacing: .05em; min-height: 15px; }
  #bottom-rail .rail-fl { display: none; }
}
